├── .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 | 26 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 9 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 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 | --------------------------------------------------------------------------------