├── .changeset
├── README.md
├── config.json
├── curly-parents-peel.md
├── early-dots-repair.md
├── eleven-dryers-clean.md
├── five-badgers-greet.md
├── gold-hats-kneel.md
├── gorgeous-rabbits-sell.md
├── green-owls-train.md
├── healthy-flies-rescue.md
├── lovely-bugs-prove.md
├── mean-dodos-smoke.md
├── old-monkeys-drum.md
├── orange-seahorses-listen.md
├── pre.json
├── real-clocks-cheer.md
├── rotten-fireants-flow.md
├── serious-lizards-invite.md
├── soft-doors-play.md
├── spicy-wasps-agree.md
├── strange-parrots-smoke.md
├── strange-pianos-move.md
├── tasty-houses-drum.md
├── tasty-humans-clean.md
├── twelve-islands-carry.md
└── unlucky-brooms-melt.md
├── .github
├── actions
│ └── setup
│ │ └── action.yml
└── workflows
│ ├── pr-release.yml
│ ├── semantic-pr.yml
│ └── test.yaml
├── .gitignore
├── .golangci.yml
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── dictionaries
│ └── develar.xml
├── encodings.xml
├── go.imports.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── app-builder.iml
├── go.mod
├── go.sum
├── index.d.ts
├── index.js
├── main.go
├── package.json
├── pkg
├── archive
│ └── zipx
│ │ └── unzip.go
├── blockmap
│ ├── blockmap.go
│ ├── blockmap_test.go
│ └── command.go
├── codesign
│ └── p12.go
├── download
│ ├── ActualLocation.go
│ ├── Part.go
│ ├── artifactDownloader.go
│ ├── downloader.go
│ └── tool.go
├── electron
│ ├── electronDownloader.go
│ └── electronUnpack.go
├── fs
│ ├── copier.go
│ ├── file.go
│ └── findParent.go
├── icons
│ ├── collect-icons.go
│ ├── error.go
│ ├── fileResolver.go
│ ├── icns-to-png.go
│ ├── icns.go
│ ├── icnsToPngUsingOpenJpeg.go
│ ├── ico.go
│ ├── icon-converter.go
│ ├── icon-converter_test.go
│ ├── icons-api.go
│ └── image-util.go
├── linuxTools
│ └── tool.go
├── log
│ └── log.go
├── node-modules
│ ├── cli_test.go
│ ├── es5-demo
│ │ ├── package.json
│ │ └── pnpm-lock.yaml
│ ├── helper_test.go
│ ├── nodeModuleCollector.go
│ ├── nodeModuleCollector_test.go
│ ├── npm-demo
│ │ ├── package-lock.json
│ │ └── package.json
│ ├── parse-demo
│ │ ├── package.json
│ │ └── yarn.lock
│ ├── pnpm-demo
│ │ ├── package.json
│ │ └── pnpm-lock.yaml
│ ├── rebuild.go
│ ├── tar-demo
│ │ ├── package-lock.json
│ │ └── package.json
│ ├── tree.go
│ └── yarn-demo
│ │ ├── package.json
│ │ ├── packages
│ │ ├── foo
│ │ │ └── package.json
│ │ └── test-app
│ │ │ ├── index.html
│ │ │ ├── index.js
│ │ │ └── package.json
│ │ └── yarn.lock
├── package-format
│ ├── appimage
│ │ ├── appImage.go
│ │ ├── appLauncher.go
│ │ ├── configuration.go
│ │ └── templates
│ │ │ └── AppRun.sh
│ ├── bindata.go
│ ├── dmg
│ │ ├── dmg-win.go
│ │ ├── dmg.go
│ │ └── dmg_test.go
│ ├── fpm
│ │ └── fpm.go
│ ├── proton-native
│ │ └── protonNative.go
│ └── snap
│ │ ├── desktop-scripts
│ │ ├── desktop-common.sh
│ │ ├── desktop-gnome-specific.sh
│ │ └── desktop-init.sh
│ │ ├── snap.go
│ │ ├── snapScripts.go
│ │ ├── snapStore.go
│ │ └── snap_test.go
├── plist
│ └── plist.go
├── publisher
│ └── s3.go
├── rcedit
│ └── rcedit.go
├── remoteBuild
│ ├── RemoteBuilder.go
│ ├── buildAgentEndpoint.go
│ └── tls.go
├── util
│ ├── async.go
│ ├── cancel.go
│ ├── env.go
│ ├── exec.go
│ ├── json-util.go
│ ├── messageError.go
│ ├── osName.go
│ ├── proxy.go
│ ├── tempfile.go
│ ├── util.go
│ └── wsl.go
├── wine
│ ├── wine.go
│ └── wine_test.go
└── zap-cli-encoder
│ ├── arrayEncoder.go
│ ├── consoleEncoder.go
│ └── consoleEncoder_test.go
├── pnpm-lock.yaml
├── readme.md
├── scripts
└── build.sh
└── testData
├── 512x512.png
├── icon-jpeg2.icns
├── icon.icns
├── icon.ico
└── info.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3 | "changelog": ["@changesets/changelog-github", { "repo": "develar/app-builder" }],
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "master",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/.changeset/curly-parents-peel.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: handle the table of content resource type correctly
6 |
--------------------------------------------------------------------------------
/.changeset/early-dots-repair.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: to resolve appimage issues in electron builder, and since we can't update electron-builder-binaries repo, we should just downgrade to the last working version of appimage
6 |
--------------------------------------------------------------------------------
/.changeset/eleven-dryers-clean.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix(snap): Parse user command line options as last values
6 |
--------------------------------------------------------------------------------
/.changeset/five-badgers-greet.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: pnpm install error for node module collector (https://github.com/electron-userland/electron-builder/issues/8519)
6 |
--------------------------------------------------------------------------------
/.changeset/gold-hats-kneel.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: fix for handling native dependencies, such as `tar` node module
6 |
--------------------------------------------------------------------------------
/.changeset/gorgeous-rabbits-sell.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": minor
3 | ---
4 |
5 | feat: allow providing env var for custom app-builder binary as opposed to accessing directly from the PATH env var
6 |
--------------------------------------------------------------------------------
/.changeset/green-owls-train.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: alias name issue in node modules resolution dependency tree
6 |
--------------------------------------------------------------------------------
/.changeset/healthy-flies-rescue.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": minor
3 | ---
4 |
5 | feat: add s3ForcePathStyle option for s3 publisher
6 |
--------------------------------------------------------------------------------
/.changeset/lovely-bugs-prove.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | change node module symlink to real path
6 |
--------------------------------------------------------------------------------
/.changeset/mean-dodos-smoke.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: find the real parent node module
6 |
--------------------------------------------------------------------------------
/.changeset/old-monkeys-drum.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": minor
3 | ---
4 |
5 | Added support for OpenSUSE to rpm
6 |
--------------------------------------------------------------------------------
/.changeset/orange-seahorses-listen.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": major
3 | ---
4 |
5 | chore: changing repo structure for release automation
6 |
--------------------------------------------------------------------------------
/.changeset/pre.json:
--------------------------------------------------------------------------------
1 | {
2 | "mode": "pre",
3 | "tag": "alpha",
4 | "initialVersions": {
5 | "app-builder-bin": "4.2.0"
6 | },
7 | "changesets": [
8 | "curly-parents-peel",
9 | "early-dots-repair",
10 | "eleven-dryers-clean",
11 | "five-badgers-greet",
12 | "gold-hats-kneel",
13 | "gorgeous-rabbits-sell",
14 | "green-owls-train",
15 | "healthy-flies-rescue",
16 | "lovely-bugs-prove",
17 | "mean-dodos-smoke",
18 | "old-monkeys-drum",
19 | "orange-seahorses-listen",
20 | "real-clocks-cheer",
21 | "rotten-fireants-flow",
22 | "serious-lizards-invite",
23 | "soft-doors-play",
24 | "spicy-wasps-agree",
25 | "strange-parrots-smoke",
26 | "strange-pianos-move",
27 | "tasty-houses-drum",
28 | "tasty-humans-clean",
29 | "twelve-islands-carry",
30 | "unlucky-brooms-melt"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.changeset/real-clocks-cheer.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: revert appimage 13.0.1 to 13.0.0 due to mksquash arch compilation issues
6 |
--------------------------------------------------------------------------------
/.changeset/rotten-fireants-flow.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: Use npm config.mirror first before env variables for download URL
6 |
--------------------------------------------------------------------------------
/.changeset/serious-lizards-invite.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix current mksquashfs version only allows xz and gzip compressions
6 |
--------------------------------------------------------------------------------
/.changeset/soft-doors-play.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": minor
3 | ---
4 |
5 | feat: adding env var for "dirname" to mirror the logic in electron-builder
6 |
--------------------------------------------------------------------------------
/.changeset/spicy-wasps-agree.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | feat: resolve all the pnpm issues without hostied config
6 |
--------------------------------------------------------------------------------
/.changeset/strange-parrots-smoke.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: set correct compression enums and remove default
6 |
--------------------------------------------------------------------------------
/.changeset/strange-pianos-move.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | chore: Update extract logic for using newer 7zz/7zzs/7zr.exe binaries
6 |
--------------------------------------------------------------------------------
/.changeset/tasty-houses-drum.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: hoist dependencies to the real parent in nodeModuleCollector
6 |
--------------------------------------------------------------------------------
/.changeset/tasty-humans-clean.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": minor
3 | ---
4 |
5 | feat: Add loongarch64 support
6 |
--------------------------------------------------------------------------------
/.changeset/twelve-islands-carry.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": minor
3 | ---
4 |
5 | feat: add flatten option to `node-dep-tree` for rendering dependency conflicts in a different manner
6 |
--------------------------------------------------------------------------------
/.changeset/unlucky-brooms-melt.md:
--------------------------------------------------------------------------------
1 | ---
2 | "app-builder-bin": patch
3 | ---
4 |
5 | fix: cannot find module(archiver-utils)
6 |
--------------------------------------------------------------------------------
/.github/actions/setup/action.yml:
--------------------------------------------------------------------------------
1 | name: 'pnpm installation'
2 | description: 'Install and audit dependencies via pnpm'
3 | inputs:
4 | version: # id of input
5 | description: 'The pnpm version to use'
6 | required: false
7 | default: 8.9.0
8 |
9 | runs:
10 | using: 'composite'
11 | steps:
12 | - name: Setup pnpm
13 | uses: pnpm/action-setup@a3252b78c470c02df07e9d59298aecedc3ccdd6d # v3.0.0
14 | with:
15 | version: ${{ inputs.version }}
16 |
17 | - name: Set up Go
18 | uses: actions/setup-go@v4
19 | with:
20 | go-version: '1.21'
21 |
22 | - name: Install go packages
23 | run: go install && go install -a -v github.com/go-bindata/go-bindata/...@latest
24 | shell: bash
25 |
26 | - name: Setup node
27 | uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
28 | with:
29 | node-version: '18'
30 | cache: 'pnpm'
31 |
32 | - name: Install yarn
33 | run: pnpm install -g yarn
34 | shell: bash
35 |
36 | - name: Install dependencies
37 | run: pnpm install --frozen-lockfile
38 | shell: bash
39 |
40 | ## Usage
41 | # - name: install and audit
42 | # uses: ./.github/actions/pnpm
43 | # with:
44 | # version: ${{ env.PNPM_VERSION }}
45 |
--------------------------------------------------------------------------------
/.github/workflows/pr-release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | permissions: {}
9 | jobs:
10 | pr-release:
11 | permissions:
12 | contents: write # to create release (changesets/action)
13 | pull-requests: write # to create pull request (changesets/action)
14 |
15 | timeout-minutes: 15
16 | runs-on: macos-latest
17 | steps:
18 | - name: Checkout code repository
19 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
20 | with:
21 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
22 | fetch-depth: 0
23 |
24 | - name: Install deps and audit
25 | uses: ./.github/actions/setup
26 |
27 | - name: Set up NPM credentials
28 | run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
29 | env:
30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
31 |
32 | - name: Create versions PR & prepare publish
33 | id: changesets
34 | uses: changesets/action@v1
35 | with:
36 | version: pnpm ci:version
37 | commit: 'chore(deploy): Release'
38 | title: 'chore(deploy): Release'
39 | publish: pnpm ci:publish
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
43 |
--------------------------------------------------------------------------------
/.github/workflows/semantic-pr.yml:
--------------------------------------------------------------------------------
1 | name: "Semantic Versioning enforcer"
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - synchronize
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | main:
15 | permissions:
16 | pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs
17 | statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
18 | runs-on: ubuntu-latest
19 | steps:
20 | # Please look up the latest version from
21 | # https://github.com/amannn/action-semantic-pull-request/releases
22 | - uses: amannn/action-semantic-pull-request@e9fabac35e210fea40ca5b14c0da95a099eff26f # v5.4.0
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | master
7 | pull_request:
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 |
17 | build:
18 | runs-on: macos-latest
19 | steps:
20 | - uses: actions/checkout@v3
21 |
22 | - name: Setup and install deps
23 | uses: ./.github/actions/setup
24 |
25 | - name: Test
26 | run: make test
27 |
28 | - name: Build
29 | run: make build-all
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea/workspace.xml
3 | dist/
4 | vendor/
5 |
6 | /.idea/shelf/
7 | node_modules/
8 | mac/
9 | win/
10 | linux/
11 | app-builder
12 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters:
2 | enable:
3 | # - bodyclose
4 | - gocyclo
5 | # - prealloc
6 | - unconvert
7 | - unparam
8 | disable:
9 | - structcheck
10 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/dictionaries/develar.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Errorf
5 | KHTML
6 | appimage
7 | appimagekit
8 | asar
9 | blockmap
10 | chunking
11 | geoip
12 | goreleaser
13 | hasher
14 | hicolor
15 | htaccess
16 | icns
17 | iconset
18 | iconutil
19 | ksuid
20 | launchui
21 | libui
22 | lintian
23 | localappdata
24 | marshaler
25 | mkfs
26 | mksquashfs
27 | multipass
28 | noappend
29 | ostype
30 | pacman
31 | rcedit
32 | rpmbuild
33 | snapcore
34 | snapcraft
35 | tiffutil
36 | uintptr
37 | umask
38 | uname
39 | userland
40 | virtualgo
41 | wineprefix
42 | xattrs
43 | xzmt
44 | zstd
45 |
46 |
47 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/go.imports.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | env:
4 | global:
5 | - GO111MODULE=on
6 |
7 | cache:
8 | directories:
9 | - $HOME/.cache/go-build
10 | - $HOME/gopath/pkg/mod
11 |
12 | go:
13 | - 1.13.x
14 |
15 | script:
16 | - make build
17 | - make test
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # app-builder-bin
2 |
3 | ## 5.0.0-alpha.13
4 |
5 | ### Minor Changes
6 |
7 | - [#151](https://github.com/develar/app-builder/pull/151) [`9b2aaff`](https://github.com/develar/app-builder/commit/9b2aaffdb6cf16e1fda792df754e58181b2b47ce) Thanks [@mmaietta](https://github.com/mmaietta)! - feat: adding env var for "dirname" to mirror the logic in electron-builder
8 |
9 | ### Patch Changes
10 |
11 | - [#148](https://github.com/develar/app-builder/pull/148) [`b3207cc`](https://github.com/develar/app-builder/commit/b3207cc139330ffd6d3c10dccaa31e5e82c519dc) Thanks [@emmanuel-deloget](https://github.com/emmanuel-deloget)! - fix: handle the table of content resource type correctly
12 |
13 | ## 5.0.0-alpha.12
14 |
15 | ### Minor Changes
16 |
17 | - [#146](https://github.com/develar/app-builder/pull/146) [`a82e82c`](https://github.com/develar/app-builder/commit/a82e82cc91ffb99d8e4c6f6b9c580b5706fcad6b) Thanks [@0xlau](https://github.com/0xlau)! - feat: add s3ForcePathStyle option for s3 publisher
18 |
19 | ## 5.0.0-alpha.11
20 |
21 | ### Patch Changes
22 |
23 | - [#143](https://github.com/develar/app-builder/pull/143) [`0d13c80`](https://github.com/develar/app-builder/commit/0d13c801c7657ba04a25f68c379d469b62e18725) Thanks [@beyondkmp](https://github.com/beyondkmp)! - fix: cannot find module(archiver-utils)
24 |
25 | ## 5.0.0-alpha.10
26 |
27 | ### Patch Changes
28 |
29 | - [#138](https://github.com/develar/app-builder/pull/138) [`28db936`](https://github.com/develar/app-builder/commit/28db9367b398df6bbc579e7a6429666eae08ccd3) Thanks [@beyondkmp](https://github.com/beyondkmp)! - fix: pnpm install error for node module collector (https://github.com/electron-userland/electron-builder/issues/8519)
30 |
31 | - [#139](https://github.com/develar/app-builder/pull/139) [`128737e`](https://github.com/develar/app-builder/commit/128737e831cab4aedb48afe5e659997a16c5437a) Thanks [@mmaietta](https://github.com/mmaietta)! - chore: Update extract logic for using newer 7zz/7zzs/7zr.exe binaries
32 |
33 | ## 5.0.0-alpha.9
34 |
35 | ### Patch Changes
36 |
37 | - [#134](https://github.com/develar/app-builder/pull/134) [`82d3a96`](https://github.com/develar/app-builder/commit/82d3a963bed48f8eb623db0d805a72f0cd72396d) Thanks [@beyondkmp](https://github.com/beyondkmp)! - fix: fix for handling native dependencies, such as `tar` node module
38 |
39 | - [#136](https://github.com/develar/app-builder/pull/136) [`bbad893`](https://github.com/develar/app-builder/commit/bbad893da75c4fc7e019fa629748aabcde73c4e9) Thanks [@beyondkmp](https://github.com/beyondkmp)! - feat: resolve all the pnpm issues without hostied config
40 |
41 | ## 5.0.0-alpha.8
42 |
43 | ### Minor Changes
44 |
45 | - [#130](https://github.com/develar/app-builder/pull/130) [`df4f272`](https://github.com/develar/app-builder/commit/df4f27286a92b6fa17dd333abbdca9d53c8fc1cb) Thanks [@tisoft](https://github.com/tisoft)! - Added support for OpenSUSE to rpm
46 |
47 | ### Patch Changes
48 |
49 | - [#132](https://github.com/develar/app-builder/pull/132) [`1092684`](https://github.com/develar/app-builder/commit/1092684f6771af6abe3ef5614f6136000858003d) Thanks [@beyondkmp](https://github.com/beyondkmp)! - fix: find the real parent node module
50 |
51 | ## 5.0.0-alpha.7
52 |
53 | ### Patch Changes
54 |
55 | - [#126](https://github.com/develar/app-builder/pull/126) [`f910175`](https://github.com/develar/app-builder/commit/f9101753dd2b93b857864d4051baeb6d8856dd64) Thanks [@mmaietta](https://github.com/mmaietta)! - fix: to resolve appimage issues in electron builder, and since we can't update electron-builder-binaries repo, we should just downgrade to the last working version of appimage
56 |
57 | ## 5.0.0-alpha.6
58 |
59 | ### Patch Changes
60 |
61 | - [#124](https://github.com/develar/app-builder/pull/124) [`52ad062`](https://github.com/develar/app-builder/commit/52ad0626206c3ff7b7170afabe2136ef97107042) Thanks [@mmaietta](https://github.com/mmaietta)! - fix: set correct compression enums and remove default
62 |
63 | ## 5.0.0-alpha.5
64 |
65 | ### Patch Changes
66 |
67 | - [#123](https://github.com/develar/app-builder/pull/123) [`20feb29`](https://github.com/develar/app-builder/commit/20feb293f5fa2dc46c4e52212ec9e17e6db669a0) Thanks [@mmaietta](https://github.com/mmaietta)! - fix current mksquashfs version only allows xz and gzip compressions
68 |
69 | - [#118](https://github.com/develar/app-builder/pull/118) [`94485c6`](https://github.com/develar/app-builder/commit/94485c6d500fda34b92a6b4e0ef8314d2cc1a88d) Thanks [@fabienr](https://github.com/fabienr)! - fix: hoist dependencies to the real parent in nodeModuleCollector
70 |
71 | ## 5.0.0-alpha.4
72 |
73 | ### Patch Changes
74 |
75 | - [#119](https://github.com/develar/app-builder/pull/119) [`6a940e4`](https://github.com/develar/app-builder/commit/6a940e46da11d733f8b7c6f31b183c0e402882aa) Thanks [@beyondkmp](https://github.com/beyondkmp)! - fix: alias name issue in node modules resolution dependency tree
76 |
77 | - [#120](https://github.com/develar/app-builder/pull/120) [`189519a`](https://github.com/develar/app-builder/commit/189519a8292f939d9e5d3b47c6407444fee70334) Thanks [@beyondkmp](https://github.com/beyondkmp)! - change node module symlink to real path
78 |
79 | ## 5.0.0-alpha.3
80 |
81 | ### Minor Changes
82 |
83 | - [#116](https://github.com/develar/app-builder/pull/116) [`be4e7ec`](https://github.com/develar/app-builder/commit/be4e7ec9c438e7f803c120a66148950ba294dae5) Thanks [@beyondkmp](https://github.com/beyondkmp)! - feat: add flatten option to `node-dep-tree` for rendering dependency conflicts in a different manner
84 |
85 | ## 5.0.0-alpha.2
86 |
87 | ### Patch Changes
88 |
89 | - [#113](https://github.com/develar/app-builder/pull/113) [`43f7a34`](https://github.com/develar/app-builder/commit/43f7a3473cfbbefc5eba03f7fb04f88f54a1adf2) Thanks [@mmaietta](https://github.com/mmaietta)! - fix: revert appimage 13.0.1 to 13.0.0 due to mksquash arch compilation issues
90 |
91 | ## 5.0.0-alpha.1
92 |
93 | ### Minor Changes
94 |
95 | - [#109](https://github.com/develar/app-builder/pull/109) [`e53b84c`](https://github.com/develar/app-builder/commit/e53b84c9a36105f281825a6e6d168481ddf543a9) Thanks [@mmaietta](https://github.com/mmaietta)! - feat: allow providing env var for custom app-builder binary as opposed to accessing directly from the PATH env var
96 |
97 | ### Patch Changes
98 |
99 | - [`64bb497`](https://github.com/develar/app-builder/commit/64bb4971150edc37dbfb3819f115e4d767cf89c6) Thanks [@mmaietta](https://github.com/mmaietta)! - fix(snap): Parse user command line options as last values
100 |
101 | ## 5.0.0-alpha.0
102 |
103 | ### Major Changes
104 |
105 | - [#107](https://github.com/develar/app-builder/pull/107) [`f4642dd`](https://github.com/develar/app-builder/commit/f4642ddcd85b482d1a7ed49f14d27c509eb5aa6b) Thanks [@mmaietta](https://github.com/mmaietta)! - chore: changing repo structure for release automation
106 |
107 | ### Minor Changes
108 |
109 | - [#98](https://github.com/develar/app-builder/pull/98) [`3ed22df`](https://github.com/develar/app-builder/commit/3ed22df75fcff132a5b794ce1a421bec263bc118) Thanks [@yzewei](https://github.com/yzewei)! - feat: Add loongarch64 support
110 |
111 | ### Patch Changes
112 |
113 | - [#106](https://github.com/develar/app-builder/pull/106) [`9704964`](https://github.com/develar/app-builder/commit/970496449b0b02780d654d61af1e3277515a2545) Thanks [@theogravity](https://github.com/theogravity)! - fix: Use npm config.mirror first before env variables for download URL
114 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Vladimir Krivosheev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # go install -a -v github.com/go-bindata/go-bindata/...@latest (pack not used because cannot properly select dir to generate and no way to specify explicitly)
2 |
3 | .PHONY: lint build publish assets
4 |
5 | OS_ARCH = ""
6 | ifeq ($(OS),Windows_NT)
7 | ifeq ($(PROCESSOR_ARCHITEW6432),AMD64)
8 | OS_ARCH := windows_amd64
9 | else ifeq ($(PROCESSOR_ARCHITEW6432),ARM64)
10 | OS_ARCH := windows_arm64
11 | else
12 | OS_ARCH := windows_386
13 | endif
14 | else
15 | UNAME_S := $(shell uname -s)
16 | ifeq ($(UNAME_S),Linux)
17 | ifeq ($(UNAME_M),riscv64)
18 | OS_ARCH := linux_riscv64
19 | else ifeq ($(UNAME_M),loongarch64)
20 | OS_ARCH := linux_loong64
21 | else
22 | OS_ARCH := linux_amd64
23 | endif
24 | endif
25 | ifeq ($(UNAME_S),Darwin)
26 | OS_ARCH := darwin_$(shell uname -m)
27 | endif
28 | endif
29 |
30 | # ln -sf ~/Documents/app-builder/dist/app-builder_darwin_amd64/app-builder ~/Documents/electron-builder/node_modules/app-builder-bin/mac/app-builder
31 | # cp ~/Documents/app-builder/dist/app-builder_linux_amd64/app-builder ~/Documents/electron-builder/node_modules/app-builder-bin/linux/x64/app-builder
32 | build: assets
33 | go build -ldflags='-s -w' -o dist/app-builder_$(OS_ARCH)/app-builder
34 |
35 | build-all: assets
36 | ./scripts/build.sh
37 |
38 | # brew install golangci/tap/golangci-lint && brew upgrade golangci/tap/golangci-lint
39 | lint:
40 | golangci-lint run
41 |
42 | test:
43 | cd pkg/node-modules/pnpm-demo/ && pnpm install
44 | cd pkg/node-modules/npm-demo/ && npm install
45 | cd pkg/node-modules/tar-demo/ && npm install
46 | cd pkg/node-modules/yarn-demo/ && yarn
47 | cd pkg/node-modules/parse-demo/ && yarn
48 | cd pkg/node-modules/es5-demo/ && pnpm install
49 | go test -v ./pkg/...
50 |
51 | assets:
52 | ~/go/bin/go-bindata -o ./pkg/package-format/bindata.go -pkg package_format -prefix ./pkg/package-format ./pkg/package-format/appimage/templates
53 | ~/go/bin/go-bindata -o ./pkg/package-format/snap/snapScripts.go -pkg snap -prefix ./pkg/package-format/snap ./pkg/package-format/snap/desktop-scripts
54 |
55 | update-deps:
56 | go get -u -d
57 | go mod tidy
58 |
--------------------------------------------------------------------------------
/app-builder.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/develar/app-builder
2 |
3 | go 1.21
4 |
5 | require (
6 | github.com/aclements/go-rabin v0.0.0-20170911142644-d0b643ea1a4c
7 | github.com/alecthomas/kingpin v2.2.6+incompatible
8 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
9 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
10 | github.com/alessio/shellescape v0.0.0-20190409004728-b115ca0f9053 // indirect
11 | github.com/aws/aws-sdk-go v1.45.7
12 | github.com/biessek/golang-ico v0.0.0-20180326222316-d348d9ea4670
13 | github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
14 | github.com/develar/errors v0.9.0
15 | github.com/develar/go-fs-util v0.0.0-20190620175131-69a2d4542206
16 | github.com/develar/go-pkcs12 v0.0.0-20181115143544-54baa4f32c6a
17 | github.com/disintegration/imaging v1.6.2
18 | github.com/dustin/go-humanize v1.0.1
19 | github.com/golang/protobuf v1.3.2 // indirect
20 | github.com/json-iterator/go v1.1.12
21 | github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
22 | github.com/mattn/go-colorable v0.1.13
23 | github.com/mattn/go-isatty v0.0.19
24 | github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2
25 | github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1
26 | github.com/mitchellh/go-homedir v1.1.0
27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
28 | github.com/modern-go/reflect2 v1.0.2 // indirect
29 | github.com/onsi/ginkgo v1.8.0
30 | github.com/onsi/gomega v1.5.0
31 | github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c
32 | github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee
33 | github.com/pkg/errors v0.9.1
34 | github.com/pkg/xattr v0.4.9
35 | github.com/segmentio/ksuid v1.0.4
36 | github.com/zieckey/goini v0.0.0-20180118150432-0da17d361d26
37 | go.uber.org/multierr v1.11.0 // indirect
38 | go.uber.org/zap v1.25.0
39 | golang.org/x/image v0.12.0 // indirect
40 | golang.org/x/sys v0.12.0 // indirect
41 | gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61
42 | howett.net/plist v1.0.0
43 | )
44 |
45 | require (
46 | github.com/hpcloud/tail v1.0.0 // indirect
47 | github.com/jmespath/go-jmespath v0.4.0 // indirect
48 | github.com/samber/lo v1.38.1
49 | golang.org/x/net v0.6.0 // indirect
50 | golang.org/x/text v0.13.0 // indirect
51 | gopkg.in/fsnotify.v1 v1.4.7 // indirect
52 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
53 | gopkg.in/yaml.v2 v2.2.8 // indirect
54 | )
55 |
56 | require (
57 | github.com/kr/pretty v0.3.1 // indirect
58 | golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
59 | )
60 |
61 | //replace github.com/develar/go-pkcs12 => ../go-pkcs12
62 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export const appBuilderPath: string
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict"
2 |
3 | const path = require("path")
4 |
5 | function getPath() {
6 | if (process.env.USE_SYSTEM_APP_BUILDER === "true") {
7 | return "app-builder"
8 | }
9 |
10 | if (!!process.env.CUSTOM_APP_BUILDER_PATH) {
11 | return path.resolve(process.env.CUSTOM_APP_BUILDER_PATH)
12 | }
13 |
14 | const { platform, arch } = process;
15 | if (platform === "darwin") {
16 | return path.join(__dirname, "mac", `app-builder_${arch === "x64" ? "amd64" : arch}`)
17 | }
18 | else if (platform === "win32") {
19 | return path.join(__dirname, "win", arch, "app-builder.exe")
20 | }
21 | else {
22 | return path.join(__dirname, "linux", arch, "app-builder")
23 | }
24 | }
25 |
26 | exports.appBuilderPath = getPath()
27 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "io"
5 | "os"
6 | "os/exec"
7 | "runtime"
8 | "sync"
9 |
10 | "github.com/alecthomas/kingpin"
11 | "github.com/develar/app-builder/pkg/archive/zipx"
12 | "github.com/develar/app-builder/pkg/blockmap"
13 | "github.com/develar/app-builder/pkg/codesign"
14 | "github.com/develar/app-builder/pkg/download"
15 | "github.com/develar/app-builder/pkg/electron"
16 | "github.com/develar/app-builder/pkg/fs"
17 | "github.com/develar/app-builder/pkg/icons"
18 | "github.com/develar/app-builder/pkg/linuxTools"
19 | "github.com/develar/app-builder/pkg/log"
20 | "github.com/develar/app-builder/pkg/node-modules"
21 | "github.com/develar/app-builder/pkg/package-format/appimage"
22 | "github.com/develar/app-builder/pkg/package-format/dmg"
23 | "github.com/develar/app-builder/pkg/package-format/fpm"
24 | "github.com/develar/app-builder/pkg/package-format/proton-native"
25 | "github.com/develar/app-builder/pkg/package-format/snap"
26 | "github.com/develar/app-builder/pkg/plist"
27 | "github.com/develar/app-builder/pkg/publisher"
28 | "github.com/develar/app-builder/pkg/rcedit"
29 | "github.com/develar/app-builder/pkg/remoteBuild"
30 | "github.com/develar/app-builder/pkg/util"
31 | "github.com/develar/app-builder/pkg/wine"
32 | "github.com/develar/errors"
33 | "github.com/segmentio/ksuid"
34 | )
35 |
36 | func main() {
37 | log.InitLogger()
38 | defer func() {
39 | _ = log.LOG.Sync()
40 | }()
41 |
42 | if os.Getenv("SZA_ARCHIVE_TYPE") != "" {
43 | err := compress()
44 | if err != nil {
45 | util.LogErrorAndExit(err)
46 | }
47 | return
48 | }
49 |
50 | var app = kingpin.New("app-builder", "app-builder").Version("3.5.10")
51 |
52 | node_modules.ConfigureCommand(app)
53 | node_modules.ConfigureRebuildCommand(app)
54 | //codesign.ConfigureCommand(app)
55 | publisher.ConfigurePublishToS3Command(app)
56 | remoteBuild.ConfigureBuildCommand(app)
57 |
58 | download.ConfigureCommand(app)
59 | download.ConfigureArtifactCommand(app)
60 |
61 | electron.ConfigureCommand(app)
62 | electron.ConfigureUnpackCommand(app)
63 |
64 | zipx.ConfigureUnzipCommand(app)
65 | proton_native.ConfigureCommand(app)
66 |
67 | configurePrefetchToolsCommand(app)
68 |
69 | ConfigureCopyCommand(app)
70 | appimage.ConfigureCommand(app)
71 | snap.ConfigureCommand(app)
72 | snap.ConfigurePublishCommand(app)
73 | fpm.ConfigureCommand(app)
74 |
75 | err := icons.ConfigureCommand(app)
76 | if err != nil {
77 | util.LogErrorAndExit(err)
78 | }
79 |
80 | dmg.ConfigureCommand(app)
81 | blockmap.ConfigureCommand(app)
82 | codesign.ConfigureCertificateInfoCommand(app)
83 |
84 | wine.ConfigureCommand(app)
85 | rcedit.ConfigureCommand(app)
86 | configureKsUidCommand(app)
87 |
88 | plist.ConfigurePlistCommand(app)
89 |
90 | _, err = app.Parse(os.Args[1:])
91 | if err != nil {
92 | util.LogErrorAndExit(err)
93 | }
94 | }
95 |
96 | func ConfigureCopyCommand(app *kingpin.Application) {
97 | command := app.Command("copy", "Copy file or dir.")
98 | from := command.Flag("from", "").Required().Short('f').String()
99 | to := command.Flag("to", "").Required().Short('t').String()
100 | isUseHardLinks := command.Flag("hard-link", "Whether to use hard-links if possible").Bool()
101 |
102 | command.Action(func(context *kingpin.ParseContext) error {
103 | var fileCopier fs.FileCopier
104 | fileCopier.IsUseHardLinks = *isUseHardLinks
105 | return errors.WithStack(fileCopier.CopyDirOrFile(*from, *to))
106 | })
107 | }
108 |
109 | func configureKsUidCommand(app *kingpin.Application) {
110 | command := app.Command("ksuid", "Generate KSUID")
111 | command.Action(func(context *kingpin.ParseContext) error {
112 | _, err := os.Stdout.Write([]byte(ksuid.New().String()))
113 | return errors.WithStack(err)
114 | })
115 | }
116 |
117 | func compress() error {
118 | args := []string{"a", "-si", "-so", "-t" + util.GetEnvOrDefault("SZA_ARCHIVE_TYPE", "xz"), "-mx" + util.GetEnvOrDefault("SZA_COMPRESSION_LEVEL", "9"), "dummy"}
119 | args = append(args, os.Args[1:]...)
120 |
121 | command := exec.Command(util.Get7zPath(), args...)
122 | command.Stderr = os.Stderr
123 |
124 | stdin, err := command.StdinPipe()
125 | if nil != err {
126 | return errors.WithStack(err)
127 | }
128 |
129 | stdout, err := command.StdoutPipe()
130 | if nil != err {
131 | return errors.WithStack(err)
132 | }
133 |
134 | err = command.Start()
135 | if err != nil {
136 | return errors.WithStack(err)
137 | }
138 |
139 | var waitGroup sync.WaitGroup
140 | waitGroup.Add(2)
141 | go func() {
142 | defer waitGroup.Done()
143 | defer util.Close(stdin)
144 | _, _ = io.Copy(stdin, os.Stdin)
145 | }()
146 |
147 | go func() {
148 | defer waitGroup.Done()
149 | _, _ = io.Copy(os.Stdout, stdout)
150 | }()
151 |
152 | waitGroup.Wait()
153 | err = command.Wait()
154 | if err != nil {
155 | return errors.WithStack(err)
156 | }
157 |
158 | return nil
159 | }
160 |
161 | func configurePrefetchToolsCommand(app *kingpin.Application) {
162 | command := app.Command("prefetch-tools", "Prefetch all required tools")
163 | osName := command.Flag("osName", "").Default(runtime.GOOS).Enum("darwin", "linux", "win32")
164 | command.Action(func(context *kingpin.ParseContext) error {
165 | _, err := linuxTools.GetAppImageToolDir()
166 | if err != nil {
167 | return errors.WithStack(err)
168 | }
169 |
170 | _, err = snap.ResolveTemplateDir("", "electron4:amd64", "")
171 | if err != nil {
172 | return err
173 | }
174 |
175 | _, err = snap.ResolveTemplateDir("", "electron4:arm", "")
176 | if err != nil {
177 | return err
178 | }
179 |
180 | _, err = download.DownloadFpm()
181 | if err != nil {
182 | return err
183 | }
184 | _, err = download.DownloadZstd(util.ToOsName(*osName))
185 | if err != nil {
186 | return err
187 | }
188 | return nil
189 | })
190 | }
191 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app-builder-bin",
3 | "description": "app-builder precompiled binaries",
4 | "version": "5.0.0-alpha.13",
5 | "files": [
6 | "index.js",
7 | "mac",
8 | "linux",
9 | "win",
10 | "index.d.ts"
11 | ],
12 | "license": "MIT",
13 | "repository": "develar/app-builder",
14 | "keywords": [
15 | "snap",
16 | "appimage",
17 | "icns"
18 | ],
19 | "devDependencies": {
20 | "@changesets/changelog-github": "^0.5.0",
21 | "@changesets/cli": "^2.27.1",
22 | "conventional-changelog-cli": "^4.1.0"
23 | },
24 | "scripts": {
25 | "changeset": "changeset",
26 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md",
27 | "ci:version": "pnpm changelog && changeset version && make assets && git add .",
28 | "ci:publish": "make build-all && pnpm publish --no-git-checks --tag next && changeset tag"
29 | },
30 | "publishConfig": {
31 | "tag": "next",
32 | "git-checks": false,
33 | "executableFiles": [
34 | "linux/arm/app-builder",
35 | "linux/arm64/app-builder",
36 | "linux/ia32/app-builder",
37 | "linux/loong64/app-builder",
38 | "linux/riscv64/app-builder",
39 | "linux/x64/app-builder",
40 | "mac/app-builder",
41 | "mac/app-builder_amd64",
42 | "mac/app-builder_arm64",
43 | "win/arm64/app-builder.exe",
44 | "win/ia32/app-builder.exe",
45 | "win/x64/app-builder.exe"
46 | ]
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/archive/zipx/unzip.go:
--------------------------------------------------------------------------------
1 | package zipx
2 |
3 | import (
4 | "archive/zip"
5 | "io"
6 | "os"
7 | "path/filepath"
8 | "strings"
9 | "syscall"
10 |
11 | "github.com/alecthomas/kingpin"
12 | "github.com/develar/app-builder/pkg/fs"
13 | "github.com/develar/app-builder/pkg/util"
14 | "github.com/develar/errors"
15 | "github.com/develar/go-fs-util"
16 | "github.com/oxtoacart/bpool"
17 | )
18 |
19 | func ConfigureUnzipCommand(app *kingpin.Application) {
20 | command := app.Command("unzip", "")
21 | src := command.Flag("input", "").Short('i').Required().String()
22 | dest := command.Flag("output", "").Short('o').Required().String()
23 |
24 | command.Action(func(context *kingpin.ParseContext) error {
25 | // empty dir must be not used to ensure that some dir will be not removed by mistake, client should clean if need
26 | err := fsutil.EnsureDir(*dest)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | err = Unzip(*src, *dest, nil)
32 | if err != nil {
33 | return err
34 | }
35 |
36 | return nil
37 | })
38 | }
39 |
40 | // limit write, cpu count can be larger but IO in any case cannot handle a lot of write requests
41 | const concurrency = 4
42 |
43 | // https://github.com/mholt/archiver/issues/21
44 | // dest must be an empty dir
45 | func Unzip(src string, outputDir string, excludedFiles map[string]bool) error {
46 | if len(src) == 0 {
47 | return errors.New("input zip file name is empty")
48 | }
49 |
50 | r, err := zip.OpenReader(src)
51 | if err != nil {
52 | // return as is without stack to allow client easily compare error with known zip errors
53 | return err
54 | }
55 |
56 | defer util.Close(r)
57 |
58 | extractor := &Extractor{
59 | outputDir: filepath.Clean(outputDir),
60 | excludedFiles: excludedFiles,
61 |
62 | createdDirs: make(map[string]bool),
63 | bufferPool: bpool.NewBytePool(concurrency, 64*1024),
64 | }
65 |
66 | extractor.createdDirs[extractor.outputDir] = true
67 |
68 | lastCreatedDir := ""
69 | // create files async
70 | err = util.MapAsyncConcurrency(len(r.File), concurrency, func(taskIndex int) (func() error, error) {
71 | zipFile := r.File[taskIndex]
72 | if zipFile.FileInfo().IsDir() {
73 | // create dir (not async)
74 | err := extractor.extractDir(zipFile)
75 | if err != nil {
76 | return nil, err
77 | }
78 | return nil, nil
79 | }
80 |
81 | filePath, err := extractor.computeExtractPath(zipFile)
82 | if err != nil {
83 | return nil, err
84 | }
85 |
86 | if extractor.excludedFiles != nil {
87 | _, isExcluded := extractor.excludedFiles[filePath]
88 | if isExcluded {
89 | return nil, nil
90 | }
91 | }
92 |
93 | fileDir := filepath.Dir(filePath)
94 | if fileDir != lastCreatedDir {
95 | err = extractor.createDirIfNeed(fileDir)
96 | if err != nil {
97 | return nil, err
98 | }
99 |
100 | lastCreatedDir = fileDir
101 | }
102 |
103 | return func() error {
104 | return extractor.extractAndWriteFile(zipFile, filePath)
105 | }, nil
106 | })
107 | if err != nil {
108 | return err
109 | }
110 |
111 | return nil
112 | }
113 |
114 | type Extractor struct {
115 | outputDir string
116 | excludedFiles map[string]bool
117 |
118 | createdDirs map[string]bool
119 | bufferPool *bpool.BytePool
120 | }
121 |
122 | func (t *Extractor) createDirIfNeed(dirPath string) error {
123 | _, isDirCreated := t.createdDirs[dirPath]
124 | if isDirCreated {
125 | return nil
126 | }
127 |
128 | err := os.MkdirAll(dirPath, 0777)
129 | if err != nil {
130 | return err
131 | }
132 |
133 | t.addWithParentsToCreated(dirPath)
134 | return nil
135 | }
136 |
137 | // check t.createdDirs before create parent dir
138 | func (t *Extractor) MkdirAll(path string, perm os.FileMode) error {
139 | // fast path: if we can tell whether path is a directory or file, stop with success or error.
140 | dir, err := os.Stat(path)
141 | if err == nil {
142 | if dir.IsDir() {
143 | return nil
144 | }
145 | return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
146 | }
147 |
148 | // avoid string comparison: dir == t.outputDir, since dir is already checked to has prefix, length check is enough
149 | minLength := len(t.outputDir)
150 |
151 | // slow path: make sure parent exists and then call Mkdir for path.
152 | i := len(path)
153 | for i > minLength && !os.IsPathSeparator(path[i-1]) {
154 | i--
155 | }
156 |
157 | if i > minLength {
158 | // create parent
159 | parentPath := path[:i-1]
160 | _, isDirCreated := t.createdDirs[parentPath]
161 | if !isDirCreated {
162 | err = t.MkdirAll(parentPath, perm)
163 | if err != nil {
164 | return err
165 | }
166 | }
167 | }
168 |
169 | // parent now exists
170 | err = os.Mkdir(path, perm)
171 | if err != nil {
172 | return err
173 | }
174 |
175 | return nil
176 | }
177 |
178 | func (t *Extractor) addWithParentsToCreated(dir string) {
179 | // avoid string comparison: dir == t.outputDir, since dir is already checked to has prefix, length check is enough
180 | minLength := len(t.outputDir)
181 | for {
182 | t.createdDirs[dir] = true
183 |
184 | i := len(dir)
185 | for i > minLength && !os.IsPathSeparator(dir[i-1]) {
186 | i--
187 | }
188 |
189 | if i <= minLength {
190 | break
191 | }
192 |
193 | dir = dir[:i-1]
194 | _, isDirCreated := t.createdDirs[dir]
195 | if isDirCreated {
196 | break
197 | }
198 | }
199 | }
200 |
201 | func (t *Extractor) computeExtractPath(zipFile *zip.File) (string, error) {
202 | // #nosec G305
203 | filePath := filepath.Join(t.outputDir, zipFile.Name)
204 | if strings.HasPrefix(filePath, t.outputDir) {
205 | return filePath, nil
206 | } else {
207 | return "", errors.Errorf("%s: illegal file path", filePath)
208 | }
209 | }
210 |
211 | func (t *Extractor) extractDir(zipFile *zip.File) error {
212 | filePath, err := t.computeExtractPath(zipFile)
213 | if err != nil {
214 | return err
215 | }
216 |
217 | err = os.MkdirAll(filePath, 0777)
218 | if err != nil {
219 | return err
220 | }
221 |
222 | err = fs.SetNormalDirPermissions(filePath)
223 | if err != nil {
224 | return err
225 | }
226 |
227 | t.addWithParentsToCreated(filePath)
228 | return nil
229 | }
230 |
231 | func (t *Extractor) extractAndWriteFile(zipFile *zip.File, filePath string) error {
232 | file, err := zipFile.Open()
233 | if err != nil {
234 | return errors.WithStack(err)
235 | }
236 |
237 | defer util.Close(file)
238 |
239 | if (zipFile.FileInfo().Mode() & os.ModeSymlink) != 0 {
240 | return t.createSymlink(file, zipFile, filePath)
241 | }
242 |
243 | buffer := t.bufferPool.Get()
244 | err = fs.WriteFileAndRestoreNormalPermissions(file, filePath, zipFile.Mode(), buffer)
245 | t.bufferPool.Put(buffer)
246 | if err != nil {
247 | return err
248 | }
249 | return nil
250 | }
251 |
252 | func (t *Extractor) createSymlink(reader io.Reader, zipFile *zip.File, filePath string) error {
253 | buffer := make([]byte, zipFile.FileInfo().Size())
254 | _, err := io.ReadFull(reader, buffer)
255 | if err != nil {
256 | return err
257 | }
258 |
259 | return os.Symlink(string(buffer), filePath)
260 | }
261 |
--------------------------------------------------------------------------------
/pkg/blockmap/blockmap.go:
--------------------------------------------------------------------------------
1 | package blockmap
2 |
3 | import (
4 | "bytes"
5 | "compress/flate"
6 | "compress/gzip"
7 | "crypto/sha512"
8 | "encoding/base64"
9 | "encoding/binary"
10 | "fmt"
11 | "hash"
12 | "io"
13 | "os"
14 |
15 | "github.com/aclements/go-rabin/rabin"
16 | "github.com/develar/app-builder/pkg/util"
17 | "github.com/develar/errors"
18 | "github.com/json-iterator/go"
19 | "github.com/minio/blake2b-simd"
20 | )
21 |
22 | type BlockMap struct {
23 | Version string `json:"version"`
24 | Files []BlockMapFile `json:"files"`
25 | }
26 |
27 | type BlockMapFile struct {
28 | Name string `json:"name"`
29 | Offset uint64 `json:"offset"`
30 |
31 | Checksums []string `json:"checksums"`
32 | Sizes []int `json:"sizes"`
33 | }
34 |
35 | type InputFileInfo struct {
36 | Size int `json:"size"`
37 | Sha512 string `json:"sha512"`
38 |
39 | BlockMapSize *int `json:"blockMapSize,omitempty"`
40 |
41 | hash *hash.Hash
42 | }
43 |
44 | type ChunkerConfiguration struct {
45 | Window int
46 | Avg int
47 | Min int
48 | Max int
49 | }
50 |
51 | type CompressionFormat int
52 |
53 | const (
54 | GZIP = 0
55 | DEFLATE = 1
56 | )
57 |
58 | var DefaultChunkerConfiguration = ChunkerConfiguration{
59 | Window: 64,
60 | Avg: 16 * 1024,
61 | Min: 8 * 1024,
62 | Max: 32 * 1024,
63 | }
64 |
65 | func BuildBlockMap(inFile string, chunkerConfiguration ChunkerConfiguration, compressionFormat CompressionFormat, outFile string) (*InputFileInfo, error) {
66 | checksums, sizes, inputInfo, err := computeBlocks(inFile, chunkerConfiguration)
67 | if err != nil {
68 | return nil, err
69 | }
70 |
71 | blockMap := BlockMap{
72 | Version: "2",
73 | Files: []BlockMapFile{
74 | {
75 | Name: "file",
76 | Offset: 0,
77 | Checksums: *checksums,
78 | Sizes: *sizes,
79 | },
80 | },
81 | }
82 |
83 | serializedBlockMap, err := jsoniter.ConfigFastest.Marshal(&blockMap)
84 | if err != nil {
85 | return nil, err
86 | }
87 |
88 | if len(outFile) == 0 {
89 | archiveSize, err := appendResult(serializedBlockMap, inFile, compressionFormat, inputInfo.hash)
90 | if err != nil {
91 | return nil, err
92 | }
93 |
94 | inputInfo.Size += archiveSize + 4
95 | inputInfo.BlockMapSize = &archiveSize
96 | } else {
97 | err = writeResult(serializedBlockMap, outFile, compressionFormat)
98 | if err != nil {
99 | return nil, err
100 | }
101 | }
102 |
103 | inputInfo.Sha512 = base64.StdEncoding.EncodeToString((*inputInfo.hash).Sum(nil))
104 | return inputInfo, nil
105 | }
106 |
107 | func appendResult(data []byte, inFile string, compressionFormat CompressionFormat, hash *hash.Hash) (int, error) {
108 | archiveBuffer := new(bytes.Buffer)
109 | err := archiveData(data, compressionFormat, archiveBuffer)
110 | if err != nil {
111 | return -1, errors.WithStack(err)
112 | }
113 |
114 | outFileDescriptor, err := os.OpenFile(inFile, os.O_APPEND|os.O_WRONLY, 0)
115 | if err != nil {
116 | return -1, errors.WithStack(err)
117 | }
118 |
119 | defer util.Close(outFileDescriptor)
120 |
121 | archiveSize := archiveBuffer.Len()
122 | _, err = io.Copy(outFileDescriptor, io.TeeReader(archiveBuffer, *hash))
123 | if err != nil {
124 | return -1, errors.WithStack(err)
125 | }
126 |
127 | sizeBytes := make([]byte, 4)
128 | binary.BigEndian.PutUint32(sizeBytes, uint32(archiveSize))
129 | _, err = outFileDescriptor.Write(sizeBytes)
130 | if err != nil {
131 | return -1, errors.WithStack(err)
132 | }
133 |
134 | _, err = (*hash).Write(sizeBytes)
135 | if err != nil {
136 | return -1, errors.WithStack(err)
137 | }
138 |
139 | return archiveSize, nil
140 | }
141 |
142 | func writeResult(data []byte, outFile string, compressionFormat CompressionFormat) error {
143 | if outFile == "-" {
144 | _, err := os.Stdout.Write(data)
145 | return err
146 | }
147 |
148 | outFileDescriptor, err := os.Create(outFile)
149 | if err != nil {
150 | return err
151 | }
152 | defer util.Close(outFileDescriptor)
153 |
154 | return archiveData(data, compressionFormat, outFileDescriptor)
155 | }
156 |
157 | func archiveData(data []byte, compressionFormat CompressionFormat, destinationWriter io.Writer) error {
158 | var archiveWriter io.WriteCloser
159 | var err error
160 | if compressionFormat == DEFLATE {
161 | archiveWriter, err = flate.NewWriter(destinationWriter, flate.BestCompression)
162 | } else {
163 | archiveWriter, err = gzip.NewWriterLevel(destinationWriter, gzip.BestCompression)
164 | }
165 | if err != nil {
166 | return err
167 | }
168 |
169 | defer util.Close(archiveWriter)
170 |
171 | _, err = archiveWriter.Write(data)
172 | if err != nil {
173 | return errors.WithStack(err)
174 | }
175 |
176 | return nil
177 | }
178 |
179 | func computeBlocks(inFile string, configuration ChunkerConfiguration) (*[]string, *[]int, *InputFileInfo, error) {
180 | inputFileDescriptor, err := os.Open(inFile)
181 | if err != nil {
182 | return nil, nil, nil, err
183 | }
184 | defer util.Close(inputFileDescriptor)
185 |
186 | var checksums []string
187 | var sizes []int
188 |
189 | chunkHash, err := blake2b.New(&blake2b.Config{Size: 18})
190 | if err != nil {
191 | return nil, nil, nil, err
192 | }
193 |
194 | inputHash := sha512.New()
195 |
196 | copyBuffer := new(bytes.Buffer)
197 | r := io.TeeReader(inputFileDescriptor, copyBuffer)
198 | c := rabin.NewChunker(rabin.NewTable(rabin.Poly64, configuration.Window), r, configuration.Min, configuration.Avg, configuration.Max)
199 | for i := 0; ; i++ {
200 | copyLength, err := c.Next()
201 | if err == io.EOF {
202 | break
203 | } else if err != nil {
204 | return nil, nil, nil, err
205 | }
206 |
207 | _, err = io.Copy(chunkHash, io.TeeReader(io.LimitReader(copyBuffer, int64(copyLength)), inputHash))
208 | if err != nil {
209 | return nil, nil, nil, errors.New("error writing hash")
210 | }
211 |
212 | checksums = append(checksums, base64.StdEncoding.EncodeToString(chunkHash.Sum(nil)))
213 | sizes = append(sizes, copyLength)
214 |
215 | chunkHash.Reset()
216 | }
217 |
218 | inputFileStat, err := inputFileDescriptor.Stat()
219 | if err != nil {
220 | return nil, nil, nil, err
221 | }
222 |
223 | sum := 0
224 | for _, s := range sizes {
225 | sum += s
226 | }
227 |
228 | fileSize := int(inputFileStat.Size())
229 | if sum != fileSize {
230 | return nil, nil, nil, fmt.Errorf("expected size sum: %d. Actual: %d", fileSize, sum)
231 | }
232 |
233 | return &checksums, &sizes, &InputFileInfo{
234 | Size: fileSize,
235 | hash: &inputHash,
236 | }, nil
237 | }
238 |
--------------------------------------------------------------------------------
/pkg/blockmap/blockmap_test.go:
--------------------------------------------------------------------------------
1 | package blockmap_test
2 |
3 | import (
4 | "crypto/sha512"
5 | "encoding/base64"
6 | "io/ioutil"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/json-iterator/go"
11 | . "github.com/onsi/ginkgo"
12 | . "github.com/onsi/gomega"
13 |
14 | . "github.com/develar/app-builder/pkg/blockmap"
15 | )
16 |
17 | func TestBlockmap(t *testing.T) {
18 | RegisterFailHandler(Fail)
19 | RunSpecs(t, "Blockmap Suite")
20 | }
21 |
22 | var _ = Describe("Blockmap", func() {
23 | It("append", func() {
24 | file, err := ioutil.TempFile("", "append")
25 | Expect(err).NotTo(HaveOccurred())
26 |
27 | _, err = file.WriteString(strings.Repeat("hello world. ", 1024))
28 | Expect(err).NotTo(HaveOccurred())
29 | err = file.Close()
30 | Expect(err).NotTo(HaveOccurred())
31 |
32 | inputInfo, err := BuildBlockMap(file.Name(), DefaultChunkerConfiguration, DEFLATE, "")
33 | Expect(err).NotTo(HaveOccurred())
34 |
35 | fileData, err := ioutil.ReadFile(file.Name())
36 | Expect(err).NotTo(HaveOccurred())
37 |
38 | hash := sha512.New()
39 | _, err = hash.Write(fileData)
40 | Expect(err).NotTo(HaveOccurred())
41 | Expect(inputInfo.Sha512).To(Equal(base64.StdEncoding.EncodeToString(hash.Sum(nil))))
42 | Expect(inputInfo.Size).To(Equal(len(fileData)))
43 |
44 | serializedInputInfo, err := jsoniter.ConfigFastest.Marshal(inputInfo)
45 | Expect(err).NotTo(HaveOccurred())
46 | //noinspection SpellCheckingInspection
47 | Expect(string(serializedInputInfo)).To(Equal("{\"size\":13423,\"sha512\":\"zPFW3WAFUKFvAfBdNXHDIuZekSW/qf33lf5OgKXBKg9oOobwVH9X/DRHExC9087Cxkp3nqFrwtreWZHLso3D6g==\",\"blockMapSize\":107}"))
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/pkg/blockmap/command.go:
--------------------------------------------------------------------------------
1 | package blockmap
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/alecthomas/kingpin"
7 | "github.com/develar/app-builder/pkg/util"
8 | )
9 |
10 | func ConfigureCommand(app *kingpin.Application) {
11 | command := app.Command("blockmap", "Generates file block map for differential update using content defined chunking (that is robust to insertions, deletions, and changes to input file)")
12 | inFile := command.Flag("input", "input file").Short('i').Required().String()
13 | outFile := command.Flag("output", "output file").Short('o').String()
14 | compression := command.Flag("compression", "compression, one of: gzip, deflate").Short('c').Default("gzip").Enum("gzip", "deflate")
15 |
16 | command.Action(func(context *kingpin.ParseContext) error {
17 | var compressionFormat CompressionFormat
18 | switch *compression {
19 | case "gzip":
20 | compressionFormat = GZIP
21 | case "deflate":
22 | compressionFormat = DEFLATE
23 | default:
24 | return fmt.Errorf("unknown compression format %s", *compression)
25 | }
26 |
27 | inputInfo, err := BuildBlockMap(*inFile, DefaultChunkerConfiguration, compressionFormat, *outFile)
28 | if err != nil {
29 | return err
30 | }
31 | return util.WriteJsonToStdOut(inputInfo)
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/pkg/codesign/p12.go:
--------------------------------------------------------------------------------
1 | package codesign
2 |
3 | import (
4 | "crypto/x509"
5 | "crypto/x509/pkix"
6 | "encoding/asn1"
7 | "encoding/hex"
8 | "encoding/pem"
9 | "fmt"
10 | "io/ioutil"
11 | "os"
12 | "os/exec"
13 | "path/filepath"
14 | "strings"
15 |
16 | "github.com/alecthomas/kingpin"
17 | "github.com/develar/app-builder/pkg/download"
18 | "github.com/develar/app-builder/pkg/log"
19 | "github.com/develar/app-builder/pkg/util"
20 | "github.com/develar/errors"
21 | "github.com/develar/go-pkcs12"
22 | "github.com/json-iterator/go"
23 | "go.uber.org/zap"
24 | )
25 |
26 | func ConfigureCertificateInfoCommand(app *kingpin.Application) {
27 | command := app.Command("certificate-info", "Read information about code signing certificate")
28 | inFile := command.Flag("input", "input file").Short('i').Required().String()
29 | password := command.Flag("password", "password").Short('p').String()
30 |
31 | command.Action(func(context *kingpin.ParseContext) error {
32 | return readInfo(*inFile, *password)
33 | })
34 | }
35 |
36 | func readInfo(inFile string, password string) error {
37 | data, err := ioutil.ReadFile(inFile)
38 | if err != nil {
39 | if os.IsNotExist(err) {
40 | return writeError(err.Error())
41 | }
42 | return err
43 | }
44 |
45 | certificates, err := pkcs12.DecodeAllCerts(data, password)
46 | if err != nil {
47 | if err.Error() == "pkcs12: decryption password incorrect" {
48 | return writeError("password incorrect")
49 | }
50 |
51 | log.Warn("cannot decode PKCS 12 data using Go pure implementation, openssl will be used", zap.Error(err))
52 | certificates, err = readUsingOpenssl(inFile, password)
53 | if err != nil {
54 | if strings.Contains(err.Error(), "Mac verify error: invalid password?") {
55 | return writeError("password incorrect")
56 | }
57 |
58 | m := err.Error()
59 | if exitError, ok := errors.Cause(err).(*exec.ExitError); ok {
60 | m += "; error output:\n" + string(exitError.Stderr)
61 | }
62 | return writeError(m)
63 | }
64 | }
65 |
66 | if len(certificates) == 0 {
67 | return fmt.Errorf("no certificates")
68 | }
69 |
70 | var firstCert *x509.Certificate
71 | certLoop:
72 | for _, cert := range certificates {
73 | for _, usage := range cert.ExtKeyUsage {
74 | if usage == x509.ExtKeyUsageCodeSigning {
75 | firstCert = cert
76 | break certLoop
77 | }
78 | }
79 | }
80 |
81 | if firstCert == nil {
82 | return fmt.Errorf("no certificates with ExtKeyUsageCodeSigning")
83 | }
84 |
85 | jsonWriter := jsoniter.NewStream(jsoniter.ConfigFastest, os.Stdout, 16*1024)
86 | jsonWriter.WriteObjectStart()
87 |
88 | util.WriteStringProperty("commonName", firstCert.Subject.CommonName, jsonWriter)
89 |
90 | // DN
91 | jsonWriter.WriteMore()
92 | util.WriteStringProperty("bloodyMicrosoftSubjectDn", BloodyMsString(firstCert.Subject.ToRDNSequence()), jsonWriter)
93 |
94 | jsonWriter.WriteObjectEnd()
95 |
96 | return util.FlushJsonWriterAndCloseOut(jsonWriter)
97 | }
98 |
99 | func writeError(error string) error {
100 | jsonWriter := jsoniter.NewStream(jsoniter.ConfigFastest, os.Stdout, 16*1024)
101 | jsonWriter.WriteObjectStart()
102 | util.WriteStringProperty("error", error, jsonWriter)
103 | jsonWriter.WriteObjectEnd()
104 | return util.FlushJsonWriterAndCloseOut(jsonWriter)
105 | }
106 |
107 | func readUsingOpenssl(inFile string, password string) ([]*x509.Certificate, error) {
108 | opensslPath := "openssl"
109 | if util.GetCurrentOs() == util.WINDOWS {
110 | vendor, err := download.DownloadWinCodeSign()
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | opensslPath = filepath.Join(vendor, "openssl-ia32", "openssl.exe")
116 | }
117 |
118 | //noinspection SpellCheckingInspection
119 | pemData, err := util.Execute(exec.Command(opensslPath, "pkcs12", "-in", inFile, "-passin", "pass:"+password, "-nokeys"))
120 | if err != nil {
121 | return nil, err
122 | }
123 |
124 | var blocks []byte
125 | rest := pemData
126 | for {
127 | var block *pem.Block
128 | block, rest = pem.Decode(rest)
129 | if block == nil {
130 | log.Debug("PEM not parsed")
131 | break
132 | }
133 |
134 | blocks = append(blocks, block.Bytes...)
135 | if len(rest) == 0 {
136 | break
137 | }
138 | }
139 |
140 |
141 | result, err2 := x509.ParseCertificates(blocks)
142 | if err2 != nil {
143 | return nil, errors.WithStack(err2)
144 | }
145 | return result, nil
146 | }
147 |
148 | //noinspection SpellCheckingInspection
149 | var attributeTypeNames = map[string]string{
150 | "2.5.4.6": "C",
151 | "2.5.4.10": "O",
152 | "2.5.4.11": "OU",
153 | "2.5.4.3": "CN",
154 | "2.5.4.5": "SERIALNUMBER",
155 | "2.5.4.7": "L",
156 | "2.5.4.8": "ST",
157 | "2.5.4.9": "STREET",
158 | "2.5.4.17": "POSTALCODE",
159 | }
160 |
161 | // *** MS uses "The RDN value has quotes" for AppX, see https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/appxmanifestschema/element-identity
162 | // standard escaping doesn't work and forbidden
163 | func BloodyMsString(r pkix.RDNSequence) string {
164 | var s strings.Builder
165 | for i := 0; i < len(r); i++ {
166 | rdn := r[len(r)-1-i]
167 | if i > 0 {
168 | s.WriteRune(',')
169 | }
170 | for j, tv := range rdn {
171 | if j > 0 {
172 | s.WriteRune('+')
173 | }
174 |
175 | oidString := tv.Type.String()
176 | typeName, ok := attributeTypeNames[oidString]
177 | if !ok {
178 | derBytes, err := asn1.Marshal(tv.Value)
179 | if err == nil {
180 | s.WriteString(oidString)
181 | s.WriteString("=#")
182 | s.WriteString(hex.EncodeToString(derBytes))
183 | // no value escaping necessary
184 | continue
185 | }
186 |
187 | typeName = oidString
188 | }
189 |
190 | valueString := fmt.Sprint(tv.Value)
191 | escaped := make([]rune, 0, len(valueString))
192 |
193 | s.WriteString(typeName)
194 | s.WriteRune('=')
195 |
196 | isNeedToBeEscaped := false
197 | for _, c := range valueString {
198 | switch c {
199 | case ',', '+', '"', '\\', '<', '>', ';':
200 | isNeedToBeEscaped = true
201 | }
202 |
203 | if c == '"' {
204 | escaped = append(escaped, '"', c)
205 | } else {
206 | escaped = append(escaped, c)
207 | }
208 | }
209 |
210 | if isNeedToBeEscaped {
211 | s.WriteRune('"')
212 | }
213 | s.WriteString(string(escaped))
214 | if isNeedToBeEscaped {
215 | s.WriteRune('"')
216 | }
217 | }
218 | }
219 | return s.String()
220 | }
221 |
--------------------------------------------------------------------------------
/pkg/download/ActualLocation.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "crypto/sha512"
5 | "encoding/base64"
6 | "fmt"
7 | "io"
8 | "os"
9 |
10 | "github.com/develar/app-builder/pkg/log"
11 | "github.com/develar/app-builder/pkg/util"
12 | "github.com/develar/errors"
13 | "github.com/develar/go-fs-util"
14 | "go.uber.org/zap"
15 | )
16 |
17 | // ActualLocation represents server's status 200 or 206 response metadata. It never holds redirect responses
18 | type ActualLocation struct {
19 | Url string
20 | OutFileName string
21 | isAcceptRanges bool
22 | StatusCode int
23 | ContentLength int64
24 | Parts []*Part
25 | }
26 |
27 | func NewResolvedLocation(url string, contentLength int64, outFileName string, isAcceptRanges bool) ActualLocation {
28 | return ActualLocation{
29 | Url: url,
30 | OutFileName: outFileName,
31 | isAcceptRanges: isAcceptRanges,
32 | ContentLength: contentLength,
33 | }
34 | }
35 |
36 | func (actualLocation *ActualLocation) computeParts(minPartSize int64) {
37 | downloadAsOnePart := false
38 | if util.IsEnvTrue("DISABLE_MULTIPART_DOWNLOADING") {
39 | log.Debug("DISABLE_MULTIPART_DOWNLOADING is set to true, will be downloaded as one part", zap.Int64("length", actualLocation.ContentLength))
40 | downloadAsOnePart = true
41 | } else if actualLocation.ContentLength < 0 {
42 | log.Warn("invalid content length, will be downloaded as one part", zap.Int64("length", actualLocation.ContentLength))
43 | downloadAsOnePart = true
44 | }
45 |
46 | if downloadAsOnePart {
47 | actualLocation.Parts = make([]*Part, 1)
48 | actualLocation.Parts[0] = &Part{
49 | Name: actualLocation.OutFileName,
50 | Start: 0,
51 | End: -1,
52 | }
53 | return
54 | }
55 |
56 | var partCount int
57 | contentLength := actualLocation.ContentLength
58 | if contentLength <= minPartSize {
59 | partCount = 1
60 | } else {
61 | partCount = int(contentLength / minPartSize)
62 | maxPartCount := getMaxPartCount()
63 | if partCount > maxPartCount {
64 | partCount = maxPartCount
65 | }
66 | }
67 |
68 | partSize := contentLength / int64(partCount)
69 | actualLocation.Parts = make([]*Part, partCount)
70 |
71 | start := int64(0)
72 | for i := 0; i < partCount; i++ {
73 | end := start + partSize
74 | if end > contentLength || i == (partCount-1) {
75 | end = contentLength
76 | }
77 |
78 | var name string
79 | if i == 0 {
80 | name = actualLocation.OutFileName
81 | } else {
82 | name = fmt.Sprintf("%s.part%d", actualLocation.OutFileName, i)
83 | }
84 |
85 | actualLocation.Parts[i] = &Part{
86 | Name: name,
87 | Start: start,
88 | End: end,
89 | }
90 |
91 | start = end
92 | }
93 | }
94 |
95 | func (actualLocation *ActualLocation) deleteUnnecessaryParts() {
96 | for i := len(actualLocation.Parts) - 1; i >= 0; i-- {
97 | if actualLocation.Parts[i].Skip {
98 | actualLocation.Parts = append(actualLocation.Parts[:i], actualLocation.Parts[i+1:]...)
99 | }
100 | }
101 | }
102 |
103 | func (actualLocation *ActualLocation) concatenateParts(expectedSha512 string) error {
104 | hasCheckSum := len(expectedSha512) != 0
105 |
106 | fileMode := os.O_APPEND
107 | if hasCheckSum {
108 | if len(actualLocation.Parts) == 1 {
109 | fileMode = os.O_RDONLY
110 | } else {
111 | fileMode |= os.O_RDWR
112 | }
113 | } else {
114 | if len(actualLocation.Parts) == 1 {
115 | return nil
116 | }
117 |
118 | fileMode |= os.O_WRONLY
119 | }
120 |
121 | totalFile, err := os.OpenFile(actualLocation.Parts[0].Name, fileMode, 0644)
122 | if err != nil {
123 | return errors.WithStack(err)
124 | }
125 |
126 | defer util.Close(totalFile)
127 |
128 | buf := make([]byte, 32*1024)
129 | inputHash := sha512.New()
130 | if hasCheckSum {
131 | _, err = io.CopyBuffer(inputHash, totalFile, buf)
132 | if err != nil {
133 | return errors.WithStack(err)
134 | }
135 | }
136 |
137 | for i := 1; i < len(actualLocation.Parts); i++ {
138 | partFileName := actualLocation.Parts[i].Name
139 | partFile, err := os.Open(partFileName)
140 | if err != nil {
141 | return errors.WithStack(err)
142 | }
143 |
144 | var reader io.Reader
145 | if hasCheckSum {
146 | reader = io.TeeReader(partFile, inputHash)
147 | } else {
148 | reader = partFile
149 | }
150 |
151 | _, err = io.CopyBuffer(totalFile, reader, buf)
152 | err = fsutil.CloseAndCheckError(err, partFile)
153 | if err != nil {
154 | return errors.WithStack(err)
155 | }
156 |
157 | removeError := os.Remove(partFileName)
158 | if removeError != nil {
159 | log.Error("cannot delete part file", zap.String("partFile", partFileName), zap.Error(err))
160 | }
161 | }
162 |
163 | if hasCheckSum {
164 | actualCheckSum := base64.StdEncoding.EncodeToString((inputHash).Sum(nil))
165 | if actualCheckSum != expectedSha512 {
166 | return errors.Errorf("sha512 checksum mismatch, expected %s, got %s", expectedSha512, actualCheckSum)
167 | }
168 | }
169 |
170 | return nil
171 | }
172 |
--------------------------------------------------------------------------------
/pkg/download/Part.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "os"
9 | "time"
10 |
11 | "github.com/develar/app-builder/pkg/log"
12 | "github.com/develar/app-builder/pkg/util"
13 | "github.com/develar/errors"
14 | "github.com/develar/go-fs-util"
15 | "go.uber.org/zap"
16 | )
17 |
18 | const maxAttemptNumber = 3
19 |
20 | type Part struct {
21 | Name string
22 |
23 | Start int64
24 | End int64
25 |
26 | Skip bool
27 | isFail bool
28 | }
29 |
30 | func (part *Part) getRange() string {
31 | return fmt.Sprintf("bytes=%d-%d", part.Start, part.End-1)
32 | }
33 |
34 | func (part *Part) download(context context.Context, url string, index int, client *http.Client) error {
35 | // request cannot be reused because Range header is set
36 | request, err := http.NewRequest(http.MethodGet, url, nil)
37 | if err != nil {
38 | return errors.WithStack(err)
39 | }
40 |
41 | request = request.WithContext(context)
42 | request.Header.Set("User-Agent", getUserAgent())
43 | if part.End > 0 {
44 | request.Header.Set("Range", part.getRange())
45 | }
46 |
47 | response, err := part.doRequest(request, client, index)
48 | if err != nil {
49 | return err
50 | }
51 | if response == nil {
52 | return nil
53 | }
54 |
55 | partFile, err := os.OpenFile(part.Name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
56 | if err != nil {
57 | return fsutil.CloseAndCheckError(err, response.Body)
58 | }
59 |
60 | defer util.Close(partFile)
61 |
62 | buf := make([]byte, 32*1024)
63 | for attemptNumber := 0; ; attemptNumber++ {
64 | if attemptNumber != 0 {
65 | time.Sleep(2 * time.Second)
66 | log.Info("retrying", zap.Int("attempt", attemptNumber))
67 | response, err = part.doRequest(request, client, index)
68 | if err != nil {
69 | if response != nil {
70 | err = fsutil.CloseAndCheckError(err, response.Body)
71 | }
72 | if attemptNumber == maxAttemptNumber {
73 | return errors.WithStack(err)
74 | }
75 | continue
76 | }
77 | }
78 |
79 | written, err := writeToFile(partFile, response, &buf)
80 | if err == nil || request.Context().Err() != nil {
81 | return nil
82 | }
83 |
84 | if attemptNumber == maxAttemptNumber {
85 | return errors.WithStack(err)
86 | }
87 |
88 | if part.End > 0 {
89 | part.Start += written
90 | _, err = partFile.Seek(part.Start, io.SeekStart)
91 | if err != nil {
92 | return errors.WithStack(err)
93 | }
94 | request.Header.Set("Range", part.getRange())
95 | } else {
96 | _, err = partFile.Seek(0, io.SeekStart)
97 | if err != nil {
98 | return errors.WithStack(err)
99 | }
100 | }
101 | }
102 | }
103 |
104 | func (part *Part) doRequest(request *http.Request, client *http.Client, index int) (*http.Response, error) {
105 | log.Debug("download part", zap.String("range", request.Header.Get("Range")), zap.Int("index", index))
106 | response, err := client.Do(request)
107 | if err != nil {
108 | return nil, errors.WithStack(err)
109 | }
110 |
111 | switch response.StatusCode {
112 | case http.StatusPartialContent:
113 | return response, nil
114 | case http.StatusOK:
115 | if part.End > 0 {
116 | if index > 0 {
117 | part.Skip = true
118 | util.Close(response.Body)
119 | return nil, nil
120 | }
121 | part.End = response.ContentLength
122 | }
123 | return response, nil
124 | default:
125 | util.Close(response.Body)
126 | return nil, errors.WithStack(fmt.Errorf("part download request failed with status code %d", response.StatusCode))
127 | }
128 | }
129 |
130 | func writeToFile(file *os.File, response *http.Response, buffer *[]byte) (int64, error) {
131 | defer util.Close(response.Body)
132 | return io.CopyBuffer(file, response.Body, *buffer)
133 | }
134 |
--------------------------------------------------------------------------------
/pkg/download/tool.go:
--------------------------------------------------------------------------------
1 | package download
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "runtime"
7 |
8 | "github.com/develar/app-builder/pkg/util"
9 | "github.com/develar/errors"
10 | )
11 |
12 | func DownloadFpm() (string, error) {
13 | currentOs := util.GetCurrentOs()
14 | if currentOs == util.LINUX {
15 | var checksum string
16 | var archSuffix string
17 | if runtime.GOARCH == "amd64" {
18 | //noinspection SpellCheckingInspection
19 | checksum = "fcKdXPJSso3xFs5JyIJHG1TfHIRTGDP0xhSBGZl7pPZlz4/TJ4rD/q3wtO/uaBBYeX0qFFQAFjgu1uJ6HLHghA=="
20 | archSuffix = "-x86_64"
21 | } else {
22 | //noinspection SpellCheckingInspection
23 | checksum = "OnzvBdsHE5djcXcAT87rwbnZwS789ZAd2ehuIO42JWtBAHNzXKxV4o/24XFX5No4DJWGO2YSGQttW+zn7d/4rQ=="
24 | archSuffix = "-x86"
25 | }
26 |
27 | //noinspection SpellCheckingInspection
28 | name := "fpm-1.9.3-2.3.1-linux" + archSuffix
29 | return DownloadArtifact(
30 | name,
31 | GetGithubBaseUrl()+name+"/"+name+".7z",
32 | checksum,
33 | )
34 | } else {
35 | //noinspection SpellCheckingInspection
36 | return downloadFromGithub("fpm", "1.9.3-20150715-2.2.2-mac", "oXfq+0H2SbdrbMik07mYloAZ8uHrmf6IJk+Q3P1kwywuZnKTXSaaeZUJNlWoVpRDWNu537YxxpBQWuTcF+6xfw==")
37 | }
38 | }
39 |
40 | func DownloadZstd(osName util.OsName) (string, error) {
41 | //noinspection SpellCheckingInspection
42 | return DownloadTool(ToolDescriptor{
43 | Name: "zstd",
44 | Version: "1.5.5",
45 | mac: "hL0EMVepIyplxO4c8ZbESm6eGBs8IRMybyk81b76nLk6wHM4dXN9mi7CPmTAMa6gw06ki6Vr4w6vI69+HvIKGg==",
46 | linux: map[string]string{
47 | "x64": "01M9lAhvtX50Lb0CNZ4mY3ajGTVvKwlbDNLjE/e93lg9AfYFDNG5C9twCKbvvrXjatDCT6w3eCCFw0tw5221RA==",
48 | },
49 | win: map[string]string{
50 | "ia32": "jddFtdnYsgXmm9qozFHYqIry8fPlr61ytnKDXV+d7w/HIe4E6kCBZholADqIrGFgcCmblhY4Nh/t8oBTLE7eYQ==",
51 | "x64": "Cg/7RInWfRhfibx4TJ1SMgw5LMeFQp6lH0GA9CP1/EhlE+RomYc1yKJhwDMnO31s0841feZbqdcHTPhQTQyfDg==",
52 | },
53 | }, osName)
54 | }
55 |
56 | func DownloadWinCodeSign() (string, error) {
57 | //noinspection SpellCheckingInspection
58 | return downloadFromGithub("winCodeSign", "2.6.0", "6LQI2d9BPC3Xs0ZoTQe1o3tPiA28c7+PY69Q9i/pD8lY45psMtHuLwv3vRckiVr3Zx1cbNyLlBR8STwCdcHwtA==")
59 | }
60 |
61 | func downloadFromGithub(name string, version string, checksum string) (string, error) {
62 | id := name + "-" + version
63 | return DownloadArtifact(id, GetGithubBaseUrl()+GetGithubReleaseUrl(id)+"/"+id+".7z", checksum)
64 | }
65 |
66 | func GetGithubBaseUrl() string {
67 | v := os.Getenv("NPM_CONFIG_ELECTRON_BUILDER_BINARIES_MIRROR")
68 | if len(v) == 0 {
69 | v = os.Getenv("npm_config_electron_builder_binaries_mirror")
70 | }
71 | if len(v) == 0 {
72 | v = os.Getenv("npm_package_config_electron_builder_binaries_mirror")
73 | }
74 | if len(v) == 0 {
75 | v = os.Getenv("ELECTRON_BUILDER_BINARIES_MIRROR")
76 | }
77 | if len(v) == 0 {
78 | v = "https://github.com/electron-userland/electron-builder-binaries/releases/download/"
79 | }
80 | return v
81 | }
82 |
83 | func GetGithubReleaseUrl(defaultName string) string {
84 | v := os.Getenv("NPM_CONFIG_ELECTRON_BUILDER_BINARIES_CUSTOM_DIR")
85 | if len(v) == 0 {
86 | v = os.Getenv("npm_config_electron_builder_binaries_custom_dir")
87 | }
88 | if len(v) == 0 {
89 | v = os.Getenv("npm_package_config_electron_builder_binaries_custom_dir")
90 | }
91 | if len(v) == 0 {
92 | v = os.Getenv("ELECTRON_BUILDER_BINARIES_CUSTOM_DIR")
93 | }
94 | if len(v) == 0 {
95 | v = defaultName
96 | }
97 | return v
98 | }
99 |
100 | func DownloadTool(descriptor ToolDescriptor, osName util.OsName) (string, error) {
101 | arch := runtime.GOARCH
102 | switch arch {
103 | case "arm":
104 | //noinspection SpellCheckingInspection
105 | arch = "armv7"
106 | case "arm64":
107 | //noinspection SpellCheckingInspection
108 | arch = "armv8"
109 | case "amd64":
110 | arch = "x64"
111 | }
112 |
113 | var checksum string
114 | var archQualifier string
115 | var osQualifier string
116 | if osName == util.MAC {
117 | checksum = descriptor.mac
118 | archQualifier = ""
119 | osQualifier = "mac"
120 | } else {
121 | archQualifier = "-" + arch
122 | if osName == util.WINDOWS {
123 | osQualifier = "win"
124 | checksum = descriptor.win[arch]
125 | } else {
126 | osQualifier = "linux"
127 | checksum = descriptor.linux[arch]
128 | }
129 | }
130 |
131 | if checksum == "" {
132 | return "", errors.Errorf("Checksum not specified for %s:%s", osName, arch)
133 | }
134 |
135 | repository := descriptor.repository
136 | if repository == "" {
137 | repository = "electron-userland/electron-builder-binaries"
138 | }
139 |
140 | var tagPrefix string
141 | if descriptor.repository == "" {
142 | tagPrefix = descriptor.Name + "-"
143 | } else {
144 | tagPrefix = "v"
145 | }
146 |
147 | osAndArch := osQualifier + archQualifier
148 | return DownloadArtifact(
149 | descriptor.Name+"-"+descriptor.Version+"-"+osAndArch, /* ability to use cache dir on any platform (e.g. keep cache under project) */
150 | "https://github.com/"+repository+"/releases/download/"+tagPrefix+descriptor.Version+"/"+descriptor.Name+"-v"+descriptor.Version+"-"+osAndArch+".7z",
151 | checksum,
152 | )
153 | }
154 |
155 | type ToolDescriptor struct {
156 | Name string
157 | Version string
158 |
159 | repository string
160 |
161 | mac string
162 | linux map[string]string
163 | win map[string]string
164 | }
165 |
166 | func GetZstd() (string, error) {
167 | dir, err := DownloadZstd(util.GetCurrentOs())
168 | if err != nil {
169 | return "", err
170 | }
171 |
172 | executableName := "zstd"
173 | if util.GetCurrentOs() == util.WINDOWS {
174 | executableName += ".exe"
175 | }
176 |
177 | return filepath.Join(dir, executableName), nil
178 | }
179 |
--------------------------------------------------------------------------------
/pkg/electron/electronDownloader.go:
--------------------------------------------------------------------------------
1 | package electron
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "strings"
7 |
8 | "github.com/alecthomas/kingpin"
9 | "github.com/develar/app-builder/pkg/download"
10 | "github.com/develar/app-builder/pkg/log"
11 | "github.com/develar/app-builder/pkg/util"
12 | "github.com/develar/errors"
13 | "github.com/develar/go-fs-util"
14 | "github.com/json-iterator/go"
15 | "go.uber.org/zap"
16 | )
17 |
18 | type ElectronDownloadOptions struct {
19 | Version string `json:"version"`
20 | CacheDir string `json:"cache"`
21 | Mirror string `json:"mirror"`
22 |
23 | Platform string `json:"platform"`
24 | Arch string `json:"arch"`
25 |
26 | CustomDir string `json:"customDir"`
27 | CustomFilename string `json:"customFilename"`
28 | }
29 |
30 | func ConfigureCommand(app *kingpin.Application) {
31 | command := app.Command("download-electron", "")
32 | jsonConfig := command.Flag("configuration", "").Short('c').Required().String()
33 |
34 | command.Action(func(context *kingpin.ParseContext) error {
35 | configs, err := parseConfig(jsonConfig)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | _, err = downloadElectron(configs)
41 | return err
42 | })
43 | }
44 |
45 | func parseConfig(jsonConfig *string) ([]ElectronDownloadOptions, error) {
46 | var configs []ElectronDownloadOptions
47 | err := jsoniter.UnmarshalFromString(*jsonConfig, &configs)
48 | if err != nil {
49 | return nil, err
50 | }
51 | return configs, nil
52 | }
53 |
54 | func downloadElectron(configs []ElectronDownloadOptions) ([]string, error) {
55 | result := make([]string, len(configs))
56 | return result, util.MapAsync(len(configs), func(taskIndex int) (func() error, error) {
57 | config := configs[taskIndex]
58 | return func() error {
59 | cacheDir := config.CacheDir
60 | if cacheDir == "" {
61 | var err error
62 | cacheDir, err = download.GetCacheDirectory("electron", "ELECTRON_CACHE", false)
63 | if err != nil {
64 | return err
65 | }
66 | }
67 |
68 | electronDownloader := &ElectronDownloader{
69 | config: &config,
70 | cacheDir: cacheDir,
71 | }
72 |
73 | cachedFile, err := electronDownloader.Download()
74 | if err != nil {
75 | return err
76 | }
77 |
78 | result[taskIndex] = cachedFile
79 |
80 | return nil
81 | }, nil
82 | })
83 | }
84 |
85 | func getBaseUrl(config *ElectronDownloadOptions) string {
86 | v := config.Mirror
87 | if len(v) == 0 {
88 | v = os.Getenv("NPM_CONFIG_ELECTRON_MIRROR")
89 | }
90 | if len(v) == 0 {
91 | v = os.Getenv("npm_config_electron_mirror")
92 | }
93 | if len(v) == 0 {
94 | v = os.Getenv("ELECTRON_MIRROR")
95 | }
96 | if len(v) == 0 {
97 | if strings.Contains(config.Version, "-nightly.") {
98 | v = "https://github.com/electron/nightlies/releases/download/"
99 | } else {
100 | v = "https://github.com/electron/electron/releases/download/"
101 | }
102 | }
103 | // Compatibility with previous code caused user who need to set mirror with a suffix `/v`
104 | if strings.HasSuffix(v, "/v") {
105 | v = v[:len(v)-1]
106 | }
107 | return v
108 | }
109 |
110 | func normalizeVersion(version string) string {
111 | if strings.HasPrefix(version, "v") {
112 | return version
113 | }
114 | return "v" + version
115 | }
116 |
117 | func getMiddleUrl(config *ElectronDownloadOptions) string {
118 | v := os.Getenv("ELECTRON_CUSTOM_DIR")
119 | if len(v) == 0 {
120 | v = config.CustomDir
121 | }
122 | if len(v) == 0 {
123 | v = normalizeVersion(config.Version)
124 | }
125 | return v
126 | }
127 |
128 | func getUrlSuffix(config *ElectronDownloadOptions) string {
129 | v := os.Getenv("ELECTRON_CUSTOM_FILENAME")
130 | if len(v) == 0 {
131 | v = config.CustomFilename
132 | }
133 | if len(v) == 0 {
134 | v = getFilename(config)
135 | }
136 | return v
137 | }
138 |
139 | func getFilename(config *ElectronDownloadOptions) string {
140 | return "electron-" + normalizeVersion(config.Version) + "-" + config.Platform + "-" + config.Arch + ".zip"
141 | }
142 |
143 | type ElectronDownloader struct {
144 | config *ElectronDownloadOptions
145 |
146 | cacheDir string
147 | }
148 |
149 | func (t *ElectronDownloader) getCachedFile() string {
150 | fileName := t.config.CustomFilename
151 | if len(fileName) == 0 {
152 | fileName = getFilename(t.config)
153 | }
154 | return filepath.Join(t.cacheDir, fileName)
155 | }
156 |
157 | func (t *ElectronDownloader) Download() (string, error) {
158 | if t.config.Version == "" {
159 | return "", errors.New("version not specified")
160 | }
161 | if t.config.Platform == "" {
162 | return "", errors.New("platform not specified")
163 | }
164 | if t.config.Arch == "" {
165 | return "", errors.New("arch not specified")
166 | }
167 |
168 | cachedFile := t.getCachedFile()
169 |
170 | fileInfo, err := os.Stat(cachedFile)
171 | if err != nil && !os.IsNotExist(err) {
172 | return "", errors.WithStack(err)
173 | }
174 |
175 | if fileInfo != nil {
176 | if fileInfo.IsDir() {
177 | return "", errors.New("File expected, but got dir")
178 | }
179 | return cachedFile, nil
180 | }
181 |
182 | err = fsutil.EnsureDir(t.cacheDir)
183 | if err != nil {
184 | return "", errors.WithStack(err)
185 | }
186 |
187 | url := getBaseUrl(t.config) + getMiddleUrl(t.config) + "/" + getUrlSuffix(t.config)
188 | err = t.doDownload(url, cachedFile)
189 | if err != nil {
190 | return "", errors.WithStack(err)
191 | }
192 |
193 | return cachedFile, nil
194 | }
195 |
196 | func (t *ElectronDownloader) doDownload(url string, cachedFile string) error {
197 | tempFile, err := util.TempFile(t.cacheDir, ".zip")
198 | if err != nil {
199 | return errors.WithStack(err)
200 | }
201 |
202 | downloader := download.NewDownloader()
203 | err = downloader.Download(url, tempFile, "")
204 | if err != nil {
205 | return errors.WithStack(err)
206 | }
207 |
208 | download.RenameToFinalFile(tempFile, cachedFile, log.LOG.With(zap.String("url", url), zap.String("path", cachedFile)))
209 | return nil
210 | }
211 |
--------------------------------------------------------------------------------
/pkg/electron/electronUnpack.go:
--------------------------------------------------------------------------------
1 | package electron
2 |
3 | import (
4 | "archive/zip"
5 | "io"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/alecthomas/kingpin"
10 | "github.com/develar/app-builder/pkg/archive/zipx"
11 | "github.com/develar/app-builder/pkg/log"
12 | "github.com/develar/app-builder/pkg/util"
13 | "github.com/develar/go-fs-util"
14 | "go.uber.org/zap"
15 | )
16 |
17 | func ConfigureUnpackCommand(app *kingpin.Application) {
18 | command := app.Command("unpack-electron", "")
19 | jsonConfig := command.Flag("configuration", "").Short('c').Required().String()
20 | outputDir := command.Flag("output", "").Required().String()
21 | distMacOsAppName := command.Flag("distMacOsAppName", "").Default("Electron.app").String()
22 |
23 | command.Action(func(context *kingpin.ParseContext) error {
24 | var configs []ElectronDownloadOptions
25 | err := util.DecodeBase64IfNeeded(*jsonConfig, &configs)
26 | if err != nil {
27 | return err
28 | }
29 | return UnpackElectron(configs, *outputDir, *distMacOsAppName, true)
30 | })
31 | }
32 |
33 | func UnpackElectron(configs []ElectronDownloadOptions, outputDir string, distMacOsAppName string, isReDownloadOnFileReadError bool) error {
34 | cachedElectronZip := make(chan string, 1)
35 | err := util.MapAsync(2, func(taskIndex int) (func() error, error) {
36 | if taskIndex == 0 {
37 | return func() error {
38 | return fsutil.EnsureEmptyDir(outputDir)
39 | }, nil
40 | } else {
41 | return func() error {
42 | result, err := downloadElectron(configs)
43 | if err != nil {
44 | return err
45 | }
46 |
47 | cachedElectronZip <- result[0]
48 | return nil
49 | }, nil
50 | }
51 | })
52 |
53 | if err != nil {
54 | return err
55 | }
56 |
57 | if len(distMacOsAppName) == 0 {
58 | distMacOsAppName = "Electron.app"
59 | }
60 |
61 | excludedFiles := make(map[string]bool)
62 | excludedFiles[filepath.Join(outputDir, distMacOsAppName, "Contents", "Resources", "default_app.asar")] = true
63 | excludedFiles[filepath.Join(outputDir, "resources", "default_app.asar")] = true
64 |
65 | excludedFiles[filepath.Join(outputDir, distMacOsAppName, "Contents", "Resources", "inspector", ".htaccess")] = true
66 | excludedFiles[filepath.Join(outputDir, "resources", "inspector", ".htaccess")] = true
67 |
68 | excludedFiles[filepath.Join(outputDir, "version")] = true
69 |
70 | zipFile := <-cachedElectronZip
71 | err = zipx.Unzip(zipFile, outputDir, excludedFiles)
72 | if err != nil {
73 | if isReDownloadOnFileReadError && (err == zip.ErrFormat || err == io.ErrUnexpectedEOF) {
74 | log.Warn("cannot unpack electron zip file, will be re-downloaded", zap.Error(err))
75 | // not just download and unzip, but full - including clearing of output dir
76 | err = os.Remove(zipFile)
77 | if err != nil && !os.IsNotExist(err) {
78 | log.Warn("cannot delete", zap.Error(err), zap.String("file", zipFile))
79 | }
80 |
81 | return UnpackElectron(configs, outputDir, distMacOsAppName, false)
82 | } else {
83 | return err
84 | }
85 | }
86 |
87 | return nil
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/fs/copier.go:
--------------------------------------------------------------------------------
1 | package fs
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "runtime"
7 |
8 | "github.com/develar/app-builder/pkg/log"
9 | "github.com/develar/app-builder/pkg/util"
10 | "github.com/develar/errors"
11 | "github.com/develar/go-fs-util"
12 | "github.com/oxtoacart/bpool"
13 | "go.uber.org/zap"
14 | )
15 |
16 | var bufferPool = bpool.NewBytePool(runtime.NumCPU(), 64*1024)
17 |
18 | type FileCopier struct {
19 | IsUseHardLinks bool
20 | }
21 |
22 | // go doesn't provide native copy operation (CoW)
23 | func (t *FileCopier) copyDir(from string, to string) error {
24 | fileNames, err := fsutil.ReadDirContent(from)
25 | if err != nil {
26 | return errors.WithStack(err)
27 | }
28 |
29 | for _, name := range fileNames {
30 | if name == ".DS_Store" {
31 | continue
32 | }
33 |
34 | err = t.copyDirOrFile(filepath.Join(from, name), filepath.Join(to, name), false)
35 | if err != nil {
36 | return errors.WithStack(err)
37 | }
38 | }
39 |
40 | return nil
41 | }
42 |
43 | func CopyUsingHardlink(from string, to string) error {
44 | var fileCopier FileCopier
45 | fileCopier.IsUseHardLinks = true
46 | return fileCopier.CopyDirOrFile(from, to)
47 | }
48 |
49 | func CopyDirOrFile(from string, to string) error {
50 | var fileCopier FileCopier
51 | return fileCopier.CopyDirOrFile(from, to)
52 | }
53 |
54 | func (t *FileCopier) CopyDirOrFile(from string, to string) error {
55 | if runtime.GOOS == "windows" {
56 | t.IsUseHardLinks = false
57 | }
58 |
59 | log.Debug("copy files", zap.String("from", from), zap.String("to", to), zap.Bool("isUseHardLinks", t.IsUseHardLinks))
60 | err := t.copyDirOrFile(from, to, true)
61 | if err != nil {
62 | return errors.WithStack(err)
63 | }
64 | return nil
65 | }
66 |
67 | func (t *FileCopier) copyDirOrFile(from string, to string, isCreateParentDirs bool) error {
68 | fromInfo, err := os.Lstat(from)
69 | if err != nil {
70 | return errors.WithStack(err)
71 | }
72 |
73 | if fromInfo.IsDir() {
74 | // cannot use file mode as is because of *** *** *** umask
75 | if isCreateParentDirs {
76 | err = fsutil.EnsureDir(to)
77 | } else {
78 | err = os.Mkdir(to, 0777)
79 | }
80 | if err != nil && !os.IsExist(err) {
81 | return errors.WithStack(err)
82 | }
83 |
84 | err = SetNormalDirPermissions(to)
85 | if err != nil {
86 | return err
87 | }
88 |
89 | return t.copyDir(from, to)
90 | }
91 |
92 | if isCreateParentDirs {
93 | err := fsutil.EnsureDir(filepath.Dir(to))
94 | if err != nil {
95 | return err
96 | }
97 | }
98 |
99 | if (fromInfo.Mode() & os.ModeSymlink) != 0 {
100 | return t.createSymlink(from, to)
101 | } else {
102 | return t.CopyFile(from, to, isCreateParentDirs, fromInfo)
103 | }
104 | }
105 |
106 | func (t *FileCopier) CopyFile(from string, to string, isCreateParentDirs bool, fromInfo os.FileInfo) error {
107 | if t.IsUseHardLinks {
108 | err := os.Link(from, to)
109 | if err == nil {
110 | return nil
111 | }
112 |
113 | t.IsUseHardLinks = false
114 | log.Debug("cannot copy using hard link", zap.Error(err), zap.String("from", from), zap.String("to", to))
115 | }
116 |
117 | return CopyFileAndRestoreNormalPermissions(from, to, fromInfo.Mode())
118 | }
119 |
120 | func CopyFileAndRestoreNormalPermissions(from string, to string, fileMode os.FileMode) error {
121 | sourceFile, err := os.Open(from)
122 | if err != nil {
123 | return errors.WithStack(err)
124 | }
125 |
126 | defer util.Close(sourceFile)
127 | buffer := bufferPool.Get()
128 | err = WriteFileAndRestoreNormalPermissions(sourceFile, to, fileMode, buffer)
129 | bufferPool.Put(buffer)
130 | if err != nil {
131 | return err
132 | }
133 | return nil
134 | }
135 |
136 | func (t *FileCopier) createSymlink(from string, to string) error {
137 | link, err := os.Readlink(from)
138 | if err != nil {
139 | return errors.WithStack(err)
140 | }
141 |
142 | if filepath.IsAbs(link) {
143 | link, err = filepath.Rel(filepath.Dir(from), link)
144 | if err != nil {
145 | return errors.WithStack(err)
146 | }
147 | }
148 |
149 | err = os.Symlink(link, to)
150 | if err != nil {
151 | return errors.WithStack(err)
152 | }
153 |
154 | return nil
155 | }
156 |
--------------------------------------------------------------------------------
/pkg/fs/file.go:
--------------------------------------------------------------------------------
1 | package fs
2 |
3 | import (
4 | "io"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 |
9 | "github.com/develar/errors"
10 | "github.com/develar/go-fs-util"
11 | "github.com/phayes/permbits"
12 | )
13 |
14 | func SetNormalDirPermissions(path string) error {
15 | // https://github.com/electron-userland/electron-builder/issues/2682
16 | // always set dir permission to 0755 regardless of what was originally
17 | if runtime.GOOS != "windows" {
18 | return permbits.Chmod(path, 0755)
19 | }
20 | return nil
21 | }
22 |
23 | // https://github.com/electron-userland/electron-builder/issues/2654#issuecomment-369972916
24 | // https://github.com/electron-userland/electron-builder/issues/3452#issuecomment-438619535
25 | func SetNormalFilePermissions(path string) error {
26 | if runtime.GOOS != "windows" {
27 | return permbits.Chmod(path, 0644)
28 | }
29 | return nil
30 | }
31 |
32 | func ReadFile(file string, size int) ([]byte, error) {
33 | reader, err := os.Open(file)
34 | if err != nil {
35 | return nil, errors.WithStack(err)
36 | }
37 |
38 | result := make([]byte, size)
39 | _, err = reader.Read(result)
40 | return result, fsutil.CloseAndCheckError(err, reader)
41 | }
42 |
43 | func createFileAndCreateParentDirIfNeeded(name string) (*os.File, error) {
44 | flag := os.O_WRONLY|os.O_CREATE|os.O_TRUNC
45 | // cannot use file mode as is because of *** *** *** umask
46 | file, err := os.OpenFile(name, flag, 0666)
47 | if err == nil {
48 | return file, nil
49 | }
50 |
51 | if !os.IsNotExist(err) {
52 | return nil, errors.WithStack(err)
53 | }
54 |
55 | dir := filepath.Dir(name)
56 | err = os.MkdirAll(dir, 0777)
57 | if err != nil {
58 | return nil, errors.WithStack(err)
59 | }
60 |
61 | err = SetNormalDirPermissions(dir)
62 | if err != nil {
63 | return nil, err
64 | }
65 |
66 | file, err = os.OpenFile(name, flag, 0666)
67 | if err != nil {
68 | return nil, errors.WithStack(err)
69 | }
70 |
71 | return file, nil
72 | }
73 |
74 | func WriteFileAndRestoreNormalPermissions(source io.Reader, to string, fileMode os.FileMode, buffer []byte) error {
75 | destinationFile, err := createFileAndCreateParentDirIfNeeded(to)
76 | if err != nil {
77 | return err
78 | }
79 |
80 | _, err = io.CopyBuffer(destinationFile, source, buffer)
81 | if err != nil {
82 | _ = destinationFile.Close()
83 | return errors.WithStack(err)
84 | }
85 |
86 | err = destinationFile.Close()
87 | if err != nil {
88 | return errors.WithStack(err)
89 | }
90 |
91 | err = fixPermissions(to, fileMode)
92 | if err != nil {
93 | return err
94 | }
95 |
96 | return nil
97 | }
98 |
99 | func fixPermissions(filePath string, fileMode os.FileMode) error {
100 | originalPermissions := permbits.PermissionBits(fileMode)
101 | permissions := originalPermissions
102 |
103 | if originalPermissions.UserExecute() {
104 | permissions.SetGroupExecute(true)
105 | permissions.SetOtherExecute(true)
106 | }
107 |
108 | permissions.SetUserRead(true)
109 | permissions.SetGroupRead(true)
110 | permissions.SetOtherRead(true)
111 |
112 | permissions.SetSetuid(false)
113 | permissions.SetSetgid(false)
114 |
115 | return errors.WithStack(permbits.Chmod(filePath, permissions))
116 | }
--------------------------------------------------------------------------------
/pkg/fs/findParent.go:
--------------------------------------------------------------------------------
1 | package fs
2 |
3 | import (
4 | "os"
5 | "path"
6 | "path/filepath"
7 | )
8 |
9 | func pathExists(path string) bool {
10 | _, err := os.Stat(path)
11 | return err == nil
12 | }
13 |
14 | func FindParentWithFile(cwd string, file string) string {
15 | if pathExists(path.Join(cwd, file)) {
16 | return cwd
17 | }
18 | parent := filepath.Dir(cwd)
19 | if parent == cwd {
20 | return ""
21 | }
22 | return FindParentWithFile(parent, file)
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/icons/collect-icons.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "regexp"
7 | "strconv"
8 | "strings"
9 |
10 | "github.com/develar/app-builder/pkg/log"
11 | "github.com/develar/errors"
12 | "github.com/develar/go-fs-util"
13 | "go.uber.org/zap"
14 | )
15 |
16 | func CollectIcons(sourceDir string) ([]IconInfo, string, error) {
17 | files, err := fsutil.ReadDirContent(sourceDir)
18 | if err != nil {
19 | if os.IsNotExist(err) {
20 | return nil, "", errors.Errorf("icon directory %s doesn't exist", sourceDir)
21 | }
22 |
23 | fileInfo, statErr := os.Stat(sourceDir)
24 | if statErr == nil && !fileInfo.IsDir() {
25 | return nil, "", errors.Errorf("icon directory %s is not a directory", sourceDir)
26 | }
27 | return nil, "", errors.WithStack(err)
28 | }
29 |
30 | var result []IconInfo
31 | re := regexp.MustCompile("[0-9]+")
32 | var iconFilename string
33 | sizeToFileName := make(map[int]*IconInfo)
34 | for _, name := range files {
35 | if !(strings.HasSuffix(name, ".png") || strings.HasSuffix(name, ".PNG")) {
36 | continue
37 | }
38 |
39 | sizeString := re.FindString(name)
40 | if sizeString == "" {
41 | if name == "icon.png" {
42 | iconFilename = name
43 | }
44 | continue
45 | }
46 |
47 | size, err := strconv.Atoi(sizeString)
48 | if err != nil {
49 | // unrealistic case
50 | return nil, "", errors.WithStack(err)
51 | }
52 |
53 | iconPath := filepath.Join(sourceDir, name)
54 |
55 | existing := sizeToFileName[size]
56 | if existing != nil {
57 | // 16x16.png vs 16x16-dev.png - select shorter name
58 | if len(name) >= len(filepath.Base(existing.File)) {
59 | continue
60 | } else {
61 | existing.File = iconPath
62 | break
63 | }
64 | }
65 |
66 | iconInfo := IconInfo{iconPath, size}
67 | sizeToFileName[size] = &iconInfo
68 | result = append(result, iconInfo)
69 | }
70 |
71 | if len(result) == 0 {
72 | if len(iconFilename) == 0 {
73 | return nil, "", errors.Errorf("icon directory %s doesn't contain icons", sourceDir)
74 | }
75 |
76 | log.Debug("icon directory doesn't contain icons ([0-9]+.png), but icon.png exists", zap.String("iconDir", sourceDir))
77 | return nil, filepath.Join(sourceDir, iconFilename), nil
78 | }
79 |
80 | sortBySize(result)
81 | return result, "", nil
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/icons/error.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import "fmt"
4 |
5 | type ImageSizeError struct {
6 | File string
7 | RequiredMinSize int
8 | errorCode string
9 | }
10 |
11 | type ImageFormatError struct {
12 | File string
13 | errorCode string
14 | }
15 |
16 | func (e *ImageSizeError) ErrorCode() string {
17 | return e.errorCode
18 | }
19 |
20 | func (e *ImageFormatError) ErrorCode() string {
21 | return e.errorCode
22 | }
23 |
24 | func (e *ImageSizeError) Error() string {
25 | return fmt.Sprintf("image %s must be at least %dx%d", e.File, e.RequiredMinSize, e.RequiredMinSize)
26 | }
27 |
28 | func (e *ImageFormatError) Error() string {
29 | return fmt.Sprintf("image %s shas unknown format", e.File)
30 | }
31 |
32 | func NewImageSizeError(file string, requiredMinSize int) *ImageSizeError {
33 | return &ImageSizeError{file, requiredMinSize, "ERR_ICON_TOO_SMALL"}
34 | }
35 |
--------------------------------------------------------------------------------
/pkg/icons/fileResolver.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 |
7 | "github.com/develar/app-builder/pkg/log"
8 | "github.com/develar/errors"
9 | "go.uber.org/zap"
10 | )
11 |
12 | // returns file if exists, null if file not exists, or error if unknown error
13 | func resolveSourceFileOrNull(sourceFile string, roots []string) (string, os.FileInfo, error) {
14 | if filepath.IsAbs(sourceFile) {
15 | cleanPath := filepath.Clean(sourceFile)
16 | fileInfo, err := os.Stat(cleanPath)
17 | if err == nil {
18 | return cleanPath, fileInfo, nil
19 | }
20 | return "", nil, errors.WithStack(err)
21 | }
22 |
23 | for _, root := range roots {
24 | resolvedPath := filepath.Join(root, sourceFile)
25 | fileInfo, err := os.Stat(resolvedPath)
26 | switch {
27 | case err == nil:
28 | return resolvedPath, fileInfo, nil
29 | case os.IsNotExist(err):
30 | log.Debug("path doesn't exist", zap.String("path", resolvedPath))
31 | default:
32 | log.Debug("tried resolved path, but got error", zap.String("path", resolvedPath), zap.Error(err))
33 | }
34 | }
35 |
36 | return "", nil, nil
37 | }
38 |
39 | func resolveSourceFile(sourceFiles []string, roots []string) (string, os.FileInfo, error) {
40 | for _, sourceFile := range sourceFiles {
41 | resolvedPath, fileInfo, err := resolveSourceFileOrNull(sourceFile, roots)
42 | if err != nil {
43 | return "", nil, err
44 | }
45 | if fileInfo != nil {
46 | return resolvedPath, fileInfo, nil
47 | }
48 | }
49 |
50 | return "", nil, nil
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/icons/icns-to-png.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "os"
7 | "os/exec"
8 | "path/filepath"
9 | "runtime"
10 |
11 | "github.com/develar/app-builder/pkg/log"
12 | "github.com/develar/app-builder/pkg/util"
13 | "github.com/develar/errors"
14 | "github.com/develar/go-fs-util"
15 | "github.com/disintegration/imaging"
16 | )
17 |
18 | type Icns2PngMapping struct {
19 | Id string
20 | Size int
21 | }
22 |
23 | var icnsTypeToSize = []Icns2PngMapping{
24 | {"is32", 16},
25 | {"il32", 32},
26 | {"ih32", 48},
27 | {"icp6", 64},
28 | {"it32", 128},
29 | {ICNS_256, 256},
30 | {ICNS_512, 512},
31 | }
32 |
33 | func ConvertIcnsToPng(inFile string, outDir string) ([]IconInfo, error) {
34 | err := fsutil.EnsureEmptyDir(outDir)
35 | if err != nil {
36 | return nil, errors.WithStack(err)
37 | }
38 |
39 | var sizeList []int
40 | var result []IconInfo
41 | if runtime.GOOS == "darwin" && os.Getenv("FORCE_ICNS2PNG") == "" {
42 | result, err = ConvertIcnsToPngUsingIconUtil(inFile, outDir, &sizeList)
43 | if err != nil {
44 | return nil, errors.WithStack(err)
45 | }
46 | } else {
47 | result, err = ConvertIcnsToPngUsingOpenJpeg(inFile, outDir)
48 | if err != nil {
49 | return nil, errors.WithStack(err)
50 | }
51 |
52 | sortBySize(result)
53 | for _, item := range icnsTypeToSize {
54 | if !hasSize(result, item.Size) {
55 | sizeList = append(sizeList, item.Size)
56 | }
57 | }
58 | }
59 |
60 | maxIconInfo := result[len(result)-1]
61 | err = multiResizeImage(maxIconInfo.File, filepath.Join(outDir, "icon_%dx%d.png"), &result, sizeList)
62 | if err != nil {
63 | return nil, errors.WithStack(err)
64 | }
65 |
66 | sortBySize(result)
67 | return result, nil
68 | }
69 |
70 | func ConvertIcnsToPngUsingIconUtil(inFile string, outDir string, sizeList *[]int) ([]IconInfo, error) {
71 | // iconutil requires suffix .iconset
72 | outDir = filepath.Join(outDir, "result.iconset")
73 | err := os.Mkdir(outDir, 0755)
74 | if err != nil {
75 | return nil, errors.WithStack(err)
76 | }
77 |
78 | output, err := exec.Command("iconutil", "--convert", "iconset", "--output", outDir, inFile).CombinedOutput()
79 | if err != nil {
80 | log.Info(string(output))
81 | return nil, errors.WithStack(err)
82 | }
83 |
84 | iconFileNames, err := fsutil.ReadDirContent(outDir)
85 | if err != nil {
86 | return nil, errors.WithStack(err)
87 | }
88 |
89 | var result []IconInfo
90 | for _, item := range icnsTypeToSize {
91 | fileName := fmt.Sprintf("icon_%dx%d.png", item.Size, item.Size)
92 | if util.ContainsString(iconFileNames, fileName) {
93 | result = append(result, IconInfo{filepath.Join(outDir, fileName), item.Size})
94 | } else {
95 | *sizeList = append(*sizeList, item.Size)
96 | }
97 | }
98 | return result, nil
99 | }
100 |
101 | func hasSize(list []IconInfo, size int) bool {
102 | for _, info := range list {
103 | if info.Size == size {
104 | return true
105 | }
106 | }
107 | return false
108 | }
109 |
110 | func multiResizeImage(inFile string, outFileNameFormat string, result *[]IconInfo, sizeList []int) error {
111 | imageCount := len(sizeList)
112 | if imageCount == 0 {
113 | return nil
114 | }
115 |
116 | originalImage, err := LoadImage(inFile)
117 | if err != nil {
118 | return errors.WithStack(err)
119 | }
120 |
121 | return multiResizeImage2(&originalImage, outFileNameFormat, result, sizeList)
122 | }
123 |
124 | func multiResizeImage2(originalImage *image.Image, outFileNameFormat string, result *[]IconInfo, sizeList []int) error {
125 | imageCount := len(sizeList)
126 | if imageCount == 0 {
127 | return nil
128 | }
129 |
130 | maxSize := (*originalImage).Bounds().Max.X
131 |
132 | return util.MapAsync(imageCount, func(taskIndex int) (func() error, error) {
133 | size := sizeList[taskIndex]
134 | if size > maxSize {
135 | return nil, nil
136 | }
137 |
138 | outFilePath := fmt.Sprintf(outFileNameFormat, size, size)
139 | *result = append(*result, IconInfo{
140 | File: outFilePath,
141 | Size: size,
142 | })
143 |
144 | return func() error {
145 | newImage := imaging.Resize(*originalImage, size, size, imaging.Lanczos)
146 | return SaveImage(newImage, outFilePath, PNG)
147 | }, nil
148 | })
149 | }
150 |
--------------------------------------------------------------------------------
/pkg/icons/icns.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/binary"
7 | "image/png"
8 | "io"
9 | "io/ioutil"
10 |
11 | "github.com/develar/errors"
12 | "github.com/develar/go-fs-util"
13 | "github.com/disintegration/imaging"
14 | )
15 |
16 | //noinspection GoSnakeCaseUsage
17 | const (
18 | ICNS_256 = "ic08"
19 | ICNS_256_RETINA = "ic13"
20 | ICNS_512 = "ic09"
21 | ICNS_512_RETINA = "ic14"
22 | ICNS_1024 = "ic10"
23 | )
24 |
25 | var (
26 | icnsHeader = []byte{0x69, 0x63, 0x6e, 0x73}
27 |
28 | icnsExpectedSizes = []int{16, 32, 64, 128, 256, 512, 1024}
29 |
30 | // all icon sizes mapped to their respective possible OSTypes, this includes old OSTypes such as ic08 recognized on 10.5
31 | // https://github.com/electron-userland/electron-builder/issues/2533
32 | // AppIcon Generator also doesn't produce 16x16 from 1024x1025 PNG source (only 16x16@2x "retina" icon)
33 | sizeToType = map[int][]string{
34 | 16: {"icp4"},
35 | 32: {"ic11"}, // icp5 is not generated by AppIcon Generator
36 | 64: {"ic12"}, // icp6 is not generated by AppIcon Generator
37 | 128: {"ic07"},
38 | 256: {ICNS_256, ICNS_256_RETINA},
39 | 512: {ICNS_512, ICNS_512_RETINA},
40 | 1024: {ICNS_1024},
41 | }
42 | )
43 |
44 | func ConvertToIcns(inputInfo InputFileInfo, outFilePath string) error {
45 | // create a new buffer to hold the series of icons generated via resizing
46 | icns := new(bytes.Buffer)
47 |
48 | for _, size := range icnsExpectedSizes {
49 | if size > inputInfo.MaxIconSize {
50 | // do not upscale
51 | continue
52 | }
53 |
54 | var imageData []byte
55 | var err error
56 | existingFile, exists := inputInfo.SizeToPath[size]
57 | if exists {
58 | imageData, err = ioutil.ReadFile(existingFile)
59 | if err != nil {
60 | return errors.WithStack(err)
61 | }
62 | } else {
63 | if size == 16 {
64 | // https://github.com/electron-userland/electron-builder/issues/2533
65 | // AppIcon Generator also doesn't produce 16x16 from 1024x1025 PNG source (only 16x16@2x "retina" icon)
66 | continue
67 | }
68 |
69 | maxImage, err := inputInfo.GetMaxImage()
70 | if err != nil {
71 | return errors.WithStack(err)
72 | }
73 |
74 | imageBuffer := new(bytes.Buffer)
75 | err = png.Encode(imageBuffer, imaging.Resize(maxImage, size, size, imaging.Lanczos))
76 | if err != nil {
77 | return errors.WithStack(err)
78 | }
79 |
80 | imageData = imageBuffer.Bytes()
81 | }
82 |
83 | // each icon type is prefixed with a 4-byte OSType marker and a 4-byte size header (which includes the ostype/size header).
84 | // add the size of the total icon to lengthBytes in big-endian format.
85 | lengthBytes := make([]byte, 4)
86 | binary.BigEndian.PutUint32(lengthBytes, uint32(len(imageData)+8))
87 |
88 | // iterate through every OSType and append the icon to icns
89 | for _, ostype := range sizeToType[size] {
90 | _, err = icns.Write([]byte(ostype))
91 | if err != nil {
92 | return errors.WithStack(err)
93 | }
94 | _, err = icns.Write(lengthBytes)
95 | if err != nil {
96 | return errors.WithStack(err)
97 | }
98 | _, err = icns.Write(imageData)
99 | if err != nil {
100 | return errors.WithStack(err)
101 | }
102 | }
103 | }
104 |
105 | // each ICNS file is prefixed with a 4 byte header and 4 bytes marking the length of the file, MSB first
106 | lengthBytes := make([]byte, 4)
107 | binary.BigEndian.PutUint32(lengthBytes, uint32(icns.Len()+8))
108 |
109 | outFile, err := fsutil.CreateFile(outFilePath)
110 | if err != nil {
111 | return errors.WithStack(err)
112 | }
113 |
114 | _, err = outFile.Write(icnsHeader)
115 | if err != nil {
116 | return fsutil.CloseAndCheckError(err, outFile)
117 | }
118 | _, err = outFile.Write(lengthBytes)
119 | if err != nil {
120 | return fsutil.CloseAndCheckError(err, outFile)
121 | }
122 |
123 | _, err = io.Copy(outFile, icns)
124 | err = fsutil.CloseAndCheckError(err, outFile)
125 | if err != nil {
126 | return errors.WithStack(err)
127 | }
128 |
129 | return nil
130 | }
131 |
132 | func IsIcns(reader *bufio.Reader) (bool, error) {
133 | data, err := reader.Peek(4)
134 | if err != nil {
135 | return false, errors.WithStack(err)
136 | }
137 | return data[0] == 0x69 && data[1] == 0x63 && data[2] == 0x6e && data[3] == 0x73, nil
138 | }
139 |
140 | type SubImage struct {
141 | Offset int
142 | Length int
143 | }
144 |
145 | func ReadIcns(reader *bufio.Reader) (map[string]SubImage, error) {
146 | type IcnsIconEntry struct {
147 | Type [4]byte
148 | Length uint32
149 | }
150 |
151 | _, err := reader.Discard(8)
152 | if err != nil {
153 | return nil, errors.WithStack(err)
154 | }
155 |
156 | typeToImage := make(map[string]SubImage)
157 | offset := 8
158 | for {
159 | icon := IcnsIconEntry{}
160 | err = binary.Read(reader, binary.BigEndian, &icon)
161 | if err == io.EOF {
162 | break
163 | } else if err != nil {
164 | return nil, errors.WithStack(err)
165 | }
166 |
167 | offset += 8
168 | imageDataLength := int(icon.Length) - 8
169 |
170 | osType := string(icon.Type[:])
171 | if osType != "info" && osType != "TOC " && osType != "icnV" && osType != "name" {
172 | typeToImage[osType] = SubImage{
173 | Offset: offset,
174 | Length: imageDataLength,
175 | }
176 | }
177 |
178 | offset += imageDataLength
179 |
180 | _, err = reader.Discard(imageDataLength)
181 | if err != nil {
182 | return nil, errors.WithStack(err)
183 | }
184 | }
185 |
186 | return typeToImage, nil
187 | }
188 |
--------------------------------------------------------------------------------
/pkg/icons/icnsToPngUsingOpenJpeg.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "image"
7 | "io"
8 | "os"
9 | "os/exec"
10 | "path/filepath"
11 | "runtime"
12 | "strings"
13 |
14 | "github.com/develar/app-builder/pkg/linuxTools"
15 | "github.com/develar/app-builder/pkg/log"
16 | "github.com/develar/app-builder/pkg/util"
17 | "github.com/develar/errors"
18 | "github.com/develar/go-fs-util"
19 | "go.uber.org/zap"
20 | )
21 |
22 | var sameSize = map[string]string{
23 | "icp5": "ic11",
24 | "icp6": "ic12",
25 | "ic08": "ic13",
26 | "ic09": "ic14",
27 | }
28 |
29 | var typeToSize = map[string]int{
30 | "icp4": 16,
31 | "icp5": 32,
32 | "icp6": 64,
33 | "ic07": 128,
34 | "ic08": 256,
35 | "ic09": 512,
36 | "ic10": 1024,
37 | "ic11": 32,
38 | "ic12": 64,
39 | "ic13": 256,
40 | "ic14": 512,
41 | }
42 |
43 | func ConvertIcnsToPngUsingOpenJpeg(icnsPath string, outDir string) ([]IconInfo, error) {
44 | reader, err := os.Open(icnsPath)
45 | defer util.Close(reader)
46 |
47 | if err != nil {
48 | return nil, errors.WithStack(err)
49 | }
50 |
51 | bufferedReader := bufio.NewReader(reader)
52 | subImageInfoList, err := ReadIcns(bufferedReader)
53 | if err != nil {
54 | return nil, errors.WithStack(err)
55 | }
56 |
57 | var result []IconInfo
58 | for s1, s2 := range sameSize {
59 | // icns contains retina icons but with the same size
60 | if _, ok := subImageInfoList[s1]; ok {
61 | delete(subImageInfoList, s2)
62 | }
63 | }
64 |
65 | outFileNamePrefix := filepath.Join(outDir, strings.TrimSuffix(filepath.Base(icnsPath), filepath.Ext(icnsPath))) + "_"
66 | for imageType, subImage := range subImageInfoList {
67 | if isIgnoredType(imageType) {
68 | log.Debug("skip unsupported icns sub image format", zap.String("type", imageType), zap.String("file", icnsPath))
69 | continue
70 | }
71 |
72 | imageOffset := int64(subImage.Offset)
73 | _, err = reader.Seek(imageOffset, 0)
74 | if err != nil {
75 | return nil, errors.WithStack(err)
76 | }
77 | bufferedReader.Reset(reader)
78 |
79 | var outFileName string
80 |
81 | config, formatName, err := image.DecodeConfig(bufferedReader)
82 | if err != nil {
83 | outFileName = outFileNamePrefix + imageType + ".jp2"
84 | result = append(result, IconInfo{
85 | File: outFileName,
86 | Size: typeToSize[imageType],
87 | })
88 | } else {
89 | outFileName = outFileNamePrefix + fmt.Sprintf("%d.%s", config.Width, formatName)
90 | result = append(result, IconInfo{
91 | File: outFileName,
92 | Size: config.Width,
93 | })
94 | }
95 |
96 | _, err = reader.Seek(imageOffset, 0)
97 | if err != nil {
98 | return nil, errors.WithStack(err)
99 | }
100 |
101 | outWriter, err := os.Create(outFileName)
102 | if err != nil {
103 | return nil, errors.WithStack(err)
104 | }
105 |
106 | _, err = io.Copy(outWriter, io.LimitReader(reader, int64(subImage.Length)))
107 | err = fsutil.CloseAndCheckError(err, outWriter)
108 | if err != nil {
109 | return nil, errors.WithStack(err)
110 | }
111 | }
112 |
113 | err = util.MapAsync(len(result), func(taskIndex int) (func() error, error) {
114 | imageInfo := &result[taskIndex]
115 | jpeg2File := imageInfo.File
116 | if !strings.HasSuffix(jpeg2File, ".jp2") {
117 | return nil, nil
118 | }
119 |
120 | opjDecompressPath := "opj_decompress"
121 | opjLibPath := ""
122 | if !util.IsEnvTrue("USE_SYSTEM_OPG") && runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
123 | opjDecompressPath, err = linuxTools.GetLinuxTool("opj_decompress")
124 | if err != nil {
125 | return nil, errors.WithStack(err)
126 | }
127 |
128 | opjLibPath = filepath.Join(filepath.Dir(opjDecompressPath), "lib")
129 | }
130 |
131 | pngFile := fmt.Sprintf("%s%d.png", outFileNamePrefix, imageInfo.Size)
132 | imageInfo.File = pngFile
133 |
134 | return func() error {
135 | command := exec.Command(opjDecompressPath, "-quiet", "-i", jpeg2File, "-o", pngFile)
136 | if len(opjLibPath) != 0 {
137 | env := os.Environ()
138 | env = append(env,
139 | fmt.Sprintf("LD_LIBRARY_PATH=%s", opjLibPath+":"+os.Getenv("LD_LIBRARY_PATH")),
140 | )
141 | command.Env = env
142 | }
143 |
144 | _, err = util.Execute(command)
145 | if err != nil {
146 | return err
147 | }
148 |
149 | err = os.Remove(jpeg2File)
150 | if err != nil {
151 | return errors.WithStack(err)
152 | }
153 |
154 | return nil
155 | }, nil
156 | })
157 | if err != nil {
158 | return nil, errors.WithStack(err)
159 | }
160 |
161 | return result, nil
162 | }
163 |
164 | func isIgnoredType(imageType string) bool {
165 | return imageType == "ic04" || imageType == "ic05" ||
166 | strings.HasPrefix(imageType, "icm") || strings.HasPrefix(imageType, "ics") || strings.HasPrefix(imageType, "is") || strings.HasPrefix(imageType, "s") || strings.HasPrefix(imageType, "ich") ||
167 | imageType == "icl4" ||
168 | imageType == "icl8" ||
169 | imageType == "il32" ||
170 | imageType == "l8mk" ||
171 | imageType == "ih32" ||
172 | imageType == "h8mk" ||
173 | imageType == "it32" ||
174 | imageType == "t8mk"
175 | }
176 |
--------------------------------------------------------------------------------
/pkg/icons/ico.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "encoding/binary"
5 | )
6 |
7 | type Sizes struct {
8 | Width int
9 | Height int
10 | }
11 |
12 | func IsIco(data []byte) bool {
13 | return binary.LittleEndian.Uint16(data) == 0 && binary.LittleEndian.Uint16(data[:2]) == 0
14 | }
15 |
16 | func GetIcoSizes(data []byte) []Sizes {
17 | n := int(binary.LittleEndian.Uint16(data[4:]))
18 | var sizes []Sizes
19 | for i := 0; i < n; i++ {
20 | w := int(data[6+i*16] & 0xff)
21 | if w == 0 {
22 | w = 256
23 | }
24 | h := int(data[7+i*16] & 0xff)
25 | if h == 0 {
26 | h = 256
27 | }
28 |
29 | sizes = append(sizes, Sizes{
30 | Width: w,
31 | Height: h,
32 | })
33 | }
34 | return sizes
35 | }
36 |
--------------------------------------------------------------------------------
/pkg/icons/icon-converter_test.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/develar/app-builder/pkg/log"
11 | "github.com/develar/app-builder/pkg/util"
12 | . "github.com/onsi/ginkgo"
13 | . "github.com/onsi/gomega"
14 |
15 | "github.com/biessek/golang-ico"
16 | )
17 |
18 | func TestIcons(t *testing.T) {
19 | log.InitLogger()
20 | RegisterFailHandler(Fail)
21 | RunSpecs(t, "Icons Suite")
22 | }
23 |
24 | func TestCommonSourcesSet(t *testing.T) {
25 | g := NewGomegaWithT(t)
26 |
27 | result := createCommonIconSources([]string{"foo"}, "set")
28 | g.Expect(result).To(Equal([]string{"foo", "foo.png", "foo.icns", "foo.ico", "icons", "icon.png", "icon.icns", "icon.ico"}))
29 | }
30 |
31 | func TestCommonSourcesIcns(t *testing.T) {
32 | result := createCommonIconSources([]string{"foo"}, "icns")
33 | g := NewGomegaWithT(t)
34 | g.Expect(result).To(Equal([]string{"foo.icns", "foo", "foo.png", "icon.icns", "icons", "icon.png"}))
35 | }
36 |
37 | func TestCommonSourcesNil(t *testing.T) {
38 | result := createCommonIconSources(nil, "set")
39 | g := NewGomegaWithT(t)
40 | g.Expect(result).To(Equal([]string{"icons", "icon.png", "icon.icns", "icon.ico"}))
41 | }
42 |
43 | func getTestDataPath() string {
44 | testDataPath, err := filepath.Abs(filepath.Join("..", "..", "testData"))
45 | Expect(err).NotTo(HaveOccurred())
46 | return testDataPath
47 | }
48 |
49 | var _ = Describe("Blockmap", func() {
50 | var tmpDir string
51 |
52 | BeforeEach(func() {
53 | var err error
54 | tmpDir, err = ioutil.TempDir("", "")
55 | Expect(err).NotTo(HaveOccurred())
56 | })
57 |
58 | AfterEach(func() {
59 | err := os.RemoveAll(tmpDir)
60 | Expect(err).NotTo(HaveOccurred())
61 | })
62 |
63 | It("CheckIcoImageSize", func() {
64 | _, err := doConvertIcon([]string{filepath.Join(getTestDataPath(), "icon.ico")}, nil, "ico", tmpDir)
65 | Expect(err).NotTo(HaveOccurred())
66 | })
67 |
68 | It("IcnsToIco", func() {
69 | files, err := doConvertIcon([]string{filepath.Join(getTestDataPath(), "icon.icns")}, nil, "ico", tmpDir)
70 | Expect(err).NotTo(HaveOccurred())
71 | Expect(len(files)).To(Equal(1))
72 | file := files[0].File
73 |
74 | Expect(strings.HasSuffix(file, ".ico")).To(BeTrue())
75 |
76 | data, err := ioutil.ReadFile(file)
77 | Expect(err).NotTo(HaveOccurred())
78 | Expect(GetIcoSizes(data)).To(Equal([]Sizes{
79 | {Width: 256, Height: 256},
80 | }))
81 | })
82 |
83 | It("IcnsToPng", func() {
84 | result, err := ConvertIcnsToPngUsingOpenJpeg(filepath.Join(getTestDataPath(), "icon.icns"), tmpDir)
85 | Expect(err).NotTo(HaveOccurred())
86 | Expect(len(result)).To(Equal(5))
87 | })
88 |
89 | It("ConvertIcnsToPngUsingOpenJpeg", func() {
90 | // todo opj_decompress not installed on Travis
91 | //result, err := ConvertIcnsToPngUsingOpenJpeg(filepath.Join(getTestDataPath(), "icon-jpeg2.icns"), tmpDir)
92 | //Expect(err).NotTo(HaveOccurred())
93 | //Expect(len(result)).To(Equal(2))
94 | })
95 |
96 | It("LargePngTo256Ico", func() {
97 | files, err := doConvertIcon([]string{filepath.Join(getTestDataPath(), "512x512.png")}, nil, "ico", tmpDir)
98 | Expect(err).NotTo(HaveOccurred())
99 | Expect(len(files)).To(Equal(1))
100 | file := files[0].File
101 | Expect(strings.HasSuffix(file, ".ico")).To(BeTrue())
102 |
103 | reader, err := os.Open(file)
104 | Expect(err).NotTo(HaveOccurred())
105 | defer util.Close(reader)
106 | images, err := ico.DecodeAll(reader)
107 | Expect(err).NotTo(HaveOccurred())
108 |
109 | Expect(len(images)).To(Equal(1))
110 |
111 | imageSize := images[0].Bounds().Max
112 | Expect(imageSize.X).To(Equal(256))
113 | Expect(imageSize.Y).To(Equal(256))
114 | })
115 | })
116 |
--------------------------------------------------------------------------------
/pkg/icons/icons-api.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "image"
5 | "sort"
6 |
7 | "github.com/develar/errors"
8 | )
9 |
10 | type IconInfo struct {
11 | File string `json:"file"`
12 | Size int `json:"size"`
13 | }
14 |
15 | func sortBySize(list []IconInfo) {
16 | sort.Slice(list, func(i, j int) bool { return list[i].Size < list[j].Size })
17 | }
18 |
19 | type IconConvertRequest struct {
20 | Sources *[]string
21 | FallbackSources *[]string
22 | Roots *[]string
23 |
24 | OutputFormat string
25 | OutputDir string
26 | }
27 |
28 | type IconConvertResult struct {
29 | Icons []IconInfo `json:"icons"`
30 | IsFallback bool `json:"isFallback"`
31 | }
32 |
33 | type MisConfigurationError struct {
34 | Message string `json:"error"`
35 | Code string `json:"errorCode"`
36 | }
37 |
38 | type InputFileInfo struct {
39 | MaxIconSize int
40 | MaxIconPath string
41 | SizeToPath map[int]string
42 |
43 | maxImage image.Image
44 |
45 | recommendedMinSize int
46 | }
47 |
48 | func (t *InputFileInfo) GetMaxImage() (image.Image, error) {
49 | if t.maxImage == nil {
50 | var err error
51 | t.maxImage, err = loadImage(t.MaxIconPath, t.recommendedMinSize)
52 | if err != nil {
53 | return nil, errors.WithStack(err)
54 | }
55 | }
56 | return t.maxImage, nil
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/icons/image-util.go:
--------------------------------------------------------------------------------
1 | package icons
2 |
3 | import (
4 | "bufio"
5 | "image"
6 | "image/png"
7 | "io"
8 | "os"
9 |
10 | "github.com/biessek/golang-ico"
11 | "github.com/develar/app-builder/pkg/util"
12 | "github.com/develar/errors"
13 | "github.com/develar/go-fs-util"
14 | )
15 |
16 | const (
17 | PNG = 0
18 | ICO = 1
19 | )
20 |
21 | // sorted by suitability
22 | var icnsTypesForIco = []string{ICNS_256, ICNS_256_RETINA, ICNS_512, ICNS_512_RETINA, ICNS_1024}
23 |
24 | func LoadImage(file string) (image.Image, error) {
25 | reader, err := os.Open(file)
26 | if err != nil {
27 | return nil, errors.WithStack(err)
28 | }
29 |
30 | defer util.Close(reader)
31 |
32 | bufferedReader := bufio.NewReader(reader)
33 |
34 | isIcns, err := IsIcns(bufferedReader)
35 | if err != nil {
36 | return nil, errors.WithStack(err)
37 | }
38 |
39 | if isIcns {
40 | subImageInfoList, err := ReadIcns(bufferedReader)
41 | if err != nil {
42 | return nil, errors.WithStack(err)
43 | }
44 |
45 | for _, osType := range icnsTypesForIco {
46 | subImage, ok := subImageInfoList[osType]
47 | if ok {
48 | _, err = reader.Seek(int64(subImage.Offset), 0)
49 | if err != nil {
50 | return nil, errors.WithStack(err)
51 | }
52 | bufferedReader.Reset(reader)
53 | // golang doesn't support JPEG2000
54 | return DecodeImageAndClose(bufferedReader, reader)
55 | }
56 | }
57 |
58 | return nil, NewImageSizeError(file, 256)
59 | }
60 |
61 | return DecodeImageAndClose(bufferedReader, reader)
62 | }
63 |
64 | func DecodeImageConfig(file string) (*image.Config, error) {
65 | reader, err := os.Open(file)
66 | if err != nil {
67 | return nil, errors.WithStack(err)
68 | }
69 |
70 | result, _, err := image.DecodeConfig(reader)
71 | if err != nil {
72 | util.Close(reader)
73 |
74 | if err == image.ErrFormat {
75 | err = &ImageFormatError{file, "ERR_ICON_UNKNOWN_FORMAT"}
76 | }
77 | return nil, errors.WithStack(err)
78 | }
79 |
80 | err = reader.Close()
81 | if err != nil {
82 | return nil, errors.WithStack(err)
83 | }
84 |
85 | return &result, nil
86 | }
87 |
88 | func DecodeImageAndClose(reader io.Reader, closer io.Closer) (image.Image, error) {
89 | result, _, err := image.Decode(reader)
90 | return result, errors.WithStack(fsutil.CloseAndCheckError(err, closer))
91 | }
92 |
93 | func SaveImage(image image.Image, outFileName string, format int) error {
94 | outFile, err := fsutil.CreateFile(outFileName)
95 | if err != nil {
96 | return err
97 | }
98 |
99 | return SaveImage2(image, outFile, format)
100 | }
101 |
102 | func SaveImage2(image image.Image, outFile io.WriteCloser, format int) error {
103 | writer := bufio.NewWriter(outFile)
104 |
105 | var err error
106 | if format == PNG {
107 | err = png.Encode(writer, image)
108 | } else {
109 | err = ico.Encode(writer, image)
110 | }
111 |
112 | if err != nil {
113 | return fsutil.CloseAndCheckError(err, outFile)
114 | }
115 | return fsutil.CloseAndCheckError(writer.Flush(), outFile)
116 | }
117 |
--------------------------------------------------------------------------------
/pkg/linuxTools/tool.go:
--------------------------------------------------------------------------------
1 | package linuxTools
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 | "runtime"
7 |
8 | "github.com/develar/app-builder/pkg/download"
9 | "github.com/develar/app-builder/pkg/util"
10 | fsutil "github.com/develar/go-fs-util"
11 | )
12 |
13 | func GetAppImageToolDir() (string, error) {
14 | dirName := "appimage-12.0.1"
15 | //noinspection SpellCheckingInspection
16 | result, err := download.DownloadArtifact("",
17 | download.GetGithubBaseUrl()+dirName+"/"+dirName+".7z",
18 | "3el6RUh6XoYJCI/ZOApyb0LLU/gSxDntVZ46R6+JNEANzfSo7/TfrzCRp5KlDo35c24r3ZOP7nnw4RqHwkMRLw==")
19 | if err != nil {
20 | return "", err
21 | }
22 | return result, nil
23 | }
24 |
25 | func GetAppImageToolBin(toolDir string) string {
26 | if util.GetCurrentOs() == util.MAC {
27 | return filepath.Join(toolDir, "darwin")
28 | } else {
29 | return filepath.Join(toolDir, "linux-"+goArchToArchSuffix())
30 | }
31 | }
32 |
33 | func GetLinuxTool(name string) (string, error) {
34 | toolDir, err := GetAppImageToolDir()
35 | if err != nil {
36 | return "", err
37 | }
38 | return filepath.Join(GetAppImageToolBin(toolDir), name), nil
39 | }
40 |
41 | func GetMksquashfs() (string, error) {
42 | result := "mksquashfs"
43 | if !util.IsEnvTrue("USE_SYSTEM_MKSQUASHFS") {
44 | result = os.Getenv("MKSQUASHFS_PATH")
45 | if len(result) == 0 {
46 | var err error
47 | result, err = GetLinuxTool("mksquashfs")
48 | if err != nil {
49 | return "", err
50 | }
51 | }
52 | }
53 |
54 | return result, nil
55 | }
56 |
57 | func goArchToArchSuffix() string {
58 | arch := runtime.GOARCH
59 | switch arch {
60 | case "amd64":
61 | return "x64"
62 | case "386":
63 | return "ia32"
64 | case "arm":
65 | return "arm32"
66 | default:
67 | return arch
68 | }
69 | }
70 |
71 | func ReadDirContentTo(dir string, paths []string, filter func(string) bool) ([]string, error) {
72 | content, err := fsutil.ReadDirContent(dir)
73 | if err != nil {
74 | return nil, err
75 | }
76 |
77 | for _, value := range content {
78 | if filter == nil || filter(value) {
79 | paths = append(paths, filepath.Join(dir, value))
80 | }
81 | }
82 | return paths, nil
83 | }
84 |
--------------------------------------------------------------------------------
/pkg/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "io"
5 | "os"
6 |
7 | zap_cli_encoder "github.com/develar/app-builder/pkg/zap-cli-encoder"
8 | "github.com/mattn/go-colorable"
9 | "github.com/mattn/go-isatty"
10 | "go.uber.org/zap"
11 | "go.uber.org/zap/zapcore"
12 | )
13 |
14 | var LOG *zap.Logger
15 |
16 | func InitLogger() {
17 | encoderConfig := zapcore.EncoderConfig{
18 | TimeKey: "T",
19 | LevelKey: "L",
20 | NameKey: "N",
21 | CallerKey: "C",
22 | MessageKey: "M",
23 | StacktraceKey: "S",
24 | LineEnding: zapcore.DefaultLineEnding,
25 | EncodeDuration: zapcore.StringDurationEncoder,
26 | }
27 |
28 | level := zapcore.InfoLevel
29 | debugEnv, isDebugDefined := os.LookupEnv("DEBUG")
30 | if isDebugDefined && debugEnv != "false" {
31 | level = zapcore.DebugLevel
32 | }
33 |
34 | colored := isColored()
35 | var writer io.Writer
36 | if colored {
37 | writer = colorable.NewColorableStderr()
38 | } else {
39 | writer = os.Stderr
40 | }
41 | LOG = zap.New(zapcore.NewCore(
42 | zap_cli_encoder.NewConsoleEncoder(encoderConfig, colored),
43 | zapcore.AddSync(writer),
44 | level,
45 | ))
46 | }
47 |
48 | func isColored() bool {
49 | forceColor, ok := os.LookupEnv("FORCE_COLOR")
50 | if ok && (forceColor == "1" || forceColor == "true" || forceColor == "") {
51 | return true
52 | }
53 |
54 | if forceColor == "0" || forceColor == "false" || os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) {
55 | return false
56 | }
57 | return true
58 | }
59 |
60 | func Warn(msg string, fields ...zapcore.Field) {
61 | LOG.Warn(msg, fields...)
62 | }
63 |
64 | func Error(msg string, fields ...zapcore.Field) {
65 | LOG.Error(msg, fields...)
66 | }
67 |
68 | func Info(msg string, fields ...zapcore.Field) {
69 | LOG.Info(msg, fields...)
70 | }
71 |
72 | func Debug(msg string, fields ...zapcore.Field) {
73 | LOG.Debug(msg, fields...)
74 | }
75 |
76 | func IsDebugEnabled() bool {
77 | if LOG == nil {
78 | return false
79 | }
80 | return LOG.Core().Enabled(zap.DebugLevel)
81 | }
82 |
--------------------------------------------------------------------------------
/pkg/node-modules/cli_test.go:
--------------------------------------------------------------------------------
1 | package node_modules
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os/exec"
7 | "path"
8 | "testing"
9 |
10 | "github.com/develar/app-builder/pkg/fs"
11 | . "github.com/onsi/gomega"
12 | "github.com/samber/lo"
13 | )
14 |
15 | type NodeTreeDepItem struct {
16 | Name string `json:"name"`
17 | Version string `json:"version"`
18 | }
19 |
20 | type NodeTreeItem struct {
21 | Dir string `json:"dir"`
22 | Deps []NodeTreeDepItem `json:"deps"`
23 | }
24 |
25 | type NodePathItem struct {
26 | Name string `json:"name"`
27 | Version string `json:"version"`
28 | Dir string `json:"dir"`
29 | }
30 |
31 | func nodeDepPath(t *testing.T, dir string) {
32 | g := NewGomegaWithT(t)
33 | rootPath := fs.FindParentWithFile(Dirname(), "go.mod")
34 | cmd := exec.Command("go", "run", path.Join(rootPath, "main.go"), "node-dep-tree", "--flatten", "--dir", dir)
35 | output, err := cmd.Output()
36 | if err != nil {
37 | fmt.Println("err", err)
38 | }
39 | g.Expect(err).NotTo(HaveOccurred())
40 | var j []NodePathItem
41 | json.Unmarshal(output, &j)
42 | dependencies := make([]NodePathItem, 4)
43 | names := make([]string, 4)
44 | index := 0
45 | for _, d := range j {
46 | dependencies[index] = d
47 | names[index] = d.Name
48 | index++
49 | }
50 | g.Expect(names).To(Equal([]string{
51 | "js-tokens", "loose-envify", "react", "remote",
52 | }))
53 | }
54 |
55 | func nodeDepTree(t *testing.T, dir string) {
56 | g := NewGomegaWithT(t)
57 | rootPath := fs.FindParentWithFile(Dirname(), "go.mod")
58 | cmd := exec.Command("go", "run", path.Join(rootPath, "main.go"), "node-dep-tree", "--dir", dir)
59 | output, err := cmd.Output()
60 | if err != nil {
61 | fmt.Println("err", err)
62 | }
63 | g.Expect(err).NotTo(HaveOccurred())
64 | var j []NodeTreeItem
65 | json.Unmarshal(output, &j)
66 | r := lo.FlatMap(j, func(it NodeTreeItem, i int) []string {
67 | return lo.Map(it.Deps, func(it NodeTreeDepItem, i int) string {
68 | return it.Name
69 | })
70 | })
71 | g.Expect(r).To(ConsistOf([]string{
72 | "react", "remote", "js-tokens", "loose-envify",
73 | }))
74 | }
75 |
76 | func TestNodeDepTreeCmd(t *testing.T) {
77 | nodeDepTree(t, path.Join(Dirname(), "npm-demo"))
78 | nodeDepTree(t, path.Join(Dirname(), "pnpm-demo"))
79 | }
80 |
81 | func TestNodeDepPathCmd(t *testing.T) {
82 | nodeDepPath(t, path.Join(Dirname(), "npm-demo"))
83 | nodeDepPath(t, path.Join(Dirname(), "pnpm-demo"))
84 | }
85 |
--------------------------------------------------------------------------------
/pkg/node-modules/es5-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pnpm-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "es5-ext": "0.10.53"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/node-modules/es5-demo/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | dependencies:
11 | es5-ext:
12 | specifier: 0.10.53
13 | version: 0.10.53
14 |
15 | packages:
16 |
17 | d@1.0.2:
18 | resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==}
19 | engines: {node: '>=0.12'}
20 |
21 | es5-ext@0.10.53:
22 | resolution: {integrity: sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==}
23 |
24 | es5-ext@0.10.64:
25 | resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==}
26 | engines: {node: '>=0.10'}
27 |
28 | es6-iterator@2.0.3:
29 | resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==}
30 |
31 | es6-symbol@3.1.4:
32 | resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==}
33 | engines: {node: '>=0.12'}
34 |
35 | esniff@2.0.1:
36 | resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==}
37 | engines: {node: '>=0.10'}
38 |
39 | event-emitter@0.3.5:
40 | resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==}
41 |
42 | ext@1.7.0:
43 | resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==}
44 |
45 | next-tick@1.0.0:
46 | resolution: {integrity: sha512-mc/caHeUcdjnC/boPWJefDr4KUIWQNv+tlnFnJd38QMou86QtxQzBJfxgGRzvx8jazYRqrVlaHarfO72uNxPOg==}
47 |
48 | next-tick@1.1.0:
49 | resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==}
50 |
51 | type@2.7.3:
52 | resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
53 |
54 | snapshots:
55 |
56 | d@1.0.2:
57 | dependencies:
58 | es5-ext: 0.10.64
59 | type: 2.7.3
60 |
61 | es5-ext@0.10.53:
62 | dependencies:
63 | es6-iterator: 2.0.3
64 | es6-symbol: 3.1.4
65 | next-tick: 1.0.0
66 |
67 | es5-ext@0.10.64:
68 | dependencies:
69 | es6-iterator: 2.0.3
70 | es6-symbol: 3.1.4
71 | esniff: 2.0.1
72 | next-tick: 1.1.0
73 |
74 | es6-iterator@2.0.3:
75 | dependencies:
76 | d: 1.0.2
77 | es5-ext: 0.10.53
78 | es6-symbol: 3.1.4
79 |
80 | es6-symbol@3.1.4:
81 | dependencies:
82 | d: 1.0.2
83 | ext: 1.7.0
84 |
85 | esniff@2.0.1:
86 | dependencies:
87 | d: 1.0.2
88 | es5-ext: 0.10.64
89 | event-emitter: 0.3.5
90 | type: 2.7.3
91 |
92 | event-emitter@0.3.5:
93 | dependencies:
94 | d: 1.0.2
95 | es5-ext: 0.10.53
96 |
97 | ext@1.7.0:
98 | dependencies:
99 | type: 2.7.3
100 |
101 | next-tick@1.0.0: {}
102 |
103 | next-tick@1.1.0: {}
104 |
105 | type@2.7.3: {}
106 |
--------------------------------------------------------------------------------
/pkg/node-modules/helper_test.go:
--------------------------------------------------------------------------------
1 | package node_modules
2 |
3 | import (
4 | "path"
5 | "runtime"
6 | )
7 |
8 | func Dirname() string {
9 | _, filename, _, _ := runtime.Caller(1)
10 | return path.Dir(filename)
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/node-modules/npm-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "npm-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "remote": "npm:@electron/remote@2.1.2"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/pkg/node-modules/parse-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "parse-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "parse-asn1":"5.1.7"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/node-modules/pnpm-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pnpm-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "react": "18.2.0",
14 | "remote": "npm:@electron/remote@2.1.2"
15 | },
16 | "resolutions": {
17 | "electron": "31.0.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/node-modules/tar-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tar-demo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "electron-builder": "25.1.8"
14 | },
15 |
16 | "dependencies": {
17 | "tar": "7.4.3",
18 | "archiver":"7.0.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/node-modules/yarn-demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "workspaces": [
4 | "packages/*"
5 | ]
6 | }
--------------------------------------------------------------------------------
/pkg/node-modules/yarn-demo/packages/foo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "foo",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "ms": "2.0.0"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/pkg/node-modules/yarn-demo/packages/test-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hello World!
6 |
12 |
13 |
14 | Hello World!
15 | We are using node ,
16 | Chrome ,
17 | and Electron .
18 |
19 | Args: .
20 |
21 | Env: .
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/pkg/node-modules/yarn-demo/packages/test-app/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { app, ipcMain, BrowserWindow, Menu, Tray } = require("electron")
4 | const fs = require("fs")
5 | const path = require("path")
6 |
7 | // Module to control application life.
8 | // Module to create native browser window.
9 |
10 | // Keep a global reference of the window object, if you don't, the window will
11 | // be closed automatically when the JavaScript object is garbage collected.
12 | let mainWindow;
13 |
14 | let tray = null
15 |
16 | function createWindow () {
17 | if (process.platform === "linux" && process.env.APPDIR != null) {
18 | tray = new Tray(path.join(process.env.APPDIR, "testapp.png"))
19 | const contextMenu = Menu.buildFromTemplate([
20 | {label: 'Item1', type: 'radio'},
21 | {label: 'Item2', type: 'radio'},
22 | {label: 'Item3', type: 'radio', checked: true},
23 | {label: 'Item4', type: 'radio'}
24 | ])
25 | tray.setToolTip('This is my application.')
26 | tray.setContextMenu(contextMenu)
27 | }
28 |
29 | // Create the browser window.
30 | mainWindow = new BrowserWindow({width: 800, height: 600});
31 |
32 | // and load the index.html of the app.
33 | mainWindow.loadURL('file://' + __dirname + '/index.html');
34 |
35 | // Open the DevTools.
36 | mainWindow.webContents.openDevTools();
37 |
38 | mainWindow.webContents.executeJavaScript(`console.log("appData: ${app.getPath("appData").replace(/\\/g, "\\\\")}")`)
39 | mainWindow.webContents.executeJavaScript(`console.log("userData: ${app.getPath("userData").replace(/\\/g, "\\\\")}")`)
40 |
41 | // Emitted when the window is closed.
42 | mainWindow.on('closed', function() {
43 | // Dereference the window object, usually you would store windows
44 | // in an array if your app supports multi windows, this is the time
45 | // when you should delete the corresponding element.
46 | mainWindow = null;
47 | });
48 | }
49 |
50 | // This method will be called when Electron has finished
51 | // initialization and is ready to create browser windows.
52 | app.on('ready', createWindow);
53 |
54 | // Quit when all windows are closed.
55 | app.on('window-all-closed', function () {
56 | // On MacOS it is common for applications and their menu bar
57 | // to stay active until the user quits explicitly with Cmd + Q
58 | if (process.platform !== 'darwin') {
59 | app.quit();
60 | }
61 | });
62 |
63 | app.on("activate", function () {
64 | if (mainWindow === null) {
65 | createWindow()
66 | }
67 | })
68 |
69 | ipcMain.on("saveAppData", () => {
70 | try {
71 | // electron doesn't escape / in the product name
72 | fs.writeFileSync(path.join(app.getPath("appData"), "Test App ßW", "testFile"), "test")
73 | }
74 | catch (e) {
75 | mainWindow.webContents.executeJavaScript(`console.log(\`userData: ${e}\`)`)
76 | }
77 | })
--------------------------------------------------------------------------------
/pkg/node-modules/yarn-demo/packages/test-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-app",
3 | "productName": "Test App ßW",
4 | "version": "1.1.0",
5 | "homepage": "http://foo.example.com",
6 | "description": "Test Application (test quite \" #378)",
7 | "author": "Foo Bar ",
8 | "license": "MIT",
9 | "build": {
10 | "electronVersion": "23.3.10",
11 | "appId": "org.electron-builder.testApp",
12 | "compression": "store",
13 | "npmRebuild": false,
14 | "mac": {
15 | "category": "your.app.category.type"
16 | },
17 | "linux": {
18 | "category": "Development"
19 | }
20 | },
21 | "dependencies": {
22 | "foo": "1.0.0",
23 | "ms": "2.1.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/node-modules/yarn-demo/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | ms@2.0.0:
6 | version "2.0.0"
7 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
8 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
9 |
10 | ms@2.1.1:
11 | version "2.1.1"
12 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
13 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
14 |
--------------------------------------------------------------------------------
/pkg/package-format/appimage/appImage.go:
--------------------------------------------------------------------------------
1 | package appimage
2 |
3 | import (
4 | "io/ioutil"
5 | "os"
6 | "os/exec"
7 | "path/filepath"
8 | "strconv"
9 | "syscall"
10 |
11 | "github.com/alecthomas/kingpin"
12 | "github.com/develar/app-builder/pkg/blockmap"
13 | "github.com/develar/app-builder/pkg/fs"
14 | "github.com/develar/app-builder/pkg/linuxTools"
15 | "github.com/develar/app-builder/pkg/util"
16 | "github.com/develar/errors"
17 | fsutil "github.com/develar/go-fs-util"
18 | )
19 |
20 | type AppImageOptions struct {
21 | appDir *string
22 | stageDir *string
23 | arch *string
24 | output *string
25 |
26 | template *string
27 | license *string
28 | configuration *AppImageConfiguration
29 |
30 | compression *string
31 | }
32 |
33 | func ConfigureCommand(app *kingpin.Application) {
34 | command := app.Command("appimage", "Build AppImage.")
35 |
36 | //noinspection SpellCheckingInspection
37 | options := &AppImageOptions{
38 | appDir: command.Flag("app", "The app dir.").Short('a').Required().String(),
39 | stageDir: command.Flag("stage", "The stage dir.").Short('s').Required().String(),
40 | output: command.Flag("output", "The output file.").Short('o').Required().String(),
41 | arch: command.Flag("arch", "The arch.").Default("x64").Enum("x64", "ia32", "armv7l", "arm64", "riscv64", "loong64"),
42 |
43 | template: command.Flag("template", "The template file.").String(),
44 | license: command.Flag("license", "The license file.").String(),
45 |
46 | compression: command.Flag("compression", "The compression.").Enum("xz", "lzo", "zstd"),
47 | }
48 |
49 | configuration := command.Flag("configuration", "").Required().String()
50 |
51 | isRemoveStage := util.ConfigureIsRemoveStageParam(command)
52 |
53 | command.Action(func(context *kingpin.ParseContext) error {
54 | err := util.DecodeBase64IfNeeded(*configuration, &options.configuration)
55 | if err != nil {
56 | return err
57 | }
58 |
59 | err = AppImage(options)
60 | if err != nil {
61 | return err
62 | }
63 |
64 | if *isRemoveStage {
65 | err = os.RemoveAll(*options.stageDir)
66 | if err != nil {
67 | return errors.WithStack(err)
68 | }
69 | }
70 |
71 | return nil
72 | })
73 | }
74 |
75 | func AppImage(options *AppImageOptions) error {
76 | stageDir := *options.stageDir
77 |
78 | err := writeAppLauncherAndRelatedFiles(options)
79 | if err != nil {
80 | return err
81 | }
82 |
83 | outputFile := *options.output
84 | err = syscall.Unlink(outputFile)
85 | if err != nil && !os.IsNotExist(err) {
86 | return errors.WithStack(err)
87 | }
88 |
89 | appImageToolDir, err := linuxTools.GetAppImageToolDir()
90 | if err != nil {
91 | return err
92 | }
93 |
94 | arch := *options.arch
95 | if arch == "x64" || arch == "ia32" {
96 | err = fs.CopyUsingHardlink(filepath.Join(appImageToolDir, "lib", arch), filepath.Join(stageDir, "usr", "lib"))
97 | if err != nil {
98 | return err
99 | }
100 | }
101 |
102 | // mksquashfs doesn't support merging, our stage contains resources dir and mksquashfs will use resources_1 name for app resources dir
103 | err = fs.CopyUsingHardlink(*options.appDir, stageDir)
104 | if err != nil {
105 | return err
106 | }
107 |
108 | runtimeData, err := ioutil.ReadFile(filepath.Join(appImageToolDir, "runtime-"+arch))
109 | if err != nil {
110 | return errors.WithStack(err)
111 | }
112 |
113 | err = createSquashFs(options, len(runtimeData))
114 | if err != nil {
115 | return err
116 | }
117 |
118 | err = writeRuntimeData(outputFile, runtimeData)
119 | if err != nil {
120 | return err
121 | }
122 |
123 | err = os.Chmod(outputFile, 0755)
124 | if err != nil {
125 | return errors.WithStack(err)
126 | }
127 |
128 | updateInfo, err := blockmap.BuildBlockMap(outputFile, blockmap.DefaultChunkerConfiguration, blockmap.DEFLATE, "")
129 | if err != nil {
130 | return err
131 | }
132 |
133 | err = util.WriteJsonToStdOut(updateInfo)
134 | if err != nil {
135 | return err
136 | }
137 |
138 | return nil
139 | }
140 |
141 | func writeRuntimeData(filePath string, runtimeData []byte) error {
142 | file, err := os.OpenFile(filePath, os.O_RDWR, 0755)
143 | if err != nil {
144 | return errors.WithStack(err)
145 | }
146 |
147 | _, err = file.WriteAt(runtimeData, 0)
148 | return fsutil.CloseAndCheckError(err, file)
149 | }
150 |
151 | func createSquashFs(options *AppImageOptions, offset int) error {
152 | mksquashfsPath, err := linuxTools.GetMksquashfs()
153 | if err != nil {
154 | return err
155 | }
156 |
157 | var args []string
158 | args = append(args, *options.stageDir, *options.output, "-offset", strconv.Itoa(offset), "-all-root", "-noappend", "-no-progress", "-quiet", "-no-xattrs", "-no-fragments")
159 | // "-mkfs-fixed-time", "0" not available for mac yet (since AppImage developers don't provide actual version of mksquashfs for macOS and no official mksquashfs build for macOS)
160 | if *options.compression != "" {
161 | // default gzip compression - 51.9, xz - 50.4 difference is negligible, start time - well, it seems, a little bit longer (but on Parallels VM on external SSD disk)
162 | // so, to be decided later, is it worth to use xz by default
163 | args = append(args, "-comp", *options.compression)
164 | if *options.compression == "xz" {
165 | //noinspection SpellCheckingInspection
166 | args = append(args, "-Xdict-size", "100%", "-b", "1048576")
167 | }
168 | }
169 |
170 | command := exec.Command(mksquashfsPath, args...)
171 | command.Dir = *options.stageDir
172 | _, err = util.Execute(command)
173 | if err != nil {
174 | return err
175 | }
176 |
177 | return nil
178 | }
179 |
--------------------------------------------------------------------------------
/pkg/package-format/appimage/configuration.go:
--------------------------------------------------------------------------------
1 | package appimage
2 |
3 | type AppImageConfiguration struct {
4 | ProductName string `json:"productName"`
5 | ProductFilename string `json:"productFilename"`
6 | ExecutableName string `json:"executableName"`
7 | SystemIntegration string `json:"systemIntegration"`
8 |
9 | DesktopEntry string `json:"desktopEntry"`
10 |
11 | Icons []IconInfo `json:"icons"`
12 | FileAssociations []FileAssociation `json:"fileAssociations"`
13 | }
14 |
15 | type IconInfo struct {
16 | File string `json:"file"`
17 | Size int `json:"size"`
18 | }
19 |
20 | type FileAssociation struct {
21 | Ext string `json:"ext"`
22 | MimeType string `json:"mimeType"`
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/package-format/appimage/templates/AppRun.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | if [ ! -z "$DEBUG" ] ; then
5 | env
6 | set -x
7 | fi
8 |
9 | THIS="$0"
10 | # http://stackoverflow.com/questions/3190818/
11 | args=("$@")
12 | NUMBER_OF_ARGS="$#"
13 |
14 | if [ -z "$APPDIR" ] ; then
15 | # Find the AppDir. It is the directory that contains AppRun.
16 | # This assumes that this script resides inside the AppDir or a subdirectory.
17 | # If this script is run inside an AppImage, then the AppImage runtime likely has already set $APPDIR
18 | path="$(dirname "$(readlink -f "${THIS}")")"
19 | while [[ "$path" != "" && ! -e "$path/$1" ]]; do
20 | path=${path%/*}
21 | done
22 | APPDIR="$path"
23 | fi
24 |
25 | export PATH="${APPDIR}:${APPDIR}/usr/sbin:${PATH}"
26 | export XDG_DATA_DIRS="./share/:/usr/share/gnome:/usr/local/share/:/usr/share/:${XDG_DATA_DIRS}"
27 | export LD_LIBRARY_PATH="${APPDIR}/usr/lib:${LD_LIBRARY_PATH}"
28 | export XDG_DATA_DIRS="${APPDIR}"/usr/share/:"${XDG_DATA_DIRS}":/usr/share/gnome/:/usr/local/share/:/usr/share/
29 | export GSETTINGS_SCHEMA_DIR="${APPDIR}/usr/share/glib-2.0/schemas:${GSETTINGS_SCHEMA_DIR}"
30 |
31 | BIN="$APPDIR/{{.ExecutableName}}"
32 |
33 | if [ -z "$APPIMAGE_EXIT_AFTER_INSTALL" ] ; then
34 | trap atexit EXIT
35 | fi
36 |
37 | isEulaAccepted=1
38 |
39 | atexit()
40 | {
41 | if [ $isEulaAccepted == 1 ] ; then
42 | if [ $NUMBER_OF_ARGS -eq 0 ] ; then
43 | exec "$BIN"
44 | else
45 | exec "$BIN" "${args[@]}"
46 | fi
47 | fi
48 | }
49 |
50 | error()
51 | {
52 | if [ -x /usr/bin/zenity ] ; then
53 | LD_LIBRARY_PATH="" zenity --error --text "${1}" 2>/dev/null
54 | elif [ -x /usr/bin/kdialog ] ; then
55 | LD_LIBRARY_PATH="" kdialog --msgbox "${1}" 2>/dev/null
56 | elif [ -x /usr/bin/Xdialog ] ; then
57 | LD_LIBRARY_PATH="" Xdialog --msgbox "${1}" 2>/dev/null
58 | else
59 | echo "${1}"
60 | fi
61 | exit 1
62 | }
63 |
64 | yesno()
65 | {
66 | TITLE=$1
67 | TEXT=$2
68 | if [ -x /usr/bin/zenity ] ; then
69 | LD_LIBRARY_PATH="" zenity --question --title="$TITLE" --text="$TEXT" 2>/dev/null || exit 0
70 | elif [ -x /usr/bin/kdialog ] ; then
71 | LD_LIBRARY_PATH="" kdialog --title "$TITLE" --yesno "$TEXT" || exit 0
72 | elif [ -x /usr/bin/Xdialog ] ; then
73 | LD_LIBRARY_PATH="" Xdialog --title "$TITLE" --clear --yesno "$TEXT" 10 80 || exit 0
74 | else
75 | echo "zenity, kdialog, Xdialog missing. Skipping ${THIS}."
76 | exit 0
77 | fi
78 | }
79 |
80 | check_dep()
81 | {
82 | DEP=$1
83 | if [ -z $(which "$DEP") ] ; then
84 | echo "$DEP is missing. Skipping ${THIS}."
85 | exit 0
86 | fi
87 | }
88 |
89 | if [ -z "$APPIMAGE" ] ; then
90 | APPIMAGE="$APPDIR/AppRun"
91 | # not running from within an AppImage; hence using the AppRun for Exec=
92 | fi
93 |
94 | {{if .EulaFile}}
95 | if [ -z "$APPIMAGE_SILENT_INSTALL" ] ; then
96 | EULA_MARK_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/{{.ProductFilename}}"
97 | EULA_MARK_FILE="$EULA_MARK_DIR/eulaAccepted"
98 | # show EULA only if desktop file doesn't exist
99 | if [ ! -e "$EULA_MARK_FILE" ] ; then
100 | if [ -x /usr/bin/zenity ] ; then
101 | # on cancel simply exits and our trap handler launches app, so, $isEulaAccepted is set here to 0 and then to 1 if EULA accepted
102 | isEulaAccepted=0
103 | LD_LIBRARY_PATH="" zenity --text-info --title="{{.ProductName}}" --filename="$APPDIR/{{.EulaFile}}" --ok-label=Agree --cancel-label=Disagree {{if .IsHtmlEula}}--html{{end}}
104 | echo "r: $?"
105 | elif [ -x /usr/bin/kdialog ] ; then
106 | # cannot find any option to force Agree/Disagree buttons for kdialog. And official example exactly with OK button https://techbase.kde.org/Development/Tutorials/Shell_Scripting_with_KDE_Dialogs#Example_21._--textbox_dialog_box
107 | # in any case we pass labels text
108 | LD_LIBRARY_PATH="" kdialog --textbox "$APPDIR/{{.EulaFile}}" --yes-label Agree --cancel-label "Disagree"
109 | fi
110 |
111 | case $? in
112 | 0)
113 | isEulaAccepted=1
114 | echo "License accepted"
115 | mkdir -p "$EULA_MARK_DIR"
116 | touch "$EULA_MARK_FILE"
117 | ;;
118 | 1)
119 | echo "License not accepted"
120 | exit 0
121 | ;;
122 | -1)
123 | echo "An unexpected error has occurred."
124 | isEulaAccepted=1
125 | ;;
126 | esac
127 | fi
128 | fi
129 | {{end}}
--------------------------------------------------------------------------------
/pkg/package-format/dmg/dmg-win.go:
--------------------------------------------------------------------------------
1 | // +build windows
2 |
3 | package dmg
4 |
5 | import "github.com/alecthomas/kingpin"
6 |
7 | func ConfigureCommand(app *kingpin.Application) {
8 | }
--------------------------------------------------------------------------------
/pkg/package-format/dmg/dmg.go:
--------------------------------------------------------------------------------
1 | // +build !windows
2 |
3 | package dmg
4 |
5 | import (
6 | "bytes"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "regexp"
11 | "strconv"
12 | "strings"
13 |
14 | "github.com/alecthomas/kingpin"
15 | "github.com/develar/app-builder/pkg/fs"
16 | "github.com/develar/app-builder/pkg/log"
17 | "github.com/develar/app-builder/pkg/util"
18 | "github.com/develar/errors"
19 | "github.com/json-iterator/go"
20 | "github.com/pkg/xattr"
21 | "go.uber.org/zap"
22 | )
23 |
24 | func ConfigureCommand(app *kingpin.Application) {
25 | command := app.Command("dmg", "Build dmg.")
26 |
27 | volumePath := command.Flag("volume", "").Required().String()
28 | icon := command.Flag("icon", "").String()
29 | background := command.Flag("background", "").String()
30 |
31 | command.Action(func(context *kingpin.ParseContext) error {
32 | backgroundFileInImage, err := BuildDmg(*volumePath, *icon, *background)
33 | if err != nil {
34 | return err
35 | }
36 |
37 | jsonWriter := jsoniter.NewStream(jsoniter.ConfigFastest, os.Stdout, 32*1024)
38 | jsonWriter.WriteObjectStart()
39 |
40 | if *background != "" {
41 | pixelWidth, pixelHeight, err := getImageSizeUsingSips(*background)
42 | if err != nil {
43 | return err
44 | }
45 |
46 | jsonWriter.WriteObjectField("backgroundWidth")
47 | jsonWriter.WriteInt(pixelWidth)
48 | jsonWriter.WriteMore()
49 | jsonWriter.WriteObjectField("backgroundHeight")
50 | jsonWriter.WriteInt(pixelHeight)
51 |
52 | jsonWriter.WriteMore()
53 | jsonWriter.WriteObjectField("backgroundFile")
54 | jsonWriter.WriteString(backgroundFileInImage)
55 | }
56 |
57 | jsonWriter.WriteObjectEnd()
58 | err = jsonWriter.Flush()
59 | if err != nil {
60 | return err
61 | }
62 |
63 | return nil
64 | })
65 | }
66 |
67 | func getImageSizeUsingSips(background string) (int, int, error) {
68 | command := exec.Command("sips", "-g", "pixelHeight", "-g", "pixelWidth", background)
69 | result, err := util.Execute(command)
70 | if err != nil {
71 | return 0, 0, err
72 | }
73 |
74 | pixelWidth := 0
75 | pixelHeight := 0
76 | re := regexp.MustCompile(`([a-zA-Z]+):\s*(\d+)`)
77 | lines := bytes.Split(result, []byte("\n"))
78 | for _, value := range lines {
79 | if len(value) == 0 {
80 | continue
81 | }
82 |
83 | nameAndValue := re.FindStringSubmatch(string(value))
84 | if nameAndValue == nil {
85 | continue
86 | }
87 |
88 | size, err := strconv.Atoi(nameAndValue[2])
89 | if err != nil {
90 | return 0, 0, errors.WithStack(err)
91 | }
92 |
93 | switch nameAndValue[1] {
94 | case "pixelWidth":
95 | pixelWidth = size
96 | case "pixelHeight":
97 | pixelHeight = size
98 | }
99 | }
100 | return pixelWidth, pixelHeight, nil
101 | }
102 |
103 | func BuildDmg(volumePath string, icon string, backgroundPath string) (string, error) {
104 | if icon != "" {
105 | // cannot use hard link because volume uses different disk
106 | iconPath := filepath.Join(volumePath, ".VolumeIcon.icns")
107 | err := fs.CopyDirOrFile(icon, iconPath)
108 | if err != nil {
109 | return "", errors.WithStack(err)
110 | }
111 |
112 | err = setHasCustomIconAttribute(volumePath)
113 | if err != nil {
114 | return "", errors.WithStack(err)
115 | }
116 |
117 | err = setIsInvisibleAttribute(iconPath)
118 | if err != nil {
119 | return "", errors.WithStack(err)
120 | }
121 | }
122 |
123 | backgroundFileInImage := ""
124 | if backgroundPath != "" {
125 | backgroundPath, err := GetEffectiveBackgroundPath(backgroundPath)
126 | if err != nil {
127 | return "", err
128 | }
129 |
130 | backgroundFileInImage = filepath.Join(volumePath, ".background", filepath.Base(backgroundPath))
131 | err = fs.CopyDirOrFile(backgroundPath, backgroundFileInImage)
132 | if err != nil {
133 | return "", errors.WithStack(err)
134 | }
135 | }
136 | return backgroundFileInImage, nil
137 | }
138 |
139 | func GetEffectiveBackgroundPath(path string) (string, error) {
140 | if strings.HasSuffix(path, ".tiff") || strings.HasSuffix(path, ".TIFF") {
141 | return path, nil
142 | }
143 |
144 | re := regexp.MustCompile(`\.([a-z]+)$`)
145 | retinaFile := re.ReplaceAllString(path, "@2x.$1")
146 | _, err := os.Stat(retinaFile)
147 | if err != nil {
148 | if !os.IsNotExist(err) {
149 | log.Debug("checking retina file", zap.Error(err))
150 | }
151 | return path, nil
152 | }
153 |
154 | tiffFile, err := util.TempFile("", ".tiff")
155 | if err != nil {
156 | return "", err
157 | }
158 |
159 | //noinspection SpellCheckingInspection
160 | _, err = util.Execute(exec.Command("tiffutil", "-cathidpicheck", path, retinaFile, "-out", tiffFile))
161 | if err != nil {
162 | return "", err
163 | }
164 |
165 | return tiffFile, nil
166 | }
167 |
168 | func setHasCustomIconAttribute(path string) error {
169 | data := make([]byte, 32)
170 | // kHasCustomIcon
171 | data[8] = 4
172 | return xattr.Set(path, "com.apple.FinderInfo", data)
173 | }
174 |
175 | func setIsInvisibleAttribute(path string) error {
176 | data := make([]byte, 32)
177 | data[0] = 'i'
178 | data[1] = 'c'
179 | data[2] = 'n'
180 | data[3] = 's'
181 |
182 | // kIsInvisible
183 | data[8] = 0x40
184 | return xattr.Set(path, "com.apple.FinderInfo", data)
185 | }
186 |
--------------------------------------------------------------------------------
/pkg/package-format/dmg/dmg_test.go:
--------------------------------------------------------------------------------
1 | package dmg
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/develar/app-builder/pkg/log"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestSize(t *testing.T) {
11 | t.Skip("Skipping not finished test")
12 | return
13 |
14 | g := NewGomegaWithT(t)
15 |
16 | log.InitLogger()
17 |
18 | w, h, err := getImageSizeUsingSips("/Volumes/data/Desktop/test.png")
19 | g.Expect(err).To(BeNil())
20 | g.Expect(w).To(Equal(1316))
21 | g.Expect(h).To(Equal(894))
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/package-format/fpm/fpm.go:
--------------------------------------------------------------------------------
1 | package fpm
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | "path/filepath"
7 | "strings"
8 |
9 | "github.com/alecthomas/kingpin"
10 | "github.com/develar/app-builder/pkg/download"
11 | "github.com/develar/app-builder/pkg/log"
12 | "github.com/develar/app-builder/pkg/util"
13 | "github.com/pkg/errors"
14 | )
15 |
16 | type FpmConfiguration struct {
17 | Target string `json:"target"`
18 | Args []string `json:"args"`
19 |
20 | Compression string `json:"compression"`
21 |
22 | CustomDepends []string `json:"customDepends"`
23 | CustomRecommends []string `json:"customRecommends"`
24 | }
25 |
26 | func ConfigureCommand(app *kingpin.Application) {
27 | command := app.Command("fpm", "Build FPM targets.")
28 |
29 | configurationJson := command.Flag("configuration", "").Required().String()
30 | command.Action(func(context *kingpin.ParseContext) error {
31 | var configuration FpmConfiguration
32 | err := util.DecodeBase64IfNeeded(*configurationJson, &configuration)
33 | if err != nil {
34 | return err
35 | }
36 |
37 | var fpmPath string
38 | if util.GetCurrentOs() == util.WINDOWS || util.IsEnvTrue("USE_SYSTEM_FPM") {
39 | fpmPath = "fpm"
40 | } else {
41 | fpmDir, err := download.DownloadFpm()
42 | if err != nil {
43 | return err
44 | }
45 | fpmPath = filepath.Join(fpmDir, "fpm")
46 | }
47 |
48 | target := configuration.Target
49 |
50 | // must be first
51 | args := []string{"-s", "dir", "--force", "-t", target}
52 | if util.IsEnvTrue("FPM_DEBUG") {
53 | args = append(args, "--debug")
54 | }
55 | if log.IsDebugEnabled() {
56 | args = append(args, "--log", "debug")
57 | }
58 | args = configureDependencies(&configuration, target, args)
59 | args = configureRecommendations(&configuration, target, args)
60 |
61 | compression := "xz"
62 | if len(configuration.Compression) != 0 {
63 | compression = configuration.Compression
64 | }
65 |
66 | args = configureTargetSpecific(target, args, compression)
67 |
68 | args = append(args, configuration.Args...)
69 |
70 | command := exec.Command(fpmPath, args...)
71 |
72 | executablePath, err := os.Executable()
73 | if err != nil {
74 | return errors.WithStack(err)
75 | }
76 |
77 | env := os.Environ()
78 | env = append(env,
79 | "SZA_ARCHIVE_TYPE=xz",
80 | "FPM_COMPRESS_PROGRAM="+executablePath,
81 | )
82 | command.Env = env
83 |
84 | _, err = util.Execute(command)
85 | if err != nil {
86 | if execError, ok := err.(*util.ExecError); ok && strings.Contains(string(execError.Output), `"Need executable 'rpmbuild' to convert dir to rpm"`) {
87 | var installHint string
88 | if util.GetCurrentOs() == util.MAC {
89 | installHint = "brew install rpm"
90 | } else {
91 | installHint = "sudo apt-get install rpm"
92 | }
93 | log.LOG.Fatal("to build rpm, executable rpmbuild is required, please install: " + installHint)
94 | }
95 | return err
96 | }
97 |
98 | return nil
99 | })
100 | }
101 |
102 | func configureTargetSpecific(target string, args []string, compression string) []string {
103 | switch target {
104 | case "rpm":
105 | args = append(args, "--rpm-os", "linux")
106 | if compression == "xz" {
107 | args = append(args, "--rpm-compression", "xzmt")
108 | } else {
109 | args = append(args, "--rpm-compression", compression)
110 | }
111 | case "deb":
112 | args = append(args, "--deb-compression", compression)
113 | case "pacman":
114 | args = append(args, "--pacman-compression", compression)
115 | }
116 | return args
117 | }
118 |
119 | func configureDependencies(configuration *FpmConfiguration, target string, args []string) []string {
120 | depends := configuration.CustomDepends
121 | if len(depends) == 0 {
122 | depends = getDefaultDepends(target)
123 | }
124 | for _, value := range depends {
125 | args = append(args, "-d", value)
126 | }
127 | return args
128 | }
129 |
130 | func configureRecommendations(configuration *FpmConfiguration, target string, args []string) []string {
131 | if target == "deb" {
132 | recommends := configuration.CustomRecommends
133 | if len(recommends) == 0 {
134 | recommends = getDefaultRecommends(target)
135 | }
136 | for _, value := range recommends {
137 | args = append(args, "--deb-recommends", value)
138 | }
139 | }
140 | return args
141 | }
142 |
143 | //noinspection SpellCheckingInspection
144 | func getDefaultDepends(target string) []string {
145 | switch target {
146 | case "deb":
147 | return []string{
148 | "libgtk-3-0", "libnotify4", "libnss3", "libxss1", "libxtst6", "xdg-utils", "libatspi2.0-0", "libuuid1", "libsecret-1-0",
149 | }
150 |
151 | case "rpm":
152 | return []string{
153 | "gtk3", /* for electron 2+ (electron 1 uses gtk2, but this old version is not supported anymore) */
154 | "libnotify", "nss", "libXScrnSaver", "(libXtst or libXtst6)", "xdg-utils",
155 | "at-spi2-core", /* since 5.0.0 */
156 | "(libuuid or libuuid1)", /* since 4.0.0 */
157 | }
158 |
159 | case "pacman":
160 | return []string{"c-ares", "ffmpeg", "gtk3", "http-parser", "libevent", "libvpx", "libxslt", "libxss", "minizip", "nss", "re2", "snappy", "libnotify", "libappindicator-gtk3"}
161 |
162 | default:
163 | return nil
164 | }
165 | }
166 |
167 | func getDefaultRecommends(target string) []string {
168 | switch target {
169 | case "deb":
170 | return []string{
171 | "libappindicator3-1",
172 | }
173 |
174 | default:
175 | return nil
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/pkg/package-format/snap/desktop-scripts/desktop-gnome-specific.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | ##############################
4 | # GTK launcher specific part #
5 | ##############################
6 |
7 | if [ "$SNAP_DESKTOP_WAYLAND_AVAILABLE" = "true" ]; then
8 | export GDK_BACKEND="wayland"
9 | export CLUTTER_BACKEND="wayland"
10 | # Does not hurt to specify this as well, just in case
11 | export QT_QPA_PLATFORM=wayland-egl
12 | fi
13 |
14 | export GTK_PATH="$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_DESKTOP_ARCH_TRIPLET/gtk-3.0"
15 |
16 | # ibus and fcitx integration
17 | GTK_IM_MODULE_DIR=$XDG_CACHE_HOME/immodules
18 | export GTK_IM_MODULE_FILE=$GTK_IM_MODULE_DIR/immodules.cache
19 | if [ "$SNAP_DESKTOP_COMPONENTS_NEED_UPDATE" = "true" ]; then
20 | rm -rf "$GTK_IM_MODULE_DIR"
21 | mkdir -p "$GTK_IM_MODULE_DIR"
22 | if [ -x "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_DESKTOP_ARCH_TRIPLET/libgtk-3-0/gtk-query-immodules-3.0" ]; then
23 | ln -sf "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_DESKTOP_ARCH_TRIPLET/gtk-3.0/3.0.0/immodules"/*.so "$GTK_IM_MODULE_DIR"
24 | "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_DESKTOP_ARCH_TRIPLET/libgtk-3-0/gtk-query-immodules-3.0" > "$GTK_IM_MODULE_FILE"
25 | elif [ -x "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_DESKTOP_ARCH_TRIPLET/libgtk2.0-0/gtk-query-immodules-2.0" ]; then
26 | ln -sf "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_DESKTOP_ARCH_TRIPLET/gtk-2.0/2.10.0/immodules"/*.so "$GTK_IM_MODULE_DIR"
27 | "$SNAP_DESKTOP_RUNTIME/usr/lib/$SNAP_DESKTOP_ARCH_TRIPLET/libgtk2.0-0/gtk-query-immodules-2.0" > "$GTK_IM_MODULE_FILE"
28 | fi
29 | fi
30 |
31 | exec "$@"
--------------------------------------------------------------------------------
/pkg/package-format/snap/desktop-scripts/desktop-init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | #################
4 | # Launcher init #
5 | #################
6 |
7 | SNAP_DESKTOP_COMPONENTS_NEED_UPDATE="true"
8 |
9 | # shellcheck source=/dev/null
10 | . "$SNAP_USER_DATA/.last_revision" 2>/dev/null || true
11 | if [ "$SNAP_DESKTOP_LAST_REVISION" = "$SNAP_REVISION" ]; then
12 | SNAP_DESKTOP_COMPONENTS_NEED_UPDATE="false"
13 | else
14 | echo "SNAP_DESKTOP_LAST_REVISION=$SNAP_REVISION" > "$SNAP_USER_DATA/.last_revision"
15 | fi
16 |
17 | # Set $REALHOME to the users real home directory
18 | REALHOME="$(getent passwd $UID | cut -d ':' -f 6)"
19 |
20 | # If the user has modified their user-dirs settings, force an update
21 | if [[ -f "$XDG_CONFIG_HOME/user-dirs.dirs.md5sum" && -f "$XDG_CONFIG_HOME/user-dirs.locale.md5sum" ]]; then
22 | if [[ "$(md5sum < "$REALHOME/.config/user-dirs.dirs")" != "$(cat "$XDG_CONFIG_HOME/user-dirs.dirs.md5sum")" ||
23 | "$(md5sum < "$REALHOME/.config/user-dirs.locale")" != "$(cat "$XDG_CONFIG_HOME/user-dirs.locale.md5sum")" ]]; then
24 | SNAP_DESKTOP_COMPONENTS_NEED_UPDATE="true"
25 | fi
26 | fi
27 |
28 | if [ "$SNAP_ARCH" == "amd64" ]; then
29 | ARCH="x86_64-linux-gnu"
30 | elif [ "$SNAP_ARCH" == "armhf" ]; then
31 | ARCH="arm-linux-gnueabihf"
32 | elif [ "$SNAP_ARCH" == "arm64" ]; then
33 | ARCH="aarch64-linux-gnu"
34 | elif [ "$SNAP_ARCH" == "ppc64el" ]; then
35 | ARCH="powerpc64le-linux-gnu"
36 | else
37 | ARCH="$SNAP_ARCH-linux-gnu"
38 | fi
39 |
40 | SNAP_DESKTOP_ARCH_TRIPLET="$ARCH"
41 |
42 | if [ -f "$SNAP/lib/bindtextdomain.so" ]; then
43 | export LD_PRELOAD="$LD_PRELOAD:$SNAP/lib/bindtextdomain.so"
44 | fi
45 |
46 | export REALHOME
47 | export SNAP_DESKTOP_COMPONENTS_NEED_UPDATE
48 | export SNAP_DESKTOP_ARCH_TRIPLET
49 |
50 | exec "$@"
--------------------------------------------------------------------------------
/pkg/package-format/snap/snapStore.go:
--------------------------------------------------------------------------------
1 | package snap
2 |
3 | import (
4 | "os/exec"
5 | "strings"
6 |
7 | "github.com/alecthomas/kingpin"
8 | "github.com/develar/app-builder/pkg/util"
9 | )
10 |
11 | func ConfigurePublishCommand(app *kingpin.Application) {
12 | command := app.Command("publish-snap", "Publish snap.")
13 |
14 | file := command.Flag("file", "").Short('f').String()
15 | channel := command.Flag("channel", "").Short('c').Strings()
16 |
17 | command.Action(func(context *kingpin.ParseContext) error {
18 | return publishToStore(*file, *channel)
19 | })
20 | }
21 |
22 | func publishToStore(file string, channels []string) error {
23 | args := []string{"upload", file}
24 | if len(channels) != 0 {
25 | args = append(args, "--release")
26 | args = append(args, strings.Join(channels, ","))
27 | }
28 |
29 | err := CheckSnapcraftVersion(true)
30 | if err != nil {
31 | return err
32 | }
33 |
34 | command := exec.Command("snapcraft", args...)
35 | err = util.ExecuteAndPipeStdOutAndStdErr(command)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | return nil
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/package-format/snap/snap_test.go:
--------------------------------------------------------------------------------
1 | package snap
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/gomega"
7 | )
8 |
9 | func TestCheckWineVersion(t *testing.T) {
10 | g := NewGomegaWithT(t)
11 |
12 | err := doCheckSnapVersion("4.0", "")
13 | g.Expect(err).NotTo(HaveOccurred())
14 |
15 | err = doCheckSnapVersion("snapcraft, version 4.0.0", "")
16 | g.Expect(err).NotTo(HaveOccurred())
17 |
18 | err = doCheckSnapVersion("snapcraft, version '4.0.0'", "")
19 | g.Expect(err).NotTo(HaveOccurred())
20 |
21 | err = doCheckSnapVersion(" version 4.1.1", "")
22 | g.Expect(err).NotTo(HaveOccurred())
23 |
24 | err = doCheckSnapVersion("3.1", "")
25 | g.Expect(err).To(HaveOccurred())
26 | }
--------------------------------------------------------------------------------
/pkg/plist/plist.go:
--------------------------------------------------------------------------------
1 | package plist
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "os"
7 |
8 | "github.com/alecthomas/kingpin"
9 | "github.com/develar/app-builder/pkg/util"
10 | "github.com/develar/errors"
11 | "github.com/json-iterator/go"
12 | "howett.net/plist"
13 | )
14 |
15 | func ConfigurePlistCommand(app *kingpin.Application) {
16 | command := app.Command("decode-plist", "")
17 | files := command.Flag("file", "").Short('f').Required().Strings()
18 | command.Action(func(context *kingpin.ParseContext) error {
19 | return decode(*files)
20 | })
21 |
22 | encodeCommand := app.Command("encode-plist", "")
23 | encodeCommand.Action(func(context *kingpin.ParseContext) error {
24 | return encode()
25 | })
26 | }
27 |
28 | func encode() error {
29 | var fileToData map[string]interface{}
30 | err := jsoniter.NewDecoder(os.Stdin).Decode(&fileToData)
31 | if err != nil {
32 | return err
33 | }
34 |
35 | files := make([]string, len(fileToData))
36 | i := 0
37 | for file := range fileToData {
38 | files[i] = file
39 | i++
40 | }
41 |
42 | err = util.MapAsync(len(files), func(index int) (func() error, error) {
43 | file := files[index]
44 | data := fileToData[file]
45 | return func() error {
46 | var out bytes.Buffer
47 | err := plist.NewEncoderForFormat(&out, plist.XMLFormat).Encode(data)
48 | if err != nil {
49 | return err
50 | }
51 |
52 | err = ioutil.WriteFile(file, out.Bytes(), 0666)
53 | if err != nil {
54 | return err
55 | }
56 |
57 | return nil
58 | }, nil
59 | })
60 |
61 | if err != nil {
62 | return err
63 | }
64 |
65 | return nil
66 | }
67 |
68 | func decode(files []string) error {
69 | results := make([][]byte, len(files))
70 | err := util.MapAsync(len(files), func(index int) (func() error, error) {
71 | filePath := files[index]
72 | return func() error {
73 | file, err := os.Open(filePath)
74 | if err != nil {
75 | if os.IsNotExist(err) {
76 | results[index] = nil
77 | return nil
78 | }
79 | return errors.WithStack(err)
80 | }
81 |
82 | defer util.Close(file)
83 | decoder := plist.NewDecoder(file)
84 | var value interface{}
85 | err = decoder.Decode(&value)
86 | if err != nil {
87 | return errors.WithStack(err)
88 | }
89 |
90 | jsonData, err := jsoniter.Marshal(value)
91 | if err != nil {
92 | return errors.WithStack(err)
93 | }
94 |
95 | results[index] = jsonData
96 |
97 | return nil
98 | }, nil
99 | })
100 | var b bytes.Buffer
101 | b.WriteString("[")
102 | for index, value := range results {
103 | if index != 0 {
104 | b.WriteString(",")
105 | }
106 |
107 | if len(value) == 0 {
108 | b.WriteString("null")
109 | } else {
110 | b.Write(value)
111 | }
112 | }
113 | b.WriteString("]")
114 | _, _ = os.Stdout.Write(b.Bytes())
115 | return errors.WithStack(err)
116 | }
117 |
--------------------------------------------------------------------------------
/pkg/publisher/s3.go:
--------------------------------------------------------------------------------
1 | package publisher
2 |
3 | import (
4 | "context"
5 | "mime"
6 | "net/http"
7 | "os"
8 | "path"
9 | "strings"
10 | "time"
11 |
12 | "github.com/alecthomas/kingpin"
13 | "github.com/aws/aws-sdk-go/aws"
14 | "github.com/aws/aws-sdk-go/aws/awserr"
15 | "github.com/aws/aws-sdk-go/aws/credentials"
16 | "github.com/aws/aws-sdk-go/aws/session"
17 | "github.com/aws/aws-sdk-go/service/s3/s3manager"
18 | "github.com/develar/app-builder/pkg/util"
19 | "github.com/develar/errors"
20 | )
21 |
22 | type ObjectOptions struct {
23 | file *string
24 |
25 | forcePathStyle *bool
26 | endpoint *string
27 | region *string
28 | bucket *string
29 | key *string
30 |
31 | acl *string
32 | storageClass *string
33 | encryption *string
34 |
35 | accessKey *string
36 | secretKey *string
37 | }
38 |
39 | func ConfigurePublishToS3Command(app *kingpin.Application) {
40 | command := app.Command("publish-s3", "Publish to S3")
41 | options := ObjectOptions{
42 | file: command.Flag("file", "").Required().String(),
43 |
44 | forcePathStyle: command.Flag("forcePathStyle", "").Default("true").Bool(),
45 | region: command.Flag("region", "").String(),
46 | bucket: command.Flag("bucket", "").Required().String(),
47 | key: command.Flag("key", "").Required().String(),
48 | endpoint: command.Flag("endpoint", "").String(),
49 |
50 | acl: command.Flag("acl", "").String(),
51 | storageClass: command.Flag("storageClass", "").String(),
52 | encryption: command.Flag("encryption", "").String(),
53 |
54 | accessKey: command.Flag("accessKey", "").String(),
55 | secretKey: command.Flag("secretKey", "").String(),
56 | }
57 |
58 | command.Action(func(context *kingpin.ParseContext) error {
59 | err := upload(&options)
60 | if err != nil {
61 | return err
62 | }
63 | return nil
64 | })
65 |
66 | configureResolveBucketLocationCommand(app)
67 | }
68 |
69 | func configureResolveBucketLocationCommand(app *kingpin.Application) {
70 | command := app.Command("get-bucket-location", "")
71 | bucket := command.Flag("bucket", "").Required().String()
72 | command.Action(func(parseContext *kingpin.ParseContext) error {
73 | requestContext, _ := util.CreateContextWithTimeout(30*time.Second)
74 | result, err := getBucketRegion(aws.NewConfig(), *bucket, requestContext, createHttpClient())
75 | if err != nil {
76 | return err
77 | }
78 |
79 | _, err = os.Stdout.WriteString(result)
80 | if err != nil {
81 | return errors.WithStack(err)
82 | }
83 | return nil
84 | })
85 | }
86 |
87 | func getBucketRegion(awsConfig *aws.Config, bucket string, context context.Context, httpClient *http.Client) (string, error) {
88 | awsSession, err := session.NewSession(awsConfig, &aws.Config{
89 | // any region required
90 | Region: aws.String("us-east-1"),
91 | HTTPClient: httpClient,
92 | })
93 | if err != nil {
94 | return "", errors.WithStack(err)
95 | }
96 |
97 | result, err := s3manager.GetBucketRegion(context, awsSession, bucket, "")
98 | if err != nil {
99 | if awsError, ok := err.(awserr.Error); ok && awsError.Code() == "NotFound" {
100 | return "", errors.Errorf("unable to find bucket %s's region not found", bucket)
101 | }
102 | return "", errors.WithStack(err)
103 | }
104 | return result, nil
105 | }
106 |
107 | func upload(options *ObjectOptions) error {
108 | publishContext, _ := util.CreateContext()
109 |
110 | httpClient := createHttpClient()
111 |
112 | awsConfig := &aws.Config{
113 | HTTPClient: httpClient,
114 | }
115 | if *options.endpoint != "" {
116 | awsConfig.Endpoint = options.endpoint
117 | awsConfig.S3ForcePathStyle = aws.Bool(*options.forcePathStyle)
118 | }
119 |
120 | //awsConfig.WithLogLevel(aws.LogDebugWithHTTPBody)
121 |
122 | if *options.accessKey != "" {
123 | awsConfig.Credentials = credentials.NewStaticCredentials(*options.accessKey, *options.secretKey, "")
124 | }
125 |
126 | switch {
127 | case *options.region != "":
128 | awsConfig.Region = options.region
129 | case *options.endpoint != "":
130 | awsConfig.Region = aws.String("us-east-1")
131 | default:
132 | // AWS SDK for Go requires region
133 | region, err := getBucketRegion(awsConfig, *options.bucket, publishContext, httpClient)
134 | if err != nil {
135 | return errors.WithStack(err)
136 | }
137 | awsConfig.Region = ®ion
138 | }
139 |
140 | awsSession, err := session.NewSession(awsConfig)
141 | if err != nil {
142 | return errors.WithStack(err)
143 | }
144 |
145 | uploader := s3manager.NewUploader(awsSession)
146 |
147 | file, err := os.Open(*options.file)
148 | defer util.Close(file)
149 | if err != nil {
150 | return errors.WithStack(err)
151 | }
152 |
153 | uploadInput := s3manager.UploadInput{
154 | Bucket: options.bucket,
155 | Key: options.key,
156 | ContentType: aws.String(getMimeType(*options.key)),
157 | Body: file,
158 | }
159 | if *options.acl != "" {
160 | uploadInput.ACL = options.acl
161 | }
162 | if *options.storageClass != "" {
163 | uploadInput.StorageClass = options.storageClass
164 | }
165 | if *options.encryption != "" {
166 | uploadInput.ServerSideEncryption = options.encryption
167 | }
168 |
169 | _, err = uploader.UploadWithContext(publishContext, &uploadInput)
170 | if err != nil {
171 | return errors.WithStack(err)
172 | }
173 |
174 | return nil
175 | }
176 |
177 | func createHttpClient() *http.Client {
178 | return &http.Client{
179 | Transport: &http.Transport{
180 | Proxy: util.ProxyFromEnvironmentAndNpm,
181 | },
182 | }
183 | }
184 |
185 | func getMimeType(key string) string {
186 | if strings.HasSuffix(key, ".AppImage") {
187 | return "application/vnd.appimage"
188 | }
189 | if strings.HasSuffix(key, ".exe") {
190 | return "application/octet-stream"
191 | }
192 | if strings.HasSuffix(key, ".zip") {
193 | return "application/zip"
194 | }
195 | if strings.HasSuffix(key, ".blockmap") {
196 | return "application/gzip"
197 | }
198 | if strings.HasSuffix(key, ".snap") {
199 | return "application/vnd.snap"
200 | }
201 | if strings.HasSuffix(key, ".dmg") {
202 | //noinspection SpellCheckingInspection
203 | return "application/x-apple-diskimage"
204 | }
205 |
206 | ext := path.Ext(key)
207 | if ext != "" {
208 | mimeType := mime.TypeByExtension(ext)
209 | if mimeType != "" {
210 | return mimeType
211 | }
212 | }
213 | return "application/octet-stream"
214 | }
215 |
--------------------------------------------------------------------------------
/pkg/rcedit/rcedit.go:
--------------------------------------------------------------------------------
1 | package rcedit
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | "path/filepath"
7 | "runtime"
8 |
9 | "github.com/alecthomas/kingpin"
10 | "github.com/develar/app-builder/pkg/download"
11 | "github.com/develar/app-builder/pkg/util"
12 | "github.com/develar/app-builder/pkg/wine"
13 | )
14 |
15 | func ConfigureCommand(app *kingpin.Application) {
16 | command := app.Command("rcedit", "")
17 | configuration := command.Flag("args", "").Required().String()
18 |
19 | command.Action(func(context *kingpin.ParseContext) error {
20 | var rcEditArgs []string
21 | err := util.DecodeBase64IfNeeded(*configuration, &rcEditArgs)
22 | if err != nil {
23 | return err
24 | }
25 | return editResources(rcEditArgs)
26 | })
27 | }
28 |
29 | func editResources(args []string) error {
30 | winCodeSignPath, err := download.DownloadWinCodeSign()
31 | if err != nil {
32 | return err
33 | }
34 |
35 | if util.GetCurrentOs() == util.WINDOWS || util.IsWSL() {
36 | var rcEditExecutable string
37 | if runtime.GOARCH == "amd64" {
38 | rcEditExecutable = "rcedit-x64.exe"
39 | } else {
40 | rcEditExecutable = "rcedit-ia32.exe"
41 | }
42 |
43 | rcEditPath := filepath.Join(winCodeSignPath, rcEditExecutable)
44 |
45 | if util.IsWSL() {
46 | err = os.Chmod(rcEditPath, 0755)
47 | if err != nil {
48 | return err
49 | }
50 | }
51 |
52 | command := exec.Command(rcEditPath, args...)
53 | _, err = util.Execute(command)
54 | if err != nil {
55 | return err
56 | }
57 |
58 | return nil
59 | }
60 |
61 | err = wine.ExecWine(filepath.Join(winCodeSignPath, "rcedit-ia32.exe"), filepath.Join(winCodeSignPath, "rcedit-x64.exe"), args)
62 | if err != nil {
63 | return err
64 | }
65 | return nil
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/remoteBuild/buildAgentEndpoint.go:
--------------------------------------------------------------------------------
1 | package remoteBuild
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 | "os"
7 | "strconv"
8 | "strings"
9 | "time"
10 |
11 | "github.com/develar/app-builder/pkg/log"
12 | "github.com/develar/app-builder/pkg/util"
13 | "github.com/develar/errors"
14 | "github.com/json-iterator/go"
15 | "go.uber.org/zap"
16 | )
17 |
18 | func findBuildAgent(transport http.RoundTripper) (string, error) {
19 | result := os.Getenv("BUILD_AGENT_HOST")
20 | if result != "" {
21 | log.Debug("build agent host is set explicitly", zap.String("host", result))
22 | return addHttpsIfNeed(result), nil
23 | }
24 |
25 | routerUrl := addHttpsIfNeed(util.GetEnvOrDefault("BUILD_SERVICE_ROUTER_HOST", "https://service.electron.build"))
26 | // add random query param to prevent caching
27 | routerUrl += "/find-build-agent?no-cache=" + strconv.FormatInt(time.Now().Unix(), 32)
28 |
29 | client := &http.Client{
30 | Transport: transport,
31 | Timeout: 30 * time.Second,
32 | }
33 |
34 | for attemptNumber := 0; ; attemptNumber++ {
35 | result, err := getBuildAgentEndpoint(client, routerUrl)
36 | if err != nil {
37 | if attemptNumber == 3 {
38 | return "", err
39 | }
40 |
41 | waitTime := 2 * (attemptNumber + 1)
42 | log.Warn("cannot get, wait", zap.Error(err), zap.Int("attempt", attemptNumber), zap.Int("waitTime", waitTime))
43 | time.Sleep(time.Duration(waitTime) * time.Second)
44 | continue
45 | }
46 |
47 | return result, nil
48 | }
49 | }
50 |
51 | func getBuildAgentEndpoint(client *http.Client, url string) (string, error) {
52 | response, err := client.Get(url)
53 | if err != nil {
54 | return "", err
55 | }
56 |
57 | if response.Body != nil {
58 | defer util.Close(response.Body)
59 | }
60 |
61 | if response.StatusCode != http.StatusOK {
62 | return "", errors.Errorf("cannot get %s: http error %d", url, response.StatusCode)
63 | }
64 |
65 | bodyBytes, err := ioutil.ReadAll(response.Body)
66 | if err != nil {
67 | return "", err
68 | }
69 |
70 | result := jsoniter.Get(bodyBytes, "endpoint")
71 | err = result.LastError()
72 | if err != nil {
73 | return "", err
74 | }
75 |
76 | return result.ToString(), nil
77 | }
78 |
79 | func addHttpsIfNeed(s string) string {
80 | if strings.HasPrefix(s, "http") {
81 | return s
82 | } else {
83 | return "https://" + s
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/pkg/remoteBuild/tls.go:
--------------------------------------------------------------------------------
1 | package remoteBuild
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 |
7 | "github.com/develar/app-builder/pkg/util"
8 | )
9 |
10 | //noinspection SpellCheckingInspection
11 | const localCert = `-----BEGIN CERTIFICATE-----
12 | MIIBiDCCAS+gAwIBAgIRAPHSzTRLcN2nElhQdaRP47IwCgYIKoZIzj0EAwIwJDEi
13 | MCAGA1UEAxMZZWxlY3Ryb24uYnVpbGQubG9jYWwgcm9vdDAeFw0xNzExMTMxNzI4
14 | NDFaFw0yNzExMTExNzI4NDFaMCQxIjAgBgNVBAMTGWVsZWN0cm9uLmJ1aWxkLmxv
15 | Y2FsIHJvb3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQVyduuCT2acuk2QH06
16 | yal/b6O7eTTpOHk3Ucjc+ZZta2vC2+c1IKcSAwimKbTbK+nRxWWJl9ZYx9RTwbRf
17 | QjD6o0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
18 | FgQUlm08vBe4CUNAOTQN5Z1RNTfJjjYwCgYIKoZIzj0EAwIDRwAwRAIgMXlT6YM8
19 | 4pQtnhUjijVMz+NlcYafS1CEbNBMaWhP87YCIGXUmu7ON9hRLanXzBNBlrtTQG+i
20 | l/NT6REwZA64/lNy
21 | -----END CERTIFICATE-----
22 | `
23 |
24 | //noinspection SpellCheckingInspection
25 | const productionCert = `
26 | -----BEGIN CERTIFICATE-----
27 | MIIBfjCCASOgAwIBAgIRAM4hTUv8Pyo8K5cxaTWPjagwCgYIKoZIzj0EAwIwHjEc
28 | MBoGA1UEAxMTZWxlY3Ryb24uYnVpbGQgcm9vdDAeFw0xODEwMjgyMTQwMjVaFw0x
29 | OTEwMjgyMTQwMjVaMB4xHDAaBgNVBAMTE2VsZWN0cm9uLmJ1aWxkIHJvb3QwWTAT
30 | BgcqhkjOPQIBBggqhkjOPQMBBwNCAAR+4b6twzizN/z27yvwrCV5kinGUrfo+W7n
31 | L/l28ErscNe1BDSyh/IYrnMWb1rDMSLGhvkgI9Cfex1whNPHR101o0IwQDAOBgNV
32 | HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU6Dq8kK7tQlrt
33 | zkIYrYiTZGpHEp0wCgYIKoZIzj0EAwIDSQAwRgIhAP0RasTfSsU93rbNgtiRRVOi
34 | im40qSwIjEF3AsuRpl/jAiEA83J185J3KoaGiDyTnH9UfbC5XOznh5vZNMUsCv4l
35 | YYs=
36 | -----END CERTIFICATE-----
37 | `
38 |
39 | func getTls() *tls.Config {
40 | caCertPool := x509.NewCertPool()
41 | pemCerts, serverName := getCaCerts()
42 | caCertPool.AppendCertsFromPEM(pemCerts)
43 |
44 | return &tls.Config{
45 | ServerName: serverName,
46 | RootCAs: caCertPool,
47 | }
48 | }
49 |
50 | func getCaCerts() ([]byte, string) {
51 | isUseLocalCert := util.IsEnvTrue("USE_BUILD_SERVICE_LOCAL_CA")
52 | if isUseLocalCert {
53 | return []byte(localCert), "electron.build.local"
54 | } else {
55 | return []byte(productionCert), "electron.build"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/util/async.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "runtime"
5 |
6 | "github.com/develar/app-builder/pkg/log"
7 | "github.com/develar/errors"
8 | "go.uber.org/zap"
9 | )
10 |
11 | func MapAsync(taskCount int, taskProducer func(taskIndex int) (func() error, error)) error {
12 | return MapAsyncConcurrency(taskCount, runtime.NumCPU() + 1, taskProducer)
13 | }
14 |
15 | func MapAsyncConcurrency(taskCount int, concurrency int, taskProducer func(taskIndex int) (func() error, error)) error {
16 | if taskCount == 0 {
17 | return nil
18 | }
19 |
20 | log.Debug("map async", zap.Int("taskCount", taskCount))
21 |
22 | errorChannel := make(chan error, concurrency)
23 | doneChannel := make(chan bool, taskCount)
24 | quitChannel := make(chan struct{})
25 | sem := make(chan bool, concurrency)
26 |
27 | markDone := func() {
28 | // release semaphore, notify done
29 | doneChannel <- true
30 | select {
31 | case <-sem:
32 | return
33 | case <-errorChannel:
34 | break
35 | }
36 | }
37 |
38 | for i := 0; i < taskCount; i++ {
39 | // wait semaphore
40 | select {
41 | case <-errorChannel:
42 | break
43 | case sem <- true:
44 | // ok
45 | }
46 |
47 | task, err := taskProducer(i)
48 | if err != nil {
49 | close(quitChannel)
50 | return errors.WithStack(err)
51 | }
52 |
53 | if task == nil {
54 | markDone()
55 | continue
56 | }
57 |
58 | go func(task func() error) {
59 | defer markDone()
60 |
61 | // select waits on multiple channels, if quitChannel is closed, read will succeed without blocking
62 | // the default case in a select is run if no other case is ready
63 | select {
64 | case <-quitChannel:
65 | return
66 |
67 | default:
68 | err := task()
69 | if err != nil {
70 | // do not wrap - up to client to wrap if needed (to avoid later to discover cause)
71 | errorChannel <- err
72 | }
73 | }
74 | }(task)
75 | }
76 |
77 | finishedCount := 0
78 | for {
79 | select {
80 | case err := <-errorChannel:
81 | close(quitChannel)
82 | return err
83 |
84 | case <-doneChannel:
85 | finishedCount++
86 | if finishedCount == taskCount {
87 | return nil
88 | }
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/pkg/util/cancel.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "context"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 | "time"
9 |
10 | "github.com/develar/app-builder/pkg/log"
11 | "go.uber.org/zap"
12 | )
13 |
14 | func CreateContext() (context.Context, context.CancelFunc) {
15 | c, cancel := context.WithCancel(context.Background())
16 | go onCancelSignal(cancel)
17 | return c, cancel
18 | }
19 |
20 | func CreateContextWithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) {
21 | c, cancel := context.WithTimeout(context.Background(), timeout)
22 | go onCancelSignal(cancel)
23 | return c, cancel
24 | }
25 |
26 | func onCancelSignal(cancel context.CancelFunc) {
27 | defer cancel()
28 | signals := make(chan os.Signal, 2)
29 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
30 | sig := <-signals
31 | log.Info("canceling", zap.String("signal", sig.String()))
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/util/env.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "os"
5 | )
6 |
7 | func GetEnvOrDefault(envName string, defaultValue string) string {
8 | result := os.Getenv(envName)
9 | if result == "" {
10 | return defaultValue
11 | } else {
12 | return result
13 | }
14 | }
15 |
16 | func IsEnvTrue(envName string) bool {
17 | value, ok := os.LookupEnv(envName)
18 | return ok && (value == "true" || value == "" || value == "1")
19 | }
20 |
21 | func Get7zPath() string {
22 | return GetEnvOrDefault("SZA_PATH", "7za")
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/util/exec.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "os"
6 | "os/exec"
7 | "strings"
8 |
9 | "github.com/develar/app-builder/pkg/log"
10 | "github.com/develar/errors"
11 | "go.uber.org/zap"
12 | )
13 |
14 | // useful for snap, where prime command took a lot of time and we need to read progress messages
15 | func ExecuteAndPipeStdOutAndStdErr(command *exec.Cmd) error {
16 | preCommandExecute(command)
17 |
18 | // not an error - command error output printed to out stdout (like logging)
19 | command.Stdout = os.Stderr
20 | command.Stderr = os.Stderr
21 | err := command.Run()
22 | if err != nil {
23 | return errors.WithStack(err)
24 | }
25 |
26 | return nil
27 | }
28 |
29 | type ExecError struct {
30 | Cause error
31 | CommandAndArgs []string
32 | WorkingDirectory string
33 |
34 | Output []byte
35 | ErrorOutput []byte
36 |
37 | Message string
38 | ExtraFields []zap.Field
39 | }
40 |
41 | func (e *ExecError) Error() string {
42 | return e.Cause.Error()
43 | }
44 |
45 | func Execute(command *exec.Cmd) ([]byte, error) {
46 | preCommandExecute(command)
47 |
48 | var output bytes.Buffer
49 | command.Stdout = &output
50 |
51 | var errorOutput bytes.Buffer
52 | command.Stderr = &errorOutput
53 |
54 | err := command.Run()
55 | if err != nil {
56 | return output.Bytes(), &ExecError{
57 | Cause: err,
58 | CommandAndArgs: command.Args,
59 | WorkingDirectory: command.Dir,
60 |
61 | Output: output.Bytes(),
62 | ErrorOutput: errorOutput.Bytes(),
63 | }
64 | } else if log.IsDebugEnabled() && !(strings.HasSuffix(command.Path, "openssl") || strings.HasSuffix(command.Path, "openssl.exe")) {
65 | var fields []zap.Field
66 | fields = append(fields, zap.String("executable", command.Args[0]))
67 | if output.Len() > 0 {
68 | fields = append(fields, zap.String("out", output.String()))
69 | }
70 | if errorOutput.Len() > 0 {
71 | fields = append(fields, zap.String("errorOut", errorOutput.String()))
72 | }
73 | log.Debug("command executed", fields...)
74 | }
75 |
76 | return output.Bytes(), nil
77 | }
78 |
79 | func preCommandExecute(command *exec.Cmd) {
80 | if log.IsDebugEnabled() {
81 | log.Debug("execute command", zap.String("command", argListToSafeString(command.Args)), zap.String("workingDirectory", command.Dir))
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/pkg/util/json-util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/develar/errors"
7 | "github.com/json-iterator/go"
8 | )
9 |
10 | func WriteStringProperty(name string, value string, jsonWriter *jsoniter.Stream) {
11 | jsonWriter.WriteObjectField(name)
12 | jsonWriter.WriteString(value)
13 | }
14 |
15 | func FlushJsonWriterAndCloseOut(jsonWriter *jsoniter.Stream) error {
16 | err := jsonWriter.Flush()
17 | if err != nil {
18 | return errors.WithStack(err)
19 | }
20 | return errors.WithStack(os.Stdout.Close())
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/util/messageError.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | type MessageError interface {
4 | Error() string
5 | ErrorCode() string
6 | }
7 |
8 | func NewMessageError(message string, code string) *messageError {
9 | return &messageError{
10 | message: message,
11 | code: code,
12 | }
13 | }
14 |
15 | type messageError struct {
16 | message string
17 | code string
18 | }
19 |
20 | func (e *messageError) Error() string {
21 | return e.message
22 | }
23 |
24 | func (e *messageError) ErrorCode() string {
25 | return e.code
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/util/osName.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "runtime"
5 | )
6 |
7 | type OsName int
8 |
9 | const (
10 | MAC OsName = iota
11 | LINUX
12 | WINDOWS
13 | )
14 |
15 | func (t OsName) String() string {
16 | switch t {
17 | case MAC:
18 | return "mac"
19 | case WINDOWS:
20 | return "windows"
21 | default:
22 | return "linux"
23 | }
24 | }
25 |
26 | //noinspection GoExportedFuncWithUnexportedType
27 | func GetCurrentOs() OsName {
28 | return ToOsName(runtime.GOOS)
29 | }
30 |
31 | //noinspection GoExportedFuncWithUnexportedType
32 | func ToOsName(name string) OsName {
33 | switch name {
34 | case "windows", "win32", "win":
35 | return WINDOWS
36 | case "darwin", "mac", "macOS", "macOs":
37 | return MAC
38 | default:
39 | return LINUX
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/util/proxy.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "net/http"
5 | "net/url"
6 | "os"
7 | "path/filepath"
8 |
9 | "github.com/develar/app-builder/pkg/log"
10 | "github.com/develar/errors"
11 | "github.com/mitchellh/go-homedir"
12 | "github.com/zieckey/goini"
13 | "go.uber.org/zap"
14 | )
15 |
16 | func ProxyFromEnvironmentAndNpm(req *http.Request) (*url.URL, error) {
17 | if os.Getenv("NO_PROXY") == "*" {
18 | return nil, nil
19 | }
20 |
21 | result, err := http.ProxyFromEnvironment(req)
22 | if err != nil {
23 | return nil, errors.WithStack(err)
24 | }
25 |
26 | if result != nil {
27 | return result, nil
28 | }
29 |
30 | result, err = proxyFromNpm()
31 | if err != nil {
32 | log.Error("cannot detect npm proxy", zap.Error(err))
33 | return nil, nil
34 | }
35 | return result, nil
36 | }
37 |
38 | func proxyFromNpm() (*url.URL, error) {
39 | userHomeDir, err := homedir.Dir()
40 | if err != nil {
41 | return nil, errors.WithStack(err)
42 | }
43 |
44 | ini := goini.New()
45 | //noinspection SpellCheckingInspection
46 | err = ini.ParseFile(filepath.Join(userHomeDir, ".npmrc"))
47 | if err != nil {
48 | if os.IsNotExist(err) {
49 | return nil, nil
50 | }
51 | return nil, errors.WithStack(err)
52 | }
53 |
54 | v, ok := ini.Get("https-proxy")
55 | if !ok {
56 | v, _ = ini.Get("proxy")
57 | }
58 |
59 | if len(v) == 0 || v == "false" || v == "true" {
60 | return nil, nil
61 | }
62 |
63 | parsed, err := url.Parse(v)
64 | if err != nil {
65 | return nil, errors.WithStack(err)
66 | }
67 | return parsed, nil
68 | }
69 |
--------------------------------------------------------------------------------
/pkg/util/tempfile.go:
--------------------------------------------------------------------------------
1 | // Copyright 2010 The Go Authors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style
3 | // license that can be found in the LICENSE file.
4 |
5 | package util
6 |
7 | import (
8 | "os"
9 | "path/filepath"
10 | "strconv"
11 | "sync"
12 | "time"
13 |
14 | "github.com/develar/errors"
15 | )
16 |
17 | // Random number state.
18 | // We generate random temporary file names so that there's a good
19 | // chance the file doesn't exist yet - keeps the number of tries in
20 | // TempFile to a minimum.
21 | var rand uint32
22 | var randMutex sync.Mutex
23 |
24 | func reseed() uint32 {
25 | return uint32(time.Now().UnixNano() + int64(os.Getpid()))
26 | }
27 |
28 | func nextPrefix() string {
29 | randMutex.Lock()
30 | r := rand
31 | if r == 0 {
32 | r = reseed()
33 | }
34 | r = r*1664525 + 1013904223 // constants from Numerical Recipes
35 | rand = r
36 | randMutex.Unlock()
37 | return strconv.Itoa(int(1e9 + r%1e9))[1:]
38 | }
39 |
40 | // TempFile creates a new temporary file in the directory dir
41 | // with a name beginning with prefix, opens the file for reading
42 | // and writing, and returns the resulting *os.File.
43 | // If dir is the empty string, TempFile uses the default directory
44 | // for temporary files (see os.TempDir).
45 | // Multiple programs calling TempFile simultaneously
46 | // will not choose the same file. The caller can use f.Name()
47 | // to find the pathname of the file. It is the caller's responsibility
48 | // to remove the file when no longer needed.
49 | func TempFile(dir, suffix string) (string, error) {
50 | if dir == "" {
51 | dir = os.TempDir()
52 | }
53 |
54 | nConflict := 0
55 | for i := 0; i < 10000; i++ {
56 | name := filepath.Join(dir, nextPrefix()+suffix)
57 | _, err := os.Lstat(name)
58 | if os.IsNotExist(err) {
59 | return name, nil
60 | }
61 |
62 | if nConflict++; nConflict > 10 {
63 | randMutex.Lock()
64 | rand = reseed()
65 | randMutex.Unlock()
66 | }
67 | }
68 | return "", errors.Errorf("cannot find unique file name")
69 | }
70 |
71 | // TempDir creates a new temporary directory in the directory dir
72 | // with a name beginning with prefix and returns the path of the
73 | // new directory. If dir is the empty string, TempDir uses the
74 | // default directory for temporary files (see os.TempDir).
75 | // Multiple programs calling TempDir simultaneously
76 | // will not choose the same directory. It is the caller's responsibility
77 | // to remove the directory when no longer needed.
78 | func TempDir(dir, suffix string) (name string, err error) {
79 | if dir == "" {
80 | dir = os.TempDir()
81 | }
82 |
83 | nConflict := 0
84 | for i := 0; i < 10000; i++ {
85 | try := filepath.Join(dir, nextPrefix()+suffix)
86 | err = os.Mkdir(try, 0700)
87 | if os.IsExist(err) {
88 | if nConflict++; nConflict > 10 {
89 | randMutex.Lock()
90 | rand = reseed()
91 | randMutex.Unlock()
92 | }
93 | continue
94 | }
95 | if os.IsNotExist(err) {
96 | if _, err := os.Stat(dir); os.IsNotExist(err) {
97 | return "", err
98 | }
99 | }
100 | if err == nil {
101 | name = try
102 | }
103 | break
104 | }
105 | return
106 | }
107 |
--------------------------------------------------------------------------------
/pkg/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "crypto/sha512"
5 | "encoding/base64"
6 | "encoding/hex"
7 | "fmt"
8 | "io"
9 | "os"
10 | "os/exec"
11 | "strings"
12 |
13 | "github.com/alecthomas/kingpin"
14 | "github.com/develar/app-builder/pkg/log"
15 | "github.com/develar/errors"
16 | "github.com/json-iterator/go"
17 | "go.uber.org/zap"
18 | "gopkg.in/alessio/shellescape.v1"
19 | )
20 |
21 | func ConfigureIsRemoveStageParam(command *kingpin.CmdClause) *bool {
22 | var isRemoveStageDefaultValue string
23 | if log.IsDebugEnabled() && !IsEnvTrue("BUILDER_REMOVE_STAGE_EVEN_IF_DEBUG") {
24 | isRemoveStageDefaultValue = "false"
25 | } else {
26 | isRemoveStageDefaultValue = "true"
27 | }
28 |
29 | return command.Flag("remove-stage", "Whether to remove stage after build.").Default(isRemoveStageDefaultValue).Bool()
30 | }
31 |
32 | func WriteJsonToStdOut(v interface{}) error {
33 | serializedInputInfo, err := jsoniter.ConfigFastest.Marshal(v)
34 | if err != nil {
35 | return errors.WithStack(err)
36 | }
37 |
38 | _, err = os.Stdout.Write(serializedInputInfo)
39 | _ = os.Stdout.Close()
40 | return errors.WithStack(err)
41 | }
42 |
43 | func argListToSafeString(args []string) string {
44 | var result strings.Builder
45 | for index, value := range args {
46 | if strings.HasPrefix(value, "pass:") {
47 | hasher := sha512.New()
48 | _, err := hasher.Write([]byte(value))
49 | if err == nil {
50 | value = "sha512-first-8-chars-" + hex.EncodeToString(hasher.Sum(nil)[0:4])
51 | } else {
52 | log.Warn("cannot compute sha512 hash of password to log", zap.Error(err))
53 | value = ""
54 | }
55 | } else {
56 | value = shellescape.Quote(value)
57 | }
58 |
59 | if index > 0 {
60 | result.WriteRune(' ')
61 | }
62 | result.WriteString(value)
63 | }
64 |
65 | return result.String()
66 | }
67 |
68 | func StartPipedCommands(producer *exec.Cmd, consumer *exec.Cmd) error {
69 | err := producer.Start()
70 | if err != nil {
71 | return errors.WithStack(err)
72 | }
73 |
74 | err = consumer.Start()
75 | if err != nil {
76 | return errors.WithStack(err)
77 | }
78 |
79 | return nil
80 | }
81 |
82 | func RunPipedCommands(producer *exec.Cmd, consumer *exec.Cmd) error {
83 | err := StartPipedCommands(producer, consumer)
84 | if err != nil {
85 | return errors.WithStack(err)
86 | }
87 |
88 | err = WaitPipedCommand(producer, consumer)
89 | if err != nil {
90 | return errors.WithStack(err)
91 | }
92 |
93 | return nil
94 | }
95 |
96 | func WaitPipedCommand(producer *exec.Cmd, consumer *exec.Cmd) error {
97 | err := producer.Wait()
98 | if err != nil {
99 | return errors.WithStack(err)
100 | }
101 |
102 | err = consumer.Wait()
103 | if err != nil {
104 | return errors.WithStack(err)
105 | }
106 |
107 | return nil
108 | }
109 |
110 | func LogErrorAndExit(err error) {
111 | if execError, ok := err.(*ExecError); ok {
112 | message := execError.Message
113 | if len(message) == 0 {
114 | message = "cannot execute"
115 | }
116 |
117 | fields := execError.ExtraFields
118 | fields = append(fields, CreateExecErrorLogEntry(execError)...)
119 | log.LOG.Error(message, fields...)
120 | _ = log.LOG.Sync()
121 | // electron-builder in this case doesn't report app-builder error
122 | os.Exit(2)
123 | } else {
124 | log.LOG.Fatal(fmt.Sprintf("%+v", err))
125 | }
126 | }
127 |
128 | func CreateExecErrorLogEntry(execError *ExecError) []zap.Field {
129 | var fields []zap.Field
130 | fields = append(fields, zap.NamedError("cause", execError.Cause))
131 | if len(execError.Output) > 0 {
132 | fields = append(fields, zap.ByteString("out", execError.Output))
133 | }
134 | if len(execError.ErrorOutput) > 0 {
135 | fields = append(fields, zap.ByteString("errorOut", execError.ErrorOutput))
136 | }
137 | fields = append(fields,
138 | zap.String("command", argListToSafeString(execError.CommandAndArgs)),
139 | zap.String("workingDir", execError.WorkingDirectory),
140 | )
141 | return fields
142 | }
143 |
144 | // http://www.blevesearch.com/news/Deferred-Cleanup,-Checking-Errors,-and-Potential-Problems/
145 | func Close(c io.Closer) {
146 | err := c.Close()
147 | if err != nil && err != os.ErrClosed && err != io.ErrClosedPipe {
148 | if e, ok := err.(*os.PathError); ok && e.Err == os.ErrClosed {
149 | return
150 | }
151 | log.Error("cannot close", zap.Error(err))
152 | }
153 | }
154 |
155 | func ContainsString(list []string, s string) bool {
156 | for _, item := range list {
157 | if item == s {
158 | return true
159 | }
160 | }
161 | return false
162 | }
163 |
164 | func DecodeBase64IfNeeded(data string, v interface{}) error {
165 | if strings.HasPrefix(data, "{") || strings.HasPrefix(data, "[") {
166 | return jsoniter.UnmarshalFromString(data, v)
167 | } else {
168 | decodedData, err := base64.StdEncoding.DecodeString(data)
169 | if err != nil {
170 | return errors.WithStack(err)
171 | }
172 | return jsoniter.Unmarshal(decodedData, v)
173 | }
174 | }
--------------------------------------------------------------------------------
/pkg/util/wsl.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "bytes"
5 | "io/ioutil"
6 | "os/exec"
7 | "strings"
8 |
9 | "github.com/develar/errors"
10 | )
11 |
12 | func IsWSL() bool {
13 | if GetCurrentOs() != LINUX {
14 | return false
15 | }
16 |
17 | release, err := getOsRelease()
18 | if err != nil {
19 | return false
20 | }
21 |
22 | if strings.Contains(strings.ToLower(release), "microsoft") {
23 | return true
24 | }
25 |
26 | version, err := getProcVersion()
27 | if err != nil {
28 | return false
29 | }
30 |
31 | if strings.Contains(strings.ToLower(version), "microsoft") {
32 | return true
33 | }
34 |
35 | return false
36 | }
37 |
38 | func getOsRelease() (string, error) {
39 | cmd := exec.Command("uname", "-r")
40 |
41 | var out bytes.Buffer
42 | var stderr bytes.Buffer
43 | cmd.Stdout = &out
44 | cmd.Stderr = &stderr
45 |
46 | err := cmd.Run()
47 | if err != nil {
48 | return "", err
49 | }
50 |
51 | return out.String(), nil
52 | }
53 |
54 | func getProcVersion() (string, error) {
55 | content, err := ioutil.ReadFile("/proc/version")
56 | if err != nil {
57 | return "", errors.WithStack(err)
58 | }
59 |
60 | return string(content), nil
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/wine/wine.go:
--------------------------------------------------------------------------------
1 | package wine
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "os"
8 | "os/exec"
9 | "path/filepath"
10 | "strings"
11 | "time"
12 |
13 | "github.com/alecthomas/kingpin"
14 | "github.com/develar/app-builder/pkg/download"
15 | "github.com/develar/app-builder/pkg/log"
16 | "github.com/develar/app-builder/pkg/util"
17 | "github.com/json-iterator/go"
18 | "github.com/mcuadros/go-version"
19 | "go.uber.org/zap"
20 | )
21 |
22 | func ConfigureCommand(app *kingpin.Application) {
23 | command := app.Command("wine", "")
24 |
25 | ia32Name := command.Flag("ia32", "The ia32 executable name").String()
26 | // x64Name not used for now
27 | x64Name := command.Flag("x64", "The x64 executable name").String()
28 | jsonEncodedArgs := command.Flag("args", "The json-encoded array of executable args").String()
29 |
30 | command.Validate(func(clause *kingpin.CmdClause) error {
31 | return nil
32 | })
33 |
34 | command.Action(func(context *kingpin.ParseContext) error {
35 | var parsedArgs []string
36 | if len(*jsonEncodedArgs) == 0 {
37 | parsedArgs = make([]string, 0)
38 | } else {
39 | err := jsoniter.UnmarshalFromString(*jsonEncodedArgs, &parsedArgs)
40 | if err != nil {
41 | return err
42 | }
43 | }
44 |
45 | return ExecWine(*ia32Name, *x64Name, parsedArgs)
46 | })
47 | }
48 |
49 | func isMacOsCatalina() (bool, error) {
50 | osRelease, err := exec.Command("uname", "-r").Output()
51 | if err != nil {
52 | return false, err
53 | }
54 |
55 | return version.Compare(strings.TrimSpace(string(osRelease)), "19.0.0", ">="), nil
56 | }
57 |
58 | //noinspection GoUnusedParameter
59 | func ExecWine(ia32Name string, ia64Name string, args []string) error {
60 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
61 | defer cancel()
62 |
63 | useSystemWine := util.IsEnvTrue("USE_SYSTEM_WINE")
64 | if useSystemWine {
65 | log.Debug("using system wine is forced")
66 | }
67 |
68 | if util.GetCurrentOs() == util.MAC {
69 | return executeMacOsWine(useSystemWine, ctx, args, ia32Name, ia64Name)
70 | }
71 |
72 | err := checkWineVersion()
73 | if err != nil {
74 | return err
75 | }
76 |
77 | args = append([]string{ia32Name}, args...)
78 | _, err = util.Execute(exec.CommandContext(ctx, "wine", args...))
79 | if err != nil {
80 | return err
81 | }
82 |
83 | return nil
84 | }
85 |
86 | func executeMacOsWine(useSystemWine bool, ctx context.Context, args []string, ia32Name string, ia64Name string) error {
87 | catalina, err := isMacOsCatalina()
88 | if err != nil {
89 | log.Warn("cannot detect macOS version", zap.Error(err))
90 | }
91 |
92 | if catalina {
93 | if len(ia64Name) == 0 {
94 | return errors.New("macOS Catalina doesn't support 32-bit executables and as result Wine cannot run Windows 32-bit applications too")
95 | }
96 |
97 | args = append([]string{ia64Name}, args...)
98 | } else {
99 | args = append([]string{ia32Name}, args...)
100 | }
101 |
102 | if useSystemWine {
103 | command := exec.CommandContext(ctx, "wine", args...)
104 | env := os.Environ()
105 | env = append(env,
106 | fmt.Sprintf("WINEDEBUG=%s", "-all,err+all"),
107 | fmt.Sprintf("WINEDLLOVERRIDES=%s", "winemenubuilder.exe=d"),
108 | )
109 | command.Env = env
110 |
111 | if _, err := util.Execute(command); err != nil {
112 | return err
113 | }
114 |
115 | return nil
116 | }
117 |
118 | var wineDir string
119 | var wineExecutable string
120 | if catalina {
121 | dirName := "wine-4.0.1-mac"
122 | //noinspection SpellCheckingInspection
123 | checksum := "aCUQOyuPGlEvLMp0lPzb54D96+8IcLwmKTMElrZZqVWtEL1LQC7L9XpPv4RqaLX3BOeSifneEi4j9DpYdC1DCA=="
124 | wineDir, err = download.DownloadArtifact(dirName, download.GetGithubBaseUrl()+dirName+"/"+dirName+".7z", checksum)
125 | if err != nil {
126 | return err
127 | }
128 |
129 | wineExecutable = "wine64"
130 | } else {
131 | dirName := "wine-2.0.3-mac-10.13"
132 | //noinspection SpellCheckingInspection
133 | checksum := "dlEVCf0YKP5IEiOKPNE48Q8NKXbXVdhuaI9hG2oyDEay2c+93PE5qls7XUbIYq4Xi1gRK8fkWeCtzN2oLpVQtg=="
134 | wineDir, err = download.DownloadArtifact(dirName, download.GetGithubBaseUrl()+dirName+"/"+dirName+".7z", checksum)
135 | if err != nil {
136 | return err
137 | }
138 |
139 | wineExecutable = "wine"
140 | }
141 | command := exec.CommandContext(ctx, filepath.Join(wineDir, "bin", wineExecutable), args...)
142 | env := os.Environ()
143 | //noinspection SpellCheckingInspection
144 | env = append(env,
145 | fmt.Sprintf("WINEDEBUG=%s", "-all,err+all"),
146 | fmt.Sprintf("WINEDLLOVERRIDES=%s", "winemenubuilder.exe=d"),
147 | "WINEPREFIX=" + filepath.Join(wineDir, "wine-home"),
148 | fmt.Sprintf("DYLD_FALLBACK_LIBRARY_PATH=%s", filepath.Join(wineDir, "lib")+":"+os.Getenv("DYLD_FALLBACK_LIBRARY_PATH")),
149 | )
150 |
151 | //if catalina && len(ia64Name) == 0 {
152 | // //noinspection SpellCheckingInspection
153 | // env = append(env,
154 | // "WINEARCH=win32",
155 | // "WINEPREFIX="+filepath.Join(wineDir, "wine-home-ia32"),
156 | // )
157 | //} else {
158 | env = append(env, "WINEPREFIX="+filepath.Join(wineDir, "wine-home"))
159 | //}
160 | command.Env = env
161 | _, err = util.Execute(command)
162 | if err != nil {
163 | return err
164 | }
165 | return nil
166 | }
167 |
168 | func checkWineVersion() error {
169 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
170 | defer cancel()
171 |
172 | wineVersionResult, err := exec.CommandContext(ctx, "wine", "--version").Output()
173 | if err != nil {
174 | log.Debug("wine version check result", zap.Error(err))
175 | return util.NewMessageError("wine is required, please see https://electron.build/multi-platform-build#linux", "ERR_WINE_NOT_INSTALLED")
176 | }
177 | return doCheckWineVersion(strings.TrimPrefix(strings.TrimSpace(string(wineVersionResult)), "wine-"))
178 | }
179 |
180 | func doCheckWineVersion(wineVersion string) error {
181 | spaceIndex := strings.IndexRune(wineVersion, ' ')
182 | if spaceIndex > 0 {
183 | wineVersion = wineVersion[0:spaceIndex]
184 | }
185 |
186 | suffixIndex := strings.IndexRune(wineVersion, '-')
187 | if suffixIndex > 0 {
188 | wineVersion = wineVersion[0:suffixIndex]
189 | }
190 |
191 | if version.Compare(wineVersion, "1.8.0", "<") {
192 | return util.NewMessageError(`wine 1.8+ is required, but your version is `+wineVersion, "ERR_WINE_VERSION_INCOMPATIBLE")
193 | }
194 | return nil
195 | }
196 |
--------------------------------------------------------------------------------
/pkg/wine/wine_test.go:
--------------------------------------------------------------------------------
1 | package wine
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/gomega"
7 | )
8 |
9 | func TestCheckWineVersion(t *testing.T) {
10 | g := NewGomegaWithT(t)
11 |
12 | err := doCheckWineVersion("1.9.23 (Staging)")
13 | g.Expect(err).NotTo(HaveOccurred())
14 |
15 | err = doCheckWineVersion("2.0-rc2")
16 | g.Expect(err).NotTo(HaveOccurred())
17 |
18 | err = doCheckWineVersion("3")
19 | g.Expect(err).NotTo(HaveOccurred())
20 |
21 | err = doCheckWineVersion("3.5")
22 | g.Expect(err).NotTo(HaveOccurred())
23 |
24 | err = doCheckWineVersion("1.7")
25 | g.Expect(err).To(HaveOccurred())
26 | }
27 |
28 | //noinspection GoUnusedFunction
29 | func TestCheckWineVersionReal(t *testing.T) {
30 | t.SkipNow()
31 |
32 | g := NewGomegaWithT(t)
33 |
34 | err := checkWineVersion()
35 | g.Expect(err).To(HaveOccurred())
36 | }
37 |
--------------------------------------------------------------------------------
/pkg/zap-cli-encoder/arrayEncoder.go:
--------------------------------------------------------------------------------
1 | package zap_cli_encoder
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "go.uber.org/zap/buffer"
8 | "go.uber.org/zap/zapcore"
9 | )
10 |
11 | type bufferArrayEncoder struct {
12 | buffer *buffer.Buffer
13 | }
14 |
15 | func (t *bufferArrayEncoder) AppendComplex128(v complex128) {
16 | r, i := real(v), imag(v)
17 | t.buffer.AppendFloat(r, 64)
18 | t.buffer.AppendByte('+')
19 | t.buffer.AppendFloat(i, 64)
20 | t.buffer.AppendByte('i')
21 | }
22 |
23 | func (t *bufferArrayEncoder) AppendComplex64(v complex64) {
24 | //noinspection GoRedundantConversion
25 | t.AppendComplex128(complex128(v))
26 | }
27 |
28 | func (t *bufferArrayEncoder) AppendArray(v zapcore.ArrayMarshaler) error {
29 | enc := &bufferArrayEncoder{}
30 | err := v.MarshalLogArray(enc)
31 | _, _ = fmt.Fprintf(t.buffer, "%v", enc.buffer)
32 | return err
33 | }
34 |
35 | func (t *bufferArrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error {
36 | m := zapcore.NewMapObjectEncoder()
37 | err := v.MarshalLogObject(m)
38 | _, _ = fmt.Fprintf(t.buffer, "%v", m.Fields)
39 | return err
40 | }
41 |
42 | func (t *bufferArrayEncoder) AppendReflected(v interface{}) error {
43 | _, _ = fmt.Fprintf(t.buffer, "%v", v)
44 | return nil
45 | }
46 |
47 | func (t *bufferArrayEncoder) AppendBool(v bool) {
48 | t.buffer.AppendBool(v)
49 | }
50 |
51 | func (t *bufferArrayEncoder) AppendByteString(v []byte) {
52 | t.buffer.AppendString(string(v))
53 | }
54 |
55 | func (t *bufferArrayEncoder) AppendDuration(v time.Duration) {
56 | t.AppendString(v.String())
57 | }
58 |
59 | func (t *bufferArrayEncoder) AppendFloat64(v float64) { t.buffer.AppendFloat(v, 64) }
60 | func (t *bufferArrayEncoder) AppendFloat32(v float32) { t.buffer.AppendFloat(float64(v), 32) }
61 | func (t *bufferArrayEncoder) AppendInt(v int) { t.buffer.AppendInt(int64(v)) }
62 | func (t *bufferArrayEncoder) AppendInt64(v int64) { t.buffer.AppendInt(v) }
63 | func (t *bufferArrayEncoder) AppendInt32(v int32) { t.buffer.AppendInt(int64(v)) }
64 | func (t *bufferArrayEncoder) AppendInt16(v int16) { t.buffer.AppendInt(int64(v)) }
65 | func (t *bufferArrayEncoder) AppendInt8(v int8) { t.buffer.AppendInt(int64(v)) }
66 | func (t *bufferArrayEncoder) AppendString(v string) { t.buffer.AppendString(v) }
67 | func (t *bufferArrayEncoder) AppendTime(v time.Time) { t.buffer.AppendString(v.String()) }
68 | func (t *bufferArrayEncoder) AppendUint(v uint) { t.buffer.AppendUint(uint64(v)) }
69 | func (t *bufferArrayEncoder) AppendUint64(v uint64) { t.buffer.AppendUint(v) }
70 | func (t *bufferArrayEncoder) AppendUint32(v uint32) { t.buffer.AppendUint(uint64(v)) }
71 | func (t *bufferArrayEncoder) AppendUint16(v uint16) { t.buffer.AppendUint(uint64(v)) }
72 | func (t *bufferArrayEncoder) AppendUint8(v uint8) { t.buffer.AppendUint(uint64(v)) }
73 | func (t *bufferArrayEncoder) AppendUintptr(v uintptr) { t.buffer.AppendUint(uint64(v)) }
74 |
--------------------------------------------------------------------------------
/pkg/zap-cli-encoder/consoleEncoder_test.go:
--------------------------------------------------------------------------------
1 | package zap_cli_encoder
2 |
3 | import (
4 | "strings"
5 | "testing"
6 |
7 | . "github.com/onsi/gomega"
8 | "go.uber.org/zap/buffer"
9 | )
10 |
11 | func TestAppend(t *testing.T) {
12 | var linePool = buffer.NewPool()
13 | buf := linePool.Get()
14 | appendPaddedString("a\nb", buf)
15 |
16 | g := NewGomegaWithT(t)
17 | g.Expect(buf.String()).To(Equal("a\n" + strings.Repeat(" ", levelIndicatorRuneCount) + "b"))
18 | }
19 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # app-builder
2 |
3 | Generic helper tool to build app in a distributable formats.
4 | Used by [electron-builder](http://github.com/electron-userland/electron-builder) but applicable not only for building Electron applications.
5 |
6 | ```
7 | usage: app-builder [] [ ...]
8 |
9 | app-builder
10 |
11 | Flags:
12 | --help Show context-sensitive help (also try --help-long and --help-man).
13 | --version Show application version.
14 |
15 | Commands:
16 | help [...]
17 | Show help.
18 |
19 |
20 | blockmap --input=INPUT []
21 | Generates file block map for differential update using content defined
22 | chunking (that is robust to insertions, deletions, and changes to input
23 | file)
24 |
25 | -i, --input=INPUT input file
26 | -o, --output=OUTPUT output file
27 | -c, --compression=gzip compression, one of: gzip, deflate
28 |
29 | download --url=URL --output=OUTPUT []
30 | Download file.
31 |
32 | -u, --url=URL The URL.
33 | -o, --output=OUTPUT The output file.
34 | --sha512=SHA512 The expected sha512 of file.
35 |
36 | download-artifact --name=NAME --url=URL []
37 | Download, unpack and cache artifact from GitHub.
38 |
39 | -n, --name=NAME The artifact name.
40 | -u, --url=URL The artifact URL.
41 | --sha512=SHA512 The expected sha512 of file.
42 |
43 | copy --from=FROM --to=TO []
44 | Copy file or dir.
45 |
46 | -f, --from=FROM
47 | -t, --to=TO
48 | --hard-link Whether to use hard-links if possible
49 |
50 | appimage --app=APP --stage=STAGE --output=OUTPUT []
51 | Build AppImage.
52 |
53 | -a, --app=APP The app dir.
54 | -s, --stage=STAGE The stage dir.
55 | -o, --output=OUTPUT The output file.
56 | --arch=x64 The arch.
57 | --compression=COMPRESSION The compression.
58 | --remove-stage Whether to remove stage after build.
59 |
60 | snap --app=APP --stage=STAGE --output=OUTPUT []
61 | Build snap.
62 |
63 | -t, --template=TEMPLATE The template file.
64 | -u, --template-url=TEMPLATE-URL
65 | The template archive URL.
66 | --template-sha512=TEMPLATE-SHA512
67 | The expected sha512 of template archive.
68 | -a, --app=APP The app dir.
69 | -s, --stage=STAGE The stage dir.
70 | --icon=ICON The path to the icon.
71 | --hooks=HOOKS The hooks dir.
72 | --arch=amd64 The arch.
73 | -o, --output=OUTPUT The output file.
74 | --docker-image="snapcore/snapcraft:latest"
75 | The docker image.
76 | --docker Whether to use Docker.
77 | --remove-stage Whether to remove stage after build.
78 |
79 | icon --input=INPUT --format=FORMAT --out=OUT []
80 | create ICNS or ICO or icon set from PNG files
81 |
82 | -i, --input=INPUT ... input directory or file
83 | -f, --format=FORMAT output format
84 | --out=OUT output directory
85 | -r, --root=ROOT ... base directory to resolve relative path
86 |
87 | dmg --volume=VOLUME []
88 | Build dmg.
89 |
90 | --volume=VOLUME
91 | --icon=ICON
92 | --background=BACKGROUND
93 | ```
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -ex
3 |
4 | rm -rf win
5 | rm -rf mac
6 | rm -rf linux
7 |
8 | mkdir mac
9 | GOOS=darwin GOARCH=amd64 go build -ldflags='-s -w' -o mac/app-builder_amd64
10 | GOOS=darwin GOARCH=arm64 go build -ldflags='-s -w' -o mac/app-builder_arm64
11 | ln -s app-builder_amd64 mac/app-builder
12 |
13 | mkdir -p linux/ia32
14 | GOOS=linux GOARCH=386 go build -ldflags='-s -w' -o linux/ia32/app-builder
15 |
16 | mkdir -p linux/x64
17 | GOOS=linux GOARCH=amd64 go build -ldflags='-s -w' -o linux/x64/app-builder
18 |
19 | mkdir -p linux/riscv64
20 | GOOS=linux GOARCH=riscv64 go build -ldflags='-s -w' -o linux/riscv64/app-builder
21 |
22 | mkdir -p linux/arm
23 | GOOS=linux GOARCH=arm go build -ldflags='-s -w' -o linux/arm/app-builder
24 |
25 | mkdir -p linux/arm64
26 | GOOS=linux GOARCH=arm64 go build -ldflags='-s -w' -o linux/arm64/app-builder
27 |
28 | mkdir -p linux/loong64
29 | GOOS=linux GOARCH=loong64 go build -ldflags='-s -w' -o linux/loong64/app-builder
30 |
31 | mkdir -p win/ia32
32 | # $env:GOARCH='386'; go build -o win/ia32/app-builder.exe
33 | GOOS=windows GOARCH=386 go build -o win/ia32/app-builder.exe
34 |
35 | mkdir -p win/x64
36 | # $env:GOARCH='amd64'; go build -o win/x64/app-builder.exe
37 | GOOS=windows GOARCH=amd64 go build -o win/x64/app-builder.exe
38 |
39 | mkdir -p win/arm64
40 | # $env:GOARCH='arm64'; go build -o win/arm64/app-builder.exe
41 | GOOS=windows GOARCH=arm64 go build -o win/arm64/app-builder.exe
42 |
43 | find mac/ win/ linux/ -type f -exec chmod +x {} \;
44 |
--------------------------------------------------------------------------------
/testData/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/develar/app-builder/7004925f95d8f034fc88d7e782c9aa7583debb8e/testData/512x512.png
--------------------------------------------------------------------------------
/testData/icon-jpeg2.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/develar/app-builder/7004925f95d8f034fc88d7e782c9aa7583debb8e/testData/icon-jpeg2.icns
--------------------------------------------------------------------------------
/testData/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/develar/app-builder/7004925f95d8f034fc88d7e782c9aa7583debb8e/testData/icon.icns
--------------------------------------------------------------------------------
/testData/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/develar/app-builder/7004925f95d8f034fc88d7e782c9aa7583debb8e/testData/icon.ico
--------------------------------------------------------------------------------
/testData/info.json:
--------------------------------------------------------------------------------
1 | {"metadata":{"name":"Onshape","version":"0.5.19","license":"MIT","description":"Onshape desktop app (web application shell)","author":{"name":"Vladimir Krivosheev","email":"develar@gmail.com"},"main":"./out/index.js","build":{"appId":"org.develar.onshape","files":["out"],"mac":{"category":"public.app-category.graphics-design"},"nsis":{"createDesktopShortcut":"always"},"dmg":{"contents":[{"x":110,"y":150},{"x":240,"y":150,"type":"link","path":"/Applications"}]},"linux":{"category":"Graphics"},"npmRebuild":"false"},"dependencies":{"configstore":"^3.1.2","electron-debug":"^2.1.3","electron-is-dev":"^0.3.0","electron-log":"^2.2.16","electron-updater":"^2.23.2","keytar":"^4.2.1"},"devDependencies":{"@types/debug":"^0.0.30","electron":"2.0.3","electron-builder":"^20.17.1","rimraf":"^2.6.2","typescript":"^2.9.2"},"_id":"Onshape@0.5.19"},"configuration":{"directories":{"output":"dist","buildResources":"build"},"appId":"org.develar.onshape","files":["out"],"mac":{"category":"public.app-category.graphics-design"},"nsis":{"createDesktopShortcut":"always"},"dmg":{"contents":[{"x":110,"y":150},{"x":240,"y":150,"type":"link","path":"/Applications"}]},"linux":{"category":"Graphics"},"npmRebuild":false,"electronVersion":"2.0.3"},"repositoryInfo":{"type":"github","domain":"github.com","browsefiletemplate":"https://{domain}/{user}/{project}/{treepath}/{committish}/{path}{#fragment}","user":"develar","auth":null,"project":"onshape-desktop-shell"},"buildResourceDirName":"build"}
2 |
--------------------------------------------------------------------------------