Repository: crabnebula-dev/cargo-packager Branch: main Commit: 37a538e76608 Files: 273 Total size: 896.3 KB Directory structure: gitextract_z80xgfak/ ├── .changes/ │ ├── config.json │ └── readme.md ├── .github/ │ └── workflows/ │ ├── audit.yml │ ├── build-examples.yml │ ├── check-nodejs-bindings.yml │ ├── check.yml │ ├── covector-status.yml │ ├── covector-version-or-publish.yml │ ├── integration-tests.yml │ ├── publish-packager-nodejs.yml │ ├── publish-packager-resource-resolver-nodejs.yml │ └── publish-updater-nodejs.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── Cargo.toml ├── LICENSE.spdx ├── LICENSE_APACHE-2.0 ├── LICENSE_MIT ├── README.md ├── SECURITY.md ├── bindings/ │ ├── packager/ │ │ └── nodejs/ │ │ ├── .cargo/ │ │ │ └── config.toml │ │ ├── .npmignore │ │ ├── .npmrc │ │ ├── CHANGELOG.md │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── __test__/ │ │ │ └── index.spec.mjs │ │ ├── build.rs │ │ ├── generate-config-type.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── npm/ │ │ │ ├── darwin-arm64/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── darwin-x64/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-arm-gnueabihf/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-arm64-gnu/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-arm64-musl/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-x64-gnu/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-x64-musl/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── win32-arm64-msvc/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── win32-ia32-msvc/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ └── win32-x64-msvc/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── package.json │ │ ├── packager.js │ │ ├── schema.json │ │ ├── src/ │ │ │ └── lib.rs │ │ ├── src-ts/ │ │ │ ├── config.d.ts │ │ │ ├── index.ts │ │ │ └── plugins/ │ │ │ ├── electron/ │ │ │ │ ├── index.ts │ │ │ │ └── prune.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── resource-resolver/ │ │ └── nodejs/ │ │ ├── .cargo/ │ │ │ └── config.toml │ │ ├── .npmignore │ │ ├── CHANGELOG.md │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── __test__/ │ │ │ └── index.spec.mjs │ │ ├── build.rs │ │ ├── fix-types.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── npm/ │ │ │ ├── darwin-arm64/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── darwin-x64/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-arm-gnueabihf/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-arm64-gnu/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-arm64-musl/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-x64-gnu/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── linux-x64-musl/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── win32-arm64-msvc/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── win32-ia32-msvc/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ └── win32-x64-msvc/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── package.json │ │ └── src/ │ │ └── lib.rs │ └── updater/ │ └── nodejs/ │ ├── .cargo/ │ │ └── config.toml │ ├── .npmignore │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── __test__/ │ │ ├── app/ │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ ├── package.json │ │ │ └── preload.js │ │ └── index.spec.mjs │ ├── build.rs │ ├── index.d.ts │ ├── index.js │ ├── npm/ │ │ ├── darwin-arm64/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── darwin-x64/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── linux-arm-gnueabihf/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── linux-arm64-gnu/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── linux-arm64-musl/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── linux-x64-gnu/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── linux-x64-musl/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── win32-arm64-msvc/ │ │ │ ├── README.md │ │ │ └── package.json │ │ ├── win32-ia32-msvc/ │ │ │ ├── README.md │ │ │ └── package.json │ │ └── win32-x64-msvc/ │ │ ├── README.md │ │ └── package.json │ ├── package.json │ └── src/ │ ├── from_impls.rs │ └── lib.rs ├── crates/ │ ├── config-schema-generator/ │ │ ├── Cargo.toml │ │ ├── build.rs │ │ └── src/ │ │ └── main.rs │ ├── packager/ │ │ ├── CHANGELOG.md │ │ ├── Cargo.toml │ │ ├── LICENSE_APACHE-2.0 │ │ ├── LICENSE_MIT │ │ ├── README.md │ │ ├── schema.json │ │ └── src/ │ │ ├── bin/ │ │ │ └── cargo-packager.rs │ │ ├── cli/ │ │ │ ├── config.rs │ │ │ ├── error.rs │ │ │ ├── mod.rs │ │ │ └── signer/ │ │ │ ├── generate.rs │ │ │ ├── mod.rs │ │ │ └── sign.rs │ │ ├── codesign/ │ │ │ ├── macos.rs │ │ │ ├── mod.rs │ │ │ └── windows.rs │ │ ├── config/ │ │ │ ├── builder.rs │ │ │ ├── category.rs │ │ │ └── mod.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── package/ │ │ │ ├── app/ │ │ │ │ └── mod.rs │ │ │ ├── appimage/ │ │ │ │ ├── appimage │ │ │ │ └── mod.rs │ │ │ ├── context.rs │ │ │ ├── deb/ │ │ │ │ ├── main.desktop │ │ │ │ └── mod.rs │ │ │ ├── dmg/ │ │ │ │ ├── eula-resources-template.xml │ │ │ │ ├── mod.rs │ │ │ │ └── template.applescript │ │ │ ├── mod.rs │ │ │ ├── nsis/ │ │ │ │ ├── FileAssociation.nsh │ │ │ │ ├── installer.nsi │ │ │ │ ├── languages/ │ │ │ │ │ ├── Arabic.nsh │ │ │ │ │ ├── Bulgarian.nsh │ │ │ │ │ ├── Dutch.nsh │ │ │ │ │ ├── English.nsh │ │ │ │ │ ├── French.nsh │ │ │ │ │ ├── Japanese.nsh │ │ │ │ │ ├── Korean.nsh │ │ │ │ │ ├── Persian.nsh │ │ │ │ │ ├── PortugueseBR.nsh │ │ │ │ │ ├── SimpChinese.nsh │ │ │ │ │ ├── Spanish.nsh │ │ │ │ │ ├── SpanishInternational.nsh │ │ │ │ │ ├── Swedish.nsh │ │ │ │ │ ├── TradChinese.nsh │ │ │ │ │ └── Turkish.nsh │ │ │ │ └── mod.rs │ │ │ ├── pacman/ │ │ │ │ └── mod.rs │ │ │ └── wix/ │ │ │ ├── default-locale-strings.xml │ │ │ ├── languages.json │ │ │ ├── main.wxs │ │ │ └── mod.rs │ │ ├── shell.rs │ │ ├── sign.rs │ │ └── util.rs │ ├── resource-resolver/ │ │ ├── CHANGELOG.md │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ ├── error.rs │ │ └── lib.rs │ ├── updater/ │ │ ├── CHANGELOG.md │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── src/ │ │ │ ├── custom_serialization.rs │ │ │ ├── error.rs │ │ │ └── lib.rs │ │ └── tests/ │ │ ├── app/ │ │ │ ├── Cargo.toml │ │ │ └── src/ │ │ │ └── main.rs │ │ ├── dummy.key │ │ ├── dummy.pub.key │ │ └── update.rs │ └── utils/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ └── src/ │ ├── current_exe.rs │ └── lib.rs ├── deny.toml ├── examples/ │ ├── deno/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── deno-example.js │ │ └── packager.json │ ├── dioxus/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ ├── egui/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src/ │ │ └── main.rs │ ├── electron/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── main.js │ │ ├── package.json │ │ └── preload.js │ ├── slint/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── build.rs │ │ ├── src/ │ │ │ └── main.rs │ │ └── ui/ │ │ └── appwindow.slint │ ├── tauri/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── build.rs │ │ ├── capabilities/ │ │ │ └── base.json │ │ ├── dummy.key │ │ ├── dummy.pub.key │ │ ├── icons/ │ │ │ └── icon.icns │ │ ├── src/ │ │ │ └── main.rs │ │ ├── src-ui/ │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ └── styles.css │ │ └── tauri.conf.json │ ├── wails/ │ │ ├── .gitignore │ │ ├── Packager.toml │ │ ├── README.md │ │ ├── app.go │ │ ├── build.sh │ │ ├── frontend/ │ │ │ ├── app.css │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ ├── style.css │ │ │ └── wailsjs/ │ │ │ ├── go/ │ │ │ │ └── main/ │ │ │ │ ├── App.d.ts │ │ │ │ └── App.js │ │ │ └── runtime/ │ │ │ ├── package.json │ │ │ ├── runtime.d.ts │ │ │ └── runtime.js │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── wails.json │ └── wry/ │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── main.rs ├── package.json ├── pnpm-workspace.yaml └── renovate.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .changes/config.json ================================================ { "gitSiteUrl": "https://www.github.com/crabnebula-dev/cargo-packager", "timeout": 3600000, "pkgManagers": { "rust": { "version": true, "getPublishedVersion": { "use": "fetch:check", "options": { "url": "https://crates.io/api/v1/crates/${ pkg.pkg }/${ pkg.pkgFile.version }" } }, "postversion": [ { "command": "cargo generate-lockfile", "dryRunCommand": true, "runFromRoot": true, "pipe": false } ], "prepublish": [ { "command": "cargo generate-lockfile", "dryRunCommand": true, "runFromRoot": true, "pipe": false } ], "publish": [ { "command": "echo '
\n

Cargo Publish

\n\n```'", "dryRunCommand": true, "pipe": true }, { "command": "cargo publish", "dryRunCommand": "cargo publish --dry-run", "pipe": true }, { "command": "echo '```\n\n
\n'", "dryRunCommand": true, "pipe": true } ], "postpublish": [ "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor } -f", "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor }.${ pkgFile.versionMinor } -f", "git push --tags -f" ] }, "javascript": { "version": true, "getPublishedVersion": { "use": "fetch:check", "options": { "url": "https://registry.npmjs.com/${ pkg.pkg }/${ pkg.pkgFile.version }" } }, "prepublish": [ { "command": "pnpm install", "dryRunCommand": true }, { "command": "echo '
\n

PNPM Audit

\n\n```'", "dryRunCommand": true, "pipe": true }, { "command": "pnpm audit", "dryRunCommand": true, "runFromRoot": true, "pipe": true }, { "command": "echo '```\n\n
\n'", "dryRunCommand": true, "pipe": true }, { "command": "npm pack", "dryRunCommand": true } ], "publish": [ "sleep 15s", { "command": "echo '
\n

PNPM Publish

\n\n```'", "dryRunCommand": true, "pipe": true }, { "command": "pnpm publish --access public", "dryRunCommand": "npm publish --dry-run --access public", "pipe": true }, { "command": "echo '```\n\n
\n'", "dryRunCommand": true, "pipe": true } ], "postpublish": [ "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor } -f", "git tag ${ pkg.pkg }-v${ pkgFile.versionMajor }.${ pkgFile.versionMinor } -f", "git push --tags -f" ] } }, "packages": { "cargo-packager-utils": { "path": "./crates/utils", "manager": "rust" }, "cargo-packager": { "path": "./crates/packager", "manager": "rust", "dependencies": ["cargo-packager-utils"] }, "@crabnebula/packager": { "path": "./bindings/packager/nodejs", "manager": "javascript", "dependencies": ["cargo-packager", "cargo-packager-utils"], "prepublish": [], "publish": [], "postpublish": [] }, "cargo-packager-updater": { "path": "./crates/updater", "dependencies": ["cargo-packager-utils"], "manager": "rust" }, "@crabnebula/updater": { "path": "./bindings/updater/nodejs", "manager": "javascript", "dependencies": ["cargo-packager-updater", "cargo-packager-utils"], "prepublish": [], "publish": [], "postpublish": [] }, "cargo-packager-resource-resolver": { "path": "./crates/resource-resolver", "dependencies": ["cargo-packager-utils"], "manager": "rust" }, "@crabnebula/packager-resource-resolver": { "path": "./bindings/resource-resolver/nodejs", "manager": "javascript", "dependencies": [ "cargo-packager-resource-resolver", "cargo-packager-utils" ], "prepublish": [], "publish": [], "postpublish": [] } } } ================================================ FILE: .changes/readme.md ================================================ # Changes ##### via https://github.com/jbolda/covector As you create PRs and make changes that require a version bump, please add a new markdown file in this folder. You do not note the version _number_, but rather the type of bump that you expect: major, minor, or patch. The filename is not important, as long as it is a `.md`, but we recommend it represents the overall change for our sanity. When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process. Use the following format: ```md --- "cargo-packager": patch --- Change summary goes here ``` Summaries do not have a specific character limit, but are text only. These summaries are used within the (future implementation of) changelogs. They will give context to the change and also point back to the original PR if more details and context are needed. Changes will be designated as a `major`, `minor` or `patch` as further described in [semver](https://semver.org/). Given a version number MAJOR.MINOR.PATCH, increment the: - MAJOR version when you make incompatible API changes, - MINOR version when you add functionality in a backwards compatible manner, and - PATCH version when you make backwards compatible bug fixes. Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format, but will be discussed prior to usage (as extra steps will be necessary in consideration of merging and publishing). ================================================ FILE: .github/workflows/audit.yml ================================================ name: Audit Rust on: workflow_dispatch: schedule: - cron: "0 0 * * *" push: branches: - main paths: - ".github/workflows/audit.yml" - "**/Cargo.lock" - "**/Cargo.toml" pull_request: branches: - main paths: - ".github/workflows/audit.yml" - "**/Cargo.lock" - "**/Cargo.toml" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: audit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: rustsec/audit-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/build-examples.yml ================================================ name: Package examples on: pull_request: branches: - main paths: - ".github/workflows/build-examples.yml" - "crates/**" - "examples/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: package: if: ${{ !startsWith(github.head_ref, 'renovate/') }} strategy: fail-fast: false matrix: platform: [ubuntu-22.04, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v6 - name: install webkit2gtk if: matrix.platform == 'ubuntu-22.04' run: | sudo apt-get update sudo apt-get install -y webkit2gtk-4.1 webkit2gtk-4.0 libayatana-appindicator3-dev libxdo-dev - uses: dtolnay/rust-toolchain@stable - uses: actions/setup-go@v6 with: go-version: "stable" - uses: denoland/setup-deno@v1 - uses: Swatinem/rust-cache@v2 - run: go install github.com/wailsapp/wails/v2/cmd/wails@latest - run: cargo install cargo-binstall --locked - run: cargo binstall tauri-cli --locked --force - run: cargo r --package cargo-packager -- signer generate --password '123' --path ./signing-key -vvv - run: cargo r --package cargo-packager -- --release --private-key ./signing-key --password '123' --formats all -vvv ================================================ FILE: .github/workflows/check-nodejs-bindings.yml ================================================ name: Check Node.js bindings on: push: branches: - main paths: - ".github/workflows/check-nodejs-bindings.yml" - "crates/packager/**" - "crates/updater/**" - "crates/resource-resolver/**" - "bindings/*/nodejs/**" pull_request: branches: - main paths: - ".github/workflows/check-nodejs-bindings.yml" - "crates/packager/**" - "crates/updater/**" - "crates/resource-resolver/**" - "bindings/*/nodejs/**" env: RUST_BACKTRACE: 1 CARGO_PROFILE_DEV_DEBUG: 0 # This would add unnecessary bloat to the target folder, decreasing cache efficiency. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - uses: pnpm/action-setup@v4 with: version: latest - uses: actions/setup-node@v6 - uses: Swatinem/rust-cache@v2 - run: pnpm install - run: pnpm build ================================================ FILE: .github/workflows/check.yml ================================================ name: Check on: push: branches: - main paths: - ".github/workflows/check.yml" - "**/*.rs" - "**/Cargo.toml" pull_request: branches: - main paths: - ".github/workflows/check.yml" - "**/*.rs" - "**/Cargo.toml" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: prettier: if: ${{ !startsWith(github.head_ref, 'renovate/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 with: version: latest - run: pnpm install - run: pnpm format:check rustfmt: if: ${{ !startsWith(github.head_ref, 'renovate/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - run: cargo fmt --all -- --check clippy: if: ${{ !startsWith(github.head_ref, 'renovate/') }} strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable with: components: clippy - name: install webkit2gtk if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev libxdo-dev - uses: Swatinem/rust-cache@v2 - run: cargo clippy --workspace --all-targets --all-features -- -D warnings rust-test: strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v6 - name: install webkit2gtk if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y webkit2gtk-4.1 libayatana-appindicator3-dev libxdo-dev - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --lib --bins --tests --benches --all-features --no-fail-fast deny: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: EmbarkStudios/cargo-deny-action@v2 ================================================ FILE: .github/workflows/covector-status.yml ================================================ name: Covector Status on: [pull_request] jobs: covector: if: ${{ !startsWith(github.head_ref, 'renovate/') }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: covector status uses: jbolda/covector/packages/action@covector-v0 id: covector with: command: "status" ================================================ FILE: .github/workflows/covector-version-or-publish.yml ================================================ name: Covector Version or Publish on: push: branches: - main jobs: version-or-publish: if: ${{ !startsWith(github.head_ref, 'renovate/') }} runs-on: ubuntu-latest permissions: contents: write pull-requests: write timeout-minutes: 65 outputs: change: ${{ steps.covector.outputs.change }} commandRan: ${{ steps.covector.outputs.commandRan }} successfulPublish: ${{ steps.covector.outputs.successfulPublish }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: cargo login run: cargo login ${{ secrets.CRATES_IO_TOKEN }} - name: git config run: | git config --global user.name "${{ github.event.pusher.name }}" git config --global user.email "${{ github.event.pusher.email }}" - name: covector version or publish (publish when no change files present) uses: jbolda/covector/packages/action@covector-v0 id: covector with: token: ${{ secrets.GITHUB_TOKEN }} command: "version-or-publish" createRelease: true - name: Create Pull Request With Versions Bumped if: steps.covector.outputs.commandRan == 'version' uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 with: token: ${{ secrets.GITHUB_TOKEN }} branch: release/version-updates title: "release: apply version updates from current changes" commit-message: "release: apply version updates from current changes" labels: "version updates" body: ${{ steps.covector.outputs.change }} sign-commits: true - name: Trigger `@crabnebula/packager` publishing workflow if: | steps.covector.outputs.successfulPublish == 'true' && contains(steps.covector.outputs.packagesPublished, '@crabnebula/packager') uses: peter-evans/repository-dispatch@v3 with: event-type: publish-packager-nodejs client-payload: >- {"releaseId": "${{ steps.covector.outputs['-crabnebula-packager-releaseId'] }}" } - name: Trigger `@crabnebula/updater` publishing workflow if: | steps.covector.outputs.successfulPublish == 'true' && contains(steps.covector.outputs.packagesPublished, '@crabnebula/updater') uses: peter-evans/repository-dispatch@v3 with: event-type: publish-updater-nodejs client-payload: >- {"releaseId": "${{ steps.covector.outputs['-crabnebula-updater-releaseId'] }}" } - name: Trigger `@crabnebula/packager-resource-resolver` publishing workflow if: | steps.covector.outputs.successfulPublish == 'true' && contains(steps.covector.outputs.packagesPublished, '@crabnebula/packager-resource-resolver') uses: peter-evans/repository-dispatch@v3 with: event-type: publish-packager-resource-resolver-nodejs client-payload: >- {"releaseId": "${{ steps.covector.outputs['-crabnebula-packager-resource-resolver-releaseId'] }}" } ================================================ FILE: .github/workflows/integration-tests.yml ================================================ name: integration tests on: push: branches: - main pull_request: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test-rust: if: ${{ !startsWith(github.head_ref, 'renovate/') }} runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v6 - name: install fuse if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y fuse libfuse2 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo test --test '*' -- --ignored --nocapture test-nodejs: runs-on: ${{ matrix.platform }} strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@stable - uses: pnpm/action-setup@v4 with: version: latest - uses: actions/setup-node@v6 - uses: Swatinem/rust-cache@v2 - run: pnpm install - run: pnpm build - name: packager integration tests run: | cd bindings/packager/nodejs pnpm test timeout-minutes: 30 - name: install fuse if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y fuse libfuse2 - name: updater integration tests run: | cd bindings/updater/nodejs pnpm test timeout-minutes: 30 ================================================ FILE: .github/workflows/publish-packager-nodejs.yml ================================================ name: Publish `@crabnebula/packager` env: DEBUG: napi:* APP_NAME: packager MACOSX_DEPLOYMENT_TARGET: "10.13" permissions: contents: write id-token: write on: workflow_dispatch: inputs: releaseId: description: "ID of the `@crabnebula/packager` release" required: true repository_dispatch: types: [publish-packager-nodejs] defaults: run: working-directory: bindings/packager/nodejs jobs: build: strategy: fail-fast: false matrix: settings: - host: macos-latest target: x86_64-apple-darwin build: | pnpm build strip -x *.node - host: windows-latest build: pnpm build target: x86_64-pc-windows-msvc - host: windows-latest build: pnpm build --target i686-pc-windows-msvc target: i686-pc-windows-msvc - host: ubuntu-latest target: x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: | npm i -g --force corepack cd bindings/packager/nodejs set -e && pnpm build --target x86_64-unknown-linux-gnu && strip *.node - host: ubuntu-latest target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | cd bindings/packager/nodejs set -e pnpm build strip *.node - host: macos-latest target: aarch64-apple-darwin build: | pnpm build --target aarch64-apple-darwin --features native-tls-vendored --cargo-flags="--no-default-features" strip -x *.node - host: ubuntu-latest target: aarch64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 build: | npm i -g --force corepack cd bindings/packager/nodejs set -e && pnpm build --target aarch64-unknown-linux-gnu && aarch64-unknown-linux-gnu-strip *.node - host: ubuntu-latest target: armv7-unknown-linux-gnueabihf setup: | sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf -y build: | pnpm build --target armv7-unknown-linux-gnueabihf arm-linux-gnueabihf-strip *.node - host: ubuntu-latest target: aarch64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | cd bindings/packager/nodejs set -e && rustup target add aarch64-unknown-linux-musl && pnpm build --target aarch64-unknown-linux-musl && /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node - host: windows-latest target: aarch64-pc-windows-msvc build: pnpm build --target aarch64-pc-windows-msvc --features native-tls-vendored --cargo-flags="--no-default-features" name: stable - ${{ matrix.settings.target }} - node@18 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - uses: actions/setup-node@v6 if: ${{ !matrix.settings.docker }} - name: Install uses: dtolnay/rust-toolchain@stable if: ${{ !matrix.settings.docker }} with: toolchain: stable targets: ${{ matrix.settings.target }} - name: Cache cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ .cargo-cache target/ key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} - uses: goto-bus-stop/setup-zig@v2 if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} with: version: 0.11.0 - name: Setup toolchain run: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }} shell: bash - name: Setup node x86 if: matrix.settings.target == 'i686-pc-windows-msvc' run: pnpm config set supportedArchitectures.cpu "ia32" shell: bash - run: pnpm install - name: Setup node x86 uses: actions/setup-node@v6 if: matrix.settings.target == 'i686-pc-windows-msvc' with: node-version: 18 architecture: x86 - name: Build in docker uses: addnab/docker-run-action@v3 if: ${{ matrix.settings.docker }} with: image: ${{ matrix.settings.docker }} options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build" run: ${{ matrix.settings.build }} - name: Build run: ${{ matrix.settings.build }} if: ${{ !matrix.settings.docker }} shell: bash - name: Upload artifact uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: bindings/packager/nodejs/${{ env.APP_NAME }}.*.node if-no-files-found: error test-macOS-windows-binding: name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: settings: - host: macos-latest target: x86_64-apple-darwin - host: windows-latest target: x86_64-pc-windows-msvc node: - "18" - "20" runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: pnpm install - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: bindings/packager/nodejs - name: List packages run: ls -R . shell: bash - name: Test bindings run: | pnpm postbuild pnpm test test-linux-x64-gnu-binding: name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: pnpm install - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-gnu path: bindings/packager/nodejs - name: List packages run: ls -R . shell: bash - name: Test bindings working-directory: . run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim sh -c "cd bindings/packager/nodejs && yarn postbuild && PACKAGER_FORMATS=deb yarn test" test-linux-x64-musl-binding: name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - name: Install dependencies run: | pnpm config set supportedArchitectures.libc "musl" pnpm install - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-musl path: bindings/packager/nodejs - name: List packages run: ls -R . shell: bash - name: Test bindings working-directory: . run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-alpine sh -c "cd bindings/packager/nodejs && yarn postbuild && PACKAGER_FORMATS=deb yarn test" test-linux-aarch64-gnu-binding: name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-aarch64-unknown-linux-gnu path: bindings/packager/nodejs - name: List packages run: ls -R . shell: bash - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - name: Install dependencies run: | pnpm config set supportedArchitectures.cpu "arm64" pnpm config set supportedArchitectures.libc "glibc" pnpm install - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64 - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Setup and run tests uses: addnab/docker-run-action@v3 with: image: node:${{ matrix.node }}-slim options: "--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build" run: | export PACKAGER_FORMATS=deb set -e cd bindings/packager/nodejs yarn postbuild yarn test ls -la test-linux-aarch64-musl-binding: name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }} needs: - build runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-aarch64-unknown-linux-musl path: bindings/packager/nodejs - name: List packages run: ls -R . shell: bash - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - name: Install dependencies run: | pnpm config set supportedArchitectures.cpu "arm64" pnpm config set supportedArchitectures.libc "musl" pnpm install - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64 - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Setup and run tests uses: addnab/docker-run-action@v3 with: image: node:lts-alpine options: "--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build" run: | export PACKAGER_FORMATS=deb set -e cd bindings/packager/nodejs yarn postbuild yarn test test-linux-arm-gnueabihf-binding: name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-armv7-unknown-linux-gnueabihf path: bindings/packager/nodejs - name: List packages run: ls -R . shell: bash - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - name: Install dependencies run: | pnpm config set supportedArchitectures.cpu "arm" pnpm install - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Setup and run tests uses: addnab/docker-run-action@v3 with: image: node:${{ matrix.node }}-bullseye-slim options: "--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build" run: | export PACKAGER_FORMATS=deb set -e cd bindings/packager/nodejs yarn postbuild yarn test ls -la publish: name: Publish runs-on: ubuntu-latest needs: - test-macOS-windows-binding - test-linux-x64-gnu-binding - test-linux-x64-musl-binding - test-linux-aarch64-gnu-binding - test-linux-aarch64-musl-binding - test-linux-arm-gnueabihf-binding steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/packager/nodejs - uses: actions/setup-node@v6 - run: pnpm install - name: Download all artifacts uses: actions/download-artifact@v4 with: path: bindings/packager/nodejs/artifacts - name: Move artifacts run: pnpm artifacts - name: List packages run: ls -R ./npm shell: bash - name: build TS binding run: pnpm postbuild - name: Publish run: | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc npm publish --access public env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} RELEASE_ID: ${{ github.event.client_payload.releaseId || inputs.releaseId }} ================================================ FILE: .github/workflows/publish-packager-resource-resolver-nodejs.yml ================================================ name: Publish `@crabnebula/packager-resource-resolver` env: DEBUG: napi:* APP_NAME: packager-resource-resolver MACOSX_DEPLOYMENT_TARGET: "10.13" permissions: contents: write id-token: write on: workflow_dispatch: inputs: releaseId: description: "ID of the `@crabnebula/packager-resource-resolver` release" required: true repository_dispatch: types: [publish-packager-resource-resolver-nodejs] defaults: run: working-directory: bindings/resource-resolver/nodejs jobs: build: strategy: fail-fast: false matrix: settings: - host: macos-latest target: x86_64-apple-darwin build: | pnpm build strip -x *.node - host: windows-latest build: pnpm build target: x86_64-pc-windows-msvc - host: windows-latest build: pnpm build --target i686-pc-windows-msvc target: i686-pc-windows-msvc - host: ubuntu-latest target: x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: | npm i -g --force corepack cd bindings/resource-resolver/nodejs set -e && pnpm build --target x86_64-unknown-linux-gnu && strip *.node - host: ubuntu-latest target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | cd bindings/resource-resolver/nodejs set -e pnpm build strip *.node - host: macos-latest target: aarch64-apple-darwin build: | pnpm build --target aarch64-apple-darwin strip -x *.node - host: ubuntu-latest target: aarch64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 build: | npm i -g --force corepack cd bindings/resource-resolver/nodejs set -e && pnpm build --target aarch64-unknown-linux-gnu && aarch64-unknown-linux-gnu-strip *.node - host: ubuntu-latest target: armv7-unknown-linux-gnueabihf setup: | sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf -y build: | npm i -g --force corepack pnpm build --target armv7-unknown-linux-gnueabihf arm-linux-gnueabihf-strip *.node - host: ubuntu-latest target: aarch64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | npm i -g --force corepack cd bindings/resource-resolver/nodejs set -e && rustup target add aarch64-unknown-linux-musl && pnpm build --target aarch64-unknown-linux-musl && /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node - host: windows-latest target: aarch64-pc-windows-msvc build: pnpm build --target aarch64-pc-windows-msvc name: stable - ${{ matrix.settings.target }} - node@18 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - uses: actions/setup-node@v6 if: ${{ !matrix.settings.docker }} - name: Install uses: dtolnay/rust-toolchain@stable if: ${{ !matrix.settings.docker }} with: toolchain: stable targets: ${{ matrix.settings.target }} - name: Cache cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ .cargo-cache target/ key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} - uses: goto-bus-stop/setup-zig@v2 if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} with: version: 0.11.0 - name: Setup toolchain run: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }} shell: bash - name: Setup node x86 if: matrix.settings.target == 'i686-pc-windows-msvc' run: pnpm config set supportedArchitectures.cpu "ia32" shell: bash - run: pnpm install - name: Setup node x86 uses: actions/setup-node@v6 if: matrix.settings.target == 'i686-pc-windows-msvc' with: node-version: 18 architecture: x86 - name: Build in docker uses: addnab/docker-run-action@v3 if: ${{ matrix.settings.docker }} with: image: ${{ matrix.settings.docker }} options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build" run: ${{ matrix.settings.build }} - name: Build run: ${{ matrix.settings.build }} if: ${{ !matrix.settings.docker }} shell: bash - name: Upload artifact uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: bindings/resource-resolver/nodejs/${{ env.APP_NAME }}.*.node if-no-files-found: error test-macOS-windows-binding: name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: settings: - host: macos-latest target: x86_64-apple-darwin - host: windows-latest target: x86_64-pc-windows-msvc node: - "18" - "20" runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: pnpm install - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: bindings/resource-resolver/nodejs - name: List packages run: ls -R . shell: bash - name: Test bindings run: pnpm test test-linux-x64-gnu-binding: name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - run: pnpm install - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-gnu path: bindings/resource-resolver/nodejs - name: List packages run: ls -R . shell: bash - name: Test bindings working-directory: . run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim sh -c "cd bindings/resource-resolver/nodejs && yarn test" test-linux-x64-musl-binding: name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} - name: Install dependencies run: | pnpm config set supportedArchitectures.libc "musl" pnpm install - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-x86_64-unknown-linux-musl path: bindings/resource-resolver/nodejs - name: List packages run: ls -R . shell: bash - name: Test bindings working-directory: . run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-alpine sh -c "cd bindings/resource-resolver/nodejs && yarn test" test-linux-aarch64-gnu-binding: name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-aarch64-unknown-linux-gnu path: bindings/resource-resolver/nodejs - name: List packages run: ls -R . shell: bash - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - name: Install dependencies run: | pnpm config set supportedArchitectures.cpu "arm64" pnpm config set supportedArchitectures.libc "glibc" pnpm install - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64 - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Setup and run tests uses: addnab/docker-run-action@v3 with: image: node:${{ matrix.node }}-slim options: "--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build" run: | set -e cd bindings/resource-resolver/nodejs yarn test ls -la test-linux-aarch64-musl-binding: name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }} needs: - build runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-aarch64-unknown-linux-musl path: bindings/resource-resolver/nodejs - name: List packages run: ls -R . shell: bash - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - name: Install dependencies run: | pnpm config set supportedArchitectures.cpu "arm64" pnpm config set supportedArchitectures.libc "musl" pnpm install - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm64 - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Setup and run tests uses: addnab/docker-run-action@v3 with: image: node:lts-alpine options: "--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build" run: | set -e cd bindings/resource-resolver/nodejs yarn test test-linux-arm-gnueabihf-binding: name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }} needs: - build strategy: fail-fast: false matrix: node: - "18" - "20" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - name: Download artifacts uses: actions/download-artifact@v4 with: name: bindings-armv7-unknown-linux-gnueabihf path: bindings/resource-resolver/nodejs - name: List packages run: ls -R . shell: bash - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - name: Install dependencies run: | pnpm config set supportedArchitectures.cpu "arm" pnpm install - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: platforms: arm - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Setup and run tests uses: addnab/docker-run-action@v3 with: image: node:${{ matrix.node }}-bullseye-slim options: "--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build" run: | set -e cd bindings/resource-resolver/nodejs yarn test ls -la publish: name: Publish runs-on: ubuntu-latest needs: - build - test-macOS-windows-binding - test-linux-x64-gnu-binding - test-linux-x64-musl-binding - test-linux-aarch64-gnu-binding - test-linux-aarch64-musl-binding - test-linux-arm-gnueabihf-binding steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/resource-resolver/nodejs - uses: actions/setup-node@v6 - run: pnpm install - name: Download all artifacts uses: actions/download-artifact@v4 with: path: bindings/resource-resolver/nodejs/artifacts - name: Move artifacts run: pnpm artifacts - name: List packages run: ls -R ./npm shell: bash - name: Publish run: | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc npm publish --access public env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} RELEASE_ID: ${{ github.event.client_payload.releaseId || inputs.releaseId }} ================================================ FILE: .github/workflows/publish-updater-nodejs.yml ================================================ name: Publish `@crabnebula/updater` env: DEBUG: napi:* APP_NAME: updater MACOSX_DEPLOYMENT_TARGET: "10.13" permissions: contents: write id-token: write on: workflow_dispatch: inputs: releaseId: description: "ID of the `@crabnebula/updater` release" required: true repository_dispatch: types: [publish-updater-nodejs] defaults: run: working-directory: bindings/updater/nodejs jobs: build: strategy: fail-fast: false matrix: settings: - host: macos-latest target: x86_64-apple-darwin build: | pnpm build strip -x *.node - host: windows-latest build: pnpm build target: x86_64-pc-windows-msvc - host: windows-latest build: pnpm build --target i686-pc-windows-msvc target: i686-pc-windows-msvc - host: ubuntu-latest target: x86_64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: | npm i -g --force corepack cd bindings/updater/nodejs set -e && pnpm build --target x86_64-unknown-linux-gnu && strip *.node - host: ubuntu-latest target: x86_64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | cd bindings/updater/nodejs set -e pnpm build strip *.node - host: macos-latest target: aarch64-apple-darwin build: | pnpm build --target aarch64-apple-darwin --features native-tls-vendored --cargo-flags="--no-default-features" strip -x *.node - host: ubuntu-latest target: aarch64-unknown-linux-gnu docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 build: | npm i -g --force corepack cd bindings/updater/nodejs set -e && pnpm build --target aarch64-unknown-linux-gnu && aarch64-unknown-linux-gnu-strip *.node - host: ubuntu-latest target: armv7-unknown-linux-gnueabihf setup: | sudo apt-get update sudo apt-get install gcc-arm-linux-gnueabihf -y build: | pnpm build --target armv7-unknown-linux-gnueabihf arm-linux-gnueabihf-strip *.node - host: ubuntu-latest target: aarch64-unknown-linux-musl docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine build: | cd bindings/updater/nodejs set -e && rustup target add aarch64-unknown-linux-musl && pnpm build --target aarch64-unknown-linux-musl && /aarch64-linux-musl-cross/bin/aarch64-linux-musl-strip *.node - host: windows-latest target: aarch64-pc-windows-msvc build: pnpm build --target aarch64-pc-windows-msvc --features native-tls-vendored --cargo-flags="--no-default-features" name: stable - ${{ matrix.settings.target }} - node@18 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/updater/nodejs - uses: actions/setup-node@v6 if: ${{ !matrix.settings.docker }} - name: Install uses: dtolnay/rust-toolchain@stable if: ${{ !matrix.settings.docker }} with: toolchain: stable targets: ${{ matrix.settings.target }} - name: Cache cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ .cargo-cache target/ key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} - uses: goto-bus-stop/setup-zig@v2 if: ${{ matrix.settings.target == 'armv7-unknown-linux-gnueabihf' }} with: version: 0.11.0 - name: Setup toolchain run: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }} shell: bash - name: Setup node x86 if: matrix.settings.target == 'i686-pc-windows-msvc' run: pnpm config set supportedArchitectures.cpu "ia32" shell: bash - run: pnpm install - name: Setup node x86 uses: actions/setup-node@v6 if: matrix.settings.target == 'i686-pc-windows-msvc' with: node-version: 18 architecture: x86 - name: Build in docker uses: addnab/docker-run-action@v3 if: ${{ matrix.settings.docker }} with: image: ${{ matrix.settings.docker }} options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build" run: ${{ matrix.settings.build }} - name: Build run: ${{ matrix.settings.build }} if: ${{ !matrix.settings.docker }} shell: bash - name: Upload artifact uses: actions/upload-artifact@v4 with: name: bindings-${{ matrix.settings.target }} path: bindings/updater/nodejs/${{ env.APP_NAME }}.*.node if-no-files-found: error # FIXME: updater tests needs packager to be build first # so we need to figure how to build the packager here as well without duplicating # the publish-packager-nodejs workflow here # test-macOS-windows-binding: # name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} # needs: # - build # strategy: # fail-fast: false # matrix: # settings: # - host: macos-latest # target: x86_64-apple-darwin # - host: windows-latest # target: x86_64-pc-windows-msvc # node: # - "18" # - "20" # runs-on: ${{ matrix.settings.host }} # steps: # - uses: actions/checkout@v4 # - uses: pnpm/action-setup@v2 # with: # version: latest # package_json_file: bindings/updater/nodejs # - uses: actions/setup-node@v4 # with: # node-version: ${{ matrix.node }} # - run: pnpm install # - name: Download artifacts # uses: actions/download-artifact@v3 # with: # name: bindings-${{ matrix.settings.target }} # path: bindings/updater/nodejs # - name: List packages # run: ls -R . # shell: bash # - name: Test bindings # run: pnpm test # test-linux-x64-gnu-binding: # name: Test bindings on Linux-x64-gnu - node@${{ matrix.node }} # needs: # - build # strategy: # fail-fast: false # matrix: # node: # - "18" # - "20" # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - uses: pnpm/action-setup@v2 # with: # version: latest # package_json_file: bindings/updater/nodejs # - uses: actions/setup-node@v4 # with: # node-version: ${{ matrix.node }} # - run: pnpm install # - name: Download artifacts # uses: actions/download-artifact@v3 # with: # name: bindings-x86_64-unknown-linux-gnu # path: bindings/updater/nodejs # - name: List packages # run: ls -R . # shell: bash # - name: Test bindings # working-directory: . # run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim sh -c "cd bindings/updater/nodejs && yarn test" # test-linux-x64-musl-binding: # name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }} # needs: # - build # strategy: # fail-fast: false # matrix: # node: # - "18" # - "20" # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - uses: pnpm/action-setup@v2 # with: # version: latest # package_json_file: bindings/updater/nodejs # - uses: actions/setup-node@v4 # with: # node-version: ${{ matrix.node }} # - name: Install dependencies # run: | # pnpm config set supportedArchitectures.libc "musl" # pnpm install # - name: Download artifacts # uses: actions/download-artifact@v3 # with: # name: bindings-x86_64-unknown-linux-musl # path: bindings/updater/nodejs # - name: List packages # run: ls -R . # shell: bash # - name: Test bindings # working-directory: . # run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-alpine sh -c "cd bindings/updater/nodejs && yarn test" # test-linux-aarch64-gnu-binding: # name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }} # needs: # - build # strategy: # fail-fast: false # matrix: # node: # - "18" # - "20" # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - name: Download artifacts # uses: actions/download-artifact@v3 # with: # name: bindings-aarch64-unknown-linux-gnu # path: bindings/updater/nodejs # - name: List packages # run: ls -R . # shell: bash # - uses: pnpm/action-setup@v2 # with: # version: latest # package_json_file: bindings/updater/nodejs # - name: Install dependencies # run: | # pnpm config set supportedArchitectures.cpu "arm64" # pnpm config set supportedArchitectures.libc "glibc" # pnpm install # - name: Set up QEMU # uses: docker/setup-qemu-action@v3 # with: # platforms: arm64 # - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes # - name: Setup and run tests # uses: addnab/docker-run-action@v3 # with: # image: node:${{ matrix.node }}-slim # options: "--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build" # run: | # set -e # cd bindings/updater/nodejs # yarn test # ls -la # test-linux-aarch64-musl-binding: # name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }} # needs: # - build # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - name: Download artifacts # uses: actions/download-artifact@v3 # with: # name: bindings-aarch64-unknown-linux-musl # path: bindings/updater/nodejs # - name: List packages # run: ls -R . # shell: bash # - uses: pnpm/action-setup@v2 # with: # version: latest # package_json_file: bindings/updater/nodejs # - name: Install dependencies # run: | # pnpm config set supportedArchitectures.cpu "arm64" # pnpm config set supportedArchitectures.libc "musl" # pnpm install # - name: Set up QEMU # uses: docker/setup-qemu-action@v3 # with: # platforms: arm64 # - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes # - name: Setup and run tests # uses: addnab/docker-run-action@v3 # with: # image: node:lts-alpine # options: "--platform linux/arm64 -v ${{ github.workspace }}:/build -w /build" # run: | # set -e # cd bindings/updater/nodejs # yarn test # test-linux-arm-gnueabihf-binding: # name: Test bindings on armv7-unknown-linux-gnueabihf - node@${{ matrix.node }} # needs: # - build # strategy: # fail-fast: false # matrix: # node: # - "18" # - "20" # runs-on: ubuntu-latest # steps: # - uses: actions/checkout@v4 # - name: Download artifacts # uses: actions/download-artifact@v3 # with: # name: bindings-armv7-unknown-linux-gnueabihf # path: bindings/updater/nodejs # - name: List packages # run: ls -R . # shell: bash # - uses: pnpm/action-setup@v2 # with: # version: latest # package_json_file: bindings/updater/nodejs # - name: Install dependencies # run: | # pnpm config set supportedArchitectures.cpu "arm" # pnpm install # - name: Set up QEMU # uses: docker/setup-qemu-action@v3 # with: # platforms: arm # - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes # - name: Setup and run tests # uses: addnab/docker-run-action@v3 # with: # image: node:${{ matrix.node }}-bullseye-slim # options: "--platform linux/arm/v7 -v ${{ github.workspace }}:/build -w /build" # run: | # set -e # cd bindings/updater/nodejs # yarn test # ls -la publish: name: Publish runs-on: ubuntu-latest needs: - build # - test-macOS-windows-binding # - test-linux-x64-gnu-binding # - test-linux-x64-musl-binding # - test-linux-aarch64-gnu-binding # - test-linux-aarch64-musl-binding # - test-linux-arm-gnueabihf-binding steps: - uses: actions/checkout@v6 - run: npm i -g --force corepack && corepack enable - uses: pnpm/action-setup@v4 with: version: latest package_json_file: bindings/updater/nodejs - uses: actions/setup-node@v6 - run: pnpm install - name: Download all artifacts uses: actions/download-artifact@v4 with: path: bindings/updater/nodejs/artifacts - name: Move artifacts run: pnpm artifacts - name: List packages run: ls -R ./npm shell: bash - name: Publish run: | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc npm publish --access public env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} RELEASE_ID: ${{ github.event.client_payload.releaseId || inputs.releaseId }} ================================================ FILE: .gitignore ================================================ logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* node_modules/ *.tgz .yarn-integrity .env .env.test dist build .DS_Store /target Cargo.lock .pnp.* .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions *.node *.node.bak yarn.lock package-lock.json .idea ================================================ FILE: .npmrc ================================================ enable-pre-post-scripts=true update-notifier=false engine-strict=true resolution-mode=highest ================================================ FILE: .prettierignore ================================================ dist build *.wxs *.nsi *.nsh *.sh *.desktop *.xml *.md pnpm-lock.yaml schema.json bindings/updater/nodejs/index.js bindings/updater/nodejs/index.d.ts bindings/packager/nodejs/index.js bindings/packager/nodejs/index.d.ts bindings/packager/nodejs/src-ts/config.d.ts ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "crates/*", "examples/*", "bindings/*/nodejs", "crates/updater/tests/app", ] exclude = ["examples/deno", "examples/wails", "examples/electron"] resolver = "2" [workspace.package] authors = ["CrabNebula Ltd."] edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/crabnebula-dev/cargo-packager" [workspace.dependencies] thiserror = "2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dunce = "1" schemars = { version = "0.8", features = ["url", "preserve_order", "derive"] } clap = { version = "4.5", features = ["derive"] } dirs = "6.0" semver = "1" base64 = "0.22" tracing = "0.1" time = "0.3" tar = "0.4" napi = { version = "2.16", default-features = false } napi-derive = "2.16" napi-build = "2.1" [profile.release-size-optimized] inherits = "release" codegen-units = 1 lto = true incremental = false opt-level = "s" ================================================ FILE: LICENSE.spdx ================================================ SPDXVersion: SPDX-2.1 DataLicense: CC0-1.0 PackageName: cargo-packager DataFormat: SPDXRef-1 PackageSupplier: Organization: CrabNebula Ltd. PackageHomePage: https://github.com/crabnebula-dev/cargo-packager PackageLicenseDeclared: Apache-2.0 PackageLicenseDeclared: MIT PackageCopyrightText: 2023-2023, CrabNebula Ltd. PackageSummary: Rust executable packager, bundler and updater. A tool and library to generate installers or app bundles for your executables. It also has a comptabile updater through cargo-packager-updater crate. PackageComment: The package includes the following libraries; see Relationship information. Created: 2023-09-30T09:00:00Z PackageDownloadLocation: git://github.com/crabnebula-dev/cargo-packager PackageDownloadLocation: git+https://github.com/crabnebula-dev/cargo-packager.git PackageDownloadLocation: git+ssh://github.com/crabnebula-dev/cargo-packager.git ================================================ FILE: LICENSE_APACHE-2.0 ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: LICENSE_MIT ================================================ MIT License Copyright (c) 2023 - Present CrabNebula Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # cargo-packager cargo-packager splash Executable packager, bundler and updater. A cli tool and library to generate installers or app bundles for your executables. It also has a compatible updater through [cargo-packager-updater](./crates/updater/). #### Supported packages: - macOS - DMG (.dmg) - Bundle (.app) - Linux - Debian package (.deb) - AppImage (.AppImage) - Pacman (.tar.gz and PKGBUILD) - Windows - NSIS (.exe) - MSI using WiX Toolset (.msi) ## Rust ### CLI The packager is distributed on crates.io as a cargo subcommand, you can install it using cargo: ```sh cargo install cargo-packager --locked ``` You then need to configure your app so the cli can recognize it. Configuration can be done in `Packager.toml` or `packager.json` in your project or modify Cargo.toml and include this snippet: ```toml [package.metadata.packager] before-packaging-command = "cargo build --release" ``` Once, you are done configuring your app, run: ```sh cargo packager --release ``` ### Configuration By default, the packager reads its configuration from `Packager.toml` or `packager.json` if it exists, and from `package.metadata.packager` table in `Cargo.toml`. You can also specify a custom configuration using the `-c/--config` cli argument. For a full list of configuration options, see https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html. You could also use the [schema](./crates/packager/schema.json) file from GitHub to validate your configuration or have auto completions in your IDE. ### Building your application before packaging By default, the packager doesn't build your application, so if your app requires a compilation step, the packager has an option to specify a shell command to be executed before packaing your app, `beforePackagingCommand`. ### Cargo profiles By default, the packager looks for binaries built using the `debug` profile, if your `beforePackagingCommand` builds your app using `cargo build --release`, you will also need to run the packager in release mode `cargo packager --release`, otherwise, if you have a custom cargo profile, you will need to specify it using `--profile` cli arg `cargo packager --profile custom-release-profile`. ### Library This crate is also published to crates.io as a library that you can integrate into your tooling, just make sure to disable the default-feature flags. ```sh cargo add cargo-packager --no-default-features ``` #### Feature flags - **`cli`**: Enables the cli specifc features and dependencies. Enabled by default. - **`tracing`**: Enables `tracing` crate integration. ## NPM (Node.js) Checkout the packager NPM cli [README](./bindings/packager/nodejs/README.md) ## Examples The [`examples`](./examples/) directory contains a number of varying examples, if you want to build them all run `cargo r -p cargo-packager -- --release` in the root of this repository. Just make sure to have the tooling for each example installed on your system. You can find what tooling they require by checking the README in each example. The README also contains a command to build this example alone if you wish. Examples list (non-exhaustive): - [`tauri`](./examples/tauri/) - [`wry`](./examples/wry/) - [`dioxus`](./examples/dioxus/) - [`egui`](./examples/egui/) - [`deno`](./examples/deno/) - [`slint`](./examples/slint/) - [`wails`](./examples/wails) ## Licenses MIT or MIT/Apache 2.0 where applicable. ================================================ FILE: SECURITY.md ================================================ # Security Policy **Do not report security vulnerabilities through public GitHub issues.** **Please use the [Private Vulnerability Disclosure](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability) feature of GitHub.** Alternatively, you can also send them by email to security@crabnebula.dev. You can encrypt your mail using GnuPG if you want. See the [security.txt](https://crabnebula.dev/.well-known/security.txt) from CrabNebula ``` Contact: mailto:security@crabnebula.dev Expires: 2025-01-30T06:30:00.000Z Encryption: https://crabnebula.dev/.well-known/pgp.txt Preferred-Languages: en,de,fr Canonical: https://crabnebula.dev/.well-known/security.txt ``` Include as much of the following information: - Type of issue (e.g. buffer overflow, privilege escalation, etc.) - The location of the affected source code (tag/branch/commit or direct URL) - Any special configuration required to reproduce the issue - The distribution affected or used for reproduction. - Step-by-step instructions to reproduce the issue - Impact of the issue, including how an attacker might exploit the issue - Preferred Languages We prefer to receive reports in English. If necessary, we also understand French and German. ================================================ FILE: bindings/packager/nodejs/.cargo/config.toml ================================================ [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" [target.aarch64-unknown-linux-musl] linker = "aarch64-linux-musl-gcc" rustflags = ["-C", "target-feature=-crt-static"] [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" ================================================ FILE: bindings/packager/nodejs/.npmignore ================================================ target Cargo.lock .cargo .github npm .eslintrc .prettierignore rustfmt.toml yarn.lock *.node .yarn __test__ renovate.json ================================================ FILE: bindings/packager/nodejs/.npmrc ================================================ enable-pre-post-scripts=true ================================================ FILE: bindings/packager/nodejs/CHANGELOG.md ================================================ # Changelog ## \[0.11.8] - [`6e6a10c`](https://www.github.com/crabnebula-dev/cargo-packager/commit/6e6a10cc1692973293966034dc4b798e3976d094) ([#321](https://www.github.com/crabnebula-dev/cargo-packager/pull/321)) Allow explicitly specifying the Package name for the .deb bundle. - [`8488d86`](https://www.github.com/crabnebula-dev/cargo-packager/commit/8488d868935166e873474743c346c2724205d73e) ([#377](https://www.github.com/crabnebula-dev/cargo-packager/pull/377)) Fixed a bug where "binaries" parameter in Cargo.toml would be ignored and all targets would be packaged. - [`b2b4916`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b2b4916d1b062272fc7e34b5ed55b4fe8c8cd03a) ([#376](https://www.github.com/crabnebula-dev/cargo-packager/pull/376)) Fix bug that prevents reading macos signing certificates from environment variables. - [`c34de36`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c34de365705db150eb101caa94adf42eff74f71a) ([#365](https://www.github.com/crabnebula-dev/cargo-packager/pull/365)) Change nsi template from using `association.ext` to `association.extensions`, to match struct field in `FileAssociation`. This allows file associations to be generated in `.nsi` files, and therefore in the final NSIS installer. ### Dependencies - Upgraded to `cargo-packager@0.11.8` ## \[0.11.7] - [`d49b606`](https://www.github.com/crabnebula-dev/cargo-packager/commit/d49b606ba8a612c833233ec8a6061481a2118639) ([#353](https://www.github.com/crabnebula-dev/cargo-packager/pull/353)) Allow using notarization credentials stored on the Keychain by providing the `APPLE_KEYCHAIN_PROFILE` environment variable. See `xcrun notarytool store-credentials` for more information. - [`b337564`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b337564c0e5a9de966b4124890dddea1e353acb4) ([#362](https://www.github.com/crabnebula-dev/cargo-packager/pull/362)) Updated linuxdeploy's AppImage plugin to not require libfuse on the user's system anymore. ### Dependencies - Upgraded to `cargo-packager@0.11.7` ## \[0.11.6] - [`b81b81f`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b81b81fbd7fd185edfc7652f535d0cfacb786ac9) ([#354](https://www.github.com/crabnebula-dev/cargo-packager/pull/354)) Changed the download URL of a dependency of the AppImage bundler to Tauri's mirror to resolve 404 errors. - [`5205088`](https://www.github.com/crabnebula-dev/cargo-packager/commit/5205088cd78412fb6cbe5e48a715524fcc5a2ee7) ([#340](https://www.github.com/crabnebula-dev/cargo-packager/pull/340)) Enhance sign error message. ### Dependencies - Upgraded to `cargo-packager@0.11.6` ## \[0.11.5] - [`17194a9`](https://www.github.com/crabnebula-dev/cargo-packager/commit/17194a92aabd59c9e075105072ff939f5d55a107) ([#313](https://www.github.com/crabnebula-dev/cargo-packager/pull/313)) Added `linux > generateDesktopEntry` config to allow disabling generating a .desktop file on Linux bundles (defaults to true). - [`17c52f0`](https://www.github.com/crabnebula-dev/cargo-packager/commit/17c52f057d78340983689af3c00b1f2aeff3c417) ([#289](https://www.github.com/crabnebula-dev/cargo-packager/pull/289)) Added support to embedding additional apps in the macOS app bundle. - [`17c52f0`](https://www.github.com/crabnebula-dev/cargo-packager/commit/17c52f057d78340983689af3c00b1f2aeff3c417) ([#289](https://www.github.com/crabnebula-dev/cargo-packager/pull/289)) Added support to adding an `embedded.provisionprofile` file to the macOS bundle. - [`e010574`](https://www.github.com/crabnebula-dev/cargo-packager/commit/e010574c2efa4a1aa6b8e475a62bec46f24f2bc5) ([#318](https://www.github.com/crabnebula-dev/cargo-packager/pull/318)) Add `background-app` config setting for macOS to set `LSUIElement` to `true`. ### Dependencies - Upgraded to `cargo-packager@0.11.5` ## \[0.11.4] ### Dependencies - Upgraded to `cargo-packager@0.11.4` ## \[0.11.3] ### Dependencies - Upgraded to `cargo-packager@0.11.3` ## \[0.11.2] - [`fea80d5`](https://www.github.com/crabnebula-dev/cargo-packager/commit/fea80d5760882e6cdc21c8ed2f82d323e0598926) ([#264](https://www.github.com/crabnebula-dev/cargo-packager/pull/264)) Fix `pacman` package failing to install when source directory contained whitespace. ### Dependencies - Upgraded to `cargo-packager@0.11.2` ## \[0.11.1] - [`4523722`](https://www.github.com/crabnebula-dev/cargo-packager/commit/4523722d0808faef4a91dbb227badd0354f4c71a) ([#283](https://www.github.com/crabnebula-dev/cargo-packager/pull/283)) Fixes resources paths on NSIS when cross compiling. ### Dependencies - Upgraded to `cargo-packager@0.11.1` ## \[0.11.0] - [`41b05d0`](https://www.github.com/crabnebula-dev/cargo-packager/commit/41b05d08a635d593df4cf4eefbe921b92ace77b7) ([#277](https://www.github.com/crabnebula-dev/cargo-packager/pull/277)) Add `--target` flag to specify target triple to package. ### Dependencies - Upgraded to `cargo-packager@0.11.0` ## \[0.10.3] ### Dependencies - Upgraded to `cargo-packager@0.10.3` ## \[0.10.2] ### Dependencies - Upgraded to `cargo-packager@0.10.2` - Upgraded to `cargo-packager-utils@0.1.1` ## \[0.10.1] - [`522f23b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/522f23bd867b037eeec81c43295aafd38ebe60ec) ([#258](https://www.github.com/crabnebula-dev/cargo-packager/pull/258)) Update NSIS installer template URL. - [`bce99ae`](https://www.github.com/crabnebula-dev/cargo-packager/commit/bce99aecb4160291a026dcd4750055f9079099f8) ([#260](https://www.github.com/crabnebula-dev/cargo-packager/pull/260)) Fix NSIS uninstaller removing the uninstall directory even if it was not empty. ### Dependencies - Upgraded to `cargo-packager@0.10.1` ## \[0.10.0] - [`c6207bb`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c6207bba042a8a0184ddb7e12650a4cd8f415c23) ([#254](https://www.github.com/crabnebula-dev/cargo-packager/pull/254)) Allow Linux dependencies to be specified via a file path instead of just a direct String. This enables the list of dependencies to by dynamically generated for both Debian `.deb` packages and pacman packages, which can relieve the app developer from the burden of manually maintaining a fixed list of dependencies. - [`de4dcca`](https://www.github.com/crabnebula-dev/cargo-packager/commit/de4dccaca4ae758d3adde517cc415a002873e642) ([#256](https://www.github.com/crabnebula-dev/cargo-packager/pull/256)) Automatically add an Exec arg (field code) in the `.desktop` file. This adds an `{exec_arg}` field to the default `main.desktop` template. This field is populated with a sane default value, based on the `deep_link_protocols` or `file_associations` in the `Config` struct. This allows an installed Debian package to be invoked by other applications with URLs or files as arguments, as expected. ### Dependencies - Upgraded to `cargo-packager@0.10.0` ## \[0.9.1] - [`44a19ea`](https://www.github.com/crabnebula-dev/cargo-packager/commit/44a19eae1f5f26b1bd10ba84dd6eb3d856609a67) ([#246](https://www.github.com/crabnebula-dev/cargo-packager/pull/246)) On macOS, fix notarization skipping needed environment variables when macos specific config has been specified in the config file. ### Dependencies - Upgraded to `cargo-packager@0.9.1` ## \[0.9.0] - [`ab53974`](https://www.github.com/crabnebula-dev/cargo-packager/commit/ab53974b683ce282202e1a550c551eed951e9ca7) ([#235](https://www.github.com/crabnebula-dev/cargo-packager/pull/235)) Added deep link support. ### Dependencies - Upgraded to `cargo-packager@0.9.0` ## \[0.8.1] - [`1375380`](https://www.github.com/crabnebula-dev/cargo-packager/commit/1375380c7c9d2adf55ab18a2ce23917849967995)([#196](https://www.github.com/crabnebula-dev/cargo-packager/pull/196)) Always show shell commands output for `beforePackageCommand` and `beforeEachPackagingCommand` . ### Dependencies - Upgraded to `cargo-packager@0.8.1` ## \[0.8.0] - [`2164d02`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2164d022f5705e59a189007aec7c99cce98136d8)([#198](https://www.github.com/crabnebula-dev/cargo-packager/pull/198)) Allow packaging the macOS app bundle on Linux and Windows hosts (without codesign support). - [`3057a4a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3057a4a8440bc4dc897f3038ac821ed181644d43)([#197](https://www.github.com/crabnebula-dev/cargo-packager/pull/197)) Added `Config::binaries_dir` and `--binaries-dir` so you can specify the location of the binaries without modifying the output directory. - [`4c4d919`](https://www.github.com/crabnebula-dev/cargo-packager/commit/4c4d9194fb0bd2a814f46336747e643b1e208b52)([#195](https://www.github.com/crabnebula-dev/cargo-packager/pull/195)) Error out if we cannot find a configuration file. - [`b04332c`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b04332c8fc61427dc002a40d9d46bc5f930025c2)([#194](https://www.github.com/crabnebula-dev/cargo-packager/pull/194)) Fixes a crash when packaging `.app` if an empty file is included in the bundle. - [`3057a4a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3057a4a8440bc4dc897f3038ac821ed181644d43)([#197](https://www.github.com/crabnebula-dev/cargo-packager/pull/197)) Added `--out-dir/-o` flags and removed the positional argument to specify where to ouput packages, use the newly added flags instead. ### Dependencies - Upgraded to `cargo-packager@0.8.0` ## \[0.7.0] - [`cd8898a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/cd8898a93b66a4aae050fa1006089c3c3b5646f9)([#187](https://www.github.com/crabnebula-dev/cargo-packager/pull/187)) Added codesign certificate and notarization credentials configuration options under the `macos` config (for programatic usage, taking precedence over environment variables). ### Dependencies - Upgraded to `cargo-packager@0.7.0` ## \[0.6.1] ### Dependencies - Upgraded to `cargo-packager@0.6.1` ## \[0.6.0] - [`57b379a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/57b379ad1d9029e767848fda99d4eb6415afe51a)([#148](https://www.github.com/crabnebula-dev/cargo-packager/pull/148)) Added config option to control excluded libs when packaging AppImage - [`947e032`](https://www.github.com/crabnebula-dev/cargo-packager/commit/947e0328c89d6f043c3ef1b1db5d2252d4f072a5) Fix CLI failing with `Failed to read cargo metadata: cargo metadata` for non-rust projects. - Bumpt to `0.6.0` version directly to match the Rust crate version. ### Dependencies - Upgraded to `cargo-packager@0.6.0` ## \[0.2.0] - [`9bdb953`](https://www.github.com/crabnebula-dev/cargo-packager/commit/9bdb953f1b48c8d69d86e9e42295cd36453c1648)([#137](https://www.github.com/crabnebula-dev/cargo-packager/pull/137)) Add Arch Linux package manager, `pacman` support for cargo packager. - [`a29943e`](https://www.github.com/crabnebula-dev/cargo-packager/commit/a29943e8c95d70e8b77c23021ce52f6ee13314c8)([#140](https://www.github.com/crabnebula-dev/cargo-packager/pull/140)) Fix codesigning failing on macOS under certain circumstances when the order in which files were signed was not deterministic and nesting required signing files nested more deeply first. ### Dependencies - Upgraded to `cargo-packager@0.5.0` - Upgraded to `cargo-packager-utils@0.1.0` ## \[0.1.5] - [`f08e4b8`](https://www.github.com/crabnebula-dev/cargo-packager/commit/f08e4b8972b072617fdb78f11e222427e49ebe8e) Fix the signing and notarization process for MacOS bundles - [`bfa3b00`](https://www.github.com/crabnebula-dev/cargo-packager/commit/bfa3b00cf1087b2ee1e93d9c57b6b577f6294891)([#126](https://www.github.com/crabnebula-dev/cargo-packager/pull/126)) Add `priority` and `section` options in Debian config ### Dependencies - Upgraded to `cargo-packager@0.4.5` ## \[0.1.4] - [`3b3ce76`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3b3ce76da0581cf8d553d6edeb0df24f896c62a6)([#128](https://www.github.com/crabnebula-dev/cargo-packager/pull/128)) Fix file download not working on macOS and Windows (arm). ### Dependencies - Upgraded to `cargo-packager@0.4.4` ## \[0.1.3] - [`2a50c8e`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2a50c8ea734193036db0ab461f9005ea904cf4b7)([#124](https://www.github.com/crabnebula-dev/cargo-packager/pull/124)) Fix packaing of external binaries. ### Dependencies - Upgraded to `cargo-packager@0.4.3` ## \[0.1.2] - [`bd7e6fc`](https://www.github.com/crabnebula-dev/cargo-packager/commit/bd7e6fc102a74dc4da39848f44d04968b498b3cf)([#123](https://www.github.com/crabnebula-dev/cargo-packager/pull/123)) Fixes published package not including the build folder. ### Dependencies - Upgraded to `cargo-packager@0.4.2` ## \[0.1.1] - [`7e05d24`](https://www.github.com/crabnebula-dev/cargo-packager/commit/7e05d24a697230b1f53ee5ee2f7d217047089d97)([#109](https://www.github.com/crabnebula-dev/cargo-packager/pull/109)) Check if required files/tools for packaging are outdated or mis-hashed and redownload them. - [`ea6c31b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/ea6c31b1a3b56bb5408a78f1b2d6b2a2d9ce1161)([#114](https://www.github.com/crabnebula-dev/cargo-packager/pull/114)) Fix NSIS uninstaller leaving resources behind and failing to remove the installation directory. ### Dependencies - Upgraded to `cargo-packager@0.4.1` ## \[0.1.0] - [`c4fa8fd`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c4fa8fd6334b7fd0c32710ea2df0b54aa6bde713) Initial release. ### Dependencies - Upgraded to `cargo-packager@0.4.0` ================================================ FILE: bindings/packager/nodejs/Cargo.toml ================================================ [package] name = "crabnebula_packager" version = "0.0.0" publish = false edition = { workspace = true } license = { workspace = true } repository = { workspace = true } [lib] crate-type = ["cdylib"] [dependencies] # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix napi = { workspace = true, features = ["napi4"] } napi-derive = { workspace = true } cargo-packager = { path = "../../../crates/packager/", default-features = false, features = ["cli"] } tracing = { workspace = true } serde_json = { workspace = true } [build-dependencies] napi-build = { workspace = true } [features] default = ["cargo-packager/rustls-tls"] native-tls = ["cargo-packager/native-tls"] native-tls-vendored = ["cargo-packager/native-tls-vendored"] ================================================ FILE: bindings/packager/nodejs/README.md ================================================ # @crabnebula/packager Executable packager, bundler and updater. A cli tool and library to generate installers or app bundles for your executables. It also comes with useful addons: - an [updater](https://www.npmjs.com/package/@crabnebula/updater) - a [resource resolver](https://www.npmjs.com/package/@crabnebula/packager-resource-resolver) #### Supported packages: - macOS - DMG (.dmg) - Bundle (.app) - Linux - Debian package (.deb) - AppImage (.AppImage) - Pacman (.tar.gz and PKGBUILD) - Windows - NSIS (.exe) - MSI using WiX Toolset (.msi) ### CLI The packager is distributed on NPM as a CLI, you can install it: ```sh # pnpm pnpm add -D @crabnebula/packager # yarn yarn add -D @crabnebula/packager # npm npm i -D @crabnebula/packager ``` You then need to configure your app so the CLI can recognize it. Configuration can be done in `Packager.toml` or `packager.json` in your project or `packager` key in `packager.json` Once, you are done configuring your app, run: ```sh # pnpm pnpm packager # yarn yarn packager # npm npx packager ``` ### Configuration By default, the packager reads its configuration from `Packager.toml` or `packager.json` if it exists, and from `packager.json` key in `packager.json`, You can also specify a custom configuration using the `-c/--config` cli argument. For a full list of configuration options, see https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html. You could also use the [schema](./schema.json) file from GitHub to validate your configuration or have auto completions in your IDE. ### Building your application before packaging By default, the packager doesn't build your application, so if your app requires a compilation step, the packager has an option to specify a shell command to be executed before packaing your app, `beforePackagingCommand`. ### Library The packager is also a library that you can import and integrate into your tooling. ## Licenses MIT or MIT/Apache 2.0 where applicable. ================================================ FILE: bindings/packager/nodejs/__test__/index.spec.mjs ================================================ import test from "ava"; import process from "process"; import { execSync } from "child_process"; import { packageApp } from "../build/index.js"; test("log error", async (t) => { process.env.CI = true; process.chdir("../../../examples/electron"); execSync("yarn install"); t.is( await packageApp( { formats: process.env.PACKAGER_FORMATS ? process.env.PACKAGER_FORMATS.split(",") : null, }, { verbosity: 2 }, ), undefined, ); }); ================================================ FILE: bindings/packager/nodejs/build.rs ================================================ extern crate napi_build; fn main() { napi_build::setup(); } ================================================ FILE: bindings/packager/nodejs/generate-config-type.js ================================================ const { compileFromFile } = require("json-schema-to-typescript"); const fs = require("fs"); const path = require("path"); // compile from file compileFromFile( path.join(__dirname, "../../../crates/packager/schema.json"), ).then((ts) => { for (const dir of ["src-ts", "build"]) { try { fs.mkdirSync(dir); } catch (_) {} fs.writeFileSync(path.join(dir, "config.d.ts"), ts); } }); ================================================ FILE: bindings/packager/nodejs/index.d.ts ================================================ /* tslint:disable */ /* eslint-disable */ /* auto-generated by NAPI-RS */ export function cli(args: Array, binName?: string | undefined | null): void export function packageApp(config: string): void export function packageAndSignApp(config: string, signingConfig: string): void export function initTracingSubscriber(verbosity: number): void export function logError(error: string): void ================================================ FILE: bindings/packager/nodejs/index.js ================================================ /* tslint:disable */ /* eslint-disable */ /* prettier-ignore */ /* auto-generated by NAPI-RS */ const { existsSync, readFileSync } = require('fs') const { join } = require('path') const { platform, arch } = process let nativeBinding = null let localFileExisted = false let loadError = null function isMusl() { // For Node 10 if (!process.report || typeof process.report.getReport !== 'function') { try { const lddPath = require('child_process').execSync('which ldd').toString().trim() return readFileSync(lddPath, 'utf8').includes('musl') } catch (e) { return true } } else { const { glibcVersionRuntime } = process.report.getReport().header return !glibcVersionRuntime } } switch (platform) { case 'android': switch (arch) { case 'arm64': localFileExisted = existsSync(join(__dirname, 'packager.android-arm64.node')) try { if (localFileExisted) { nativeBinding = require('./packager.android-arm64.node') } else { nativeBinding = require('@crabnebula/packager-android-arm64') } } catch (e) { loadError = e } break case 'arm': localFileExisted = existsSync(join(__dirname, 'packager.android-arm-eabi.node')) try { if (localFileExisted) { nativeBinding = require('./packager.android-arm-eabi.node') } else { nativeBinding = require('@crabnebula/packager-android-arm-eabi') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Android ${arch}`) } break case 'win32': switch (arch) { case 'x64': localFileExisted = existsSync( join(__dirname, 'packager.win32-x64-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.win32-x64-msvc.node') } else { nativeBinding = require('@crabnebula/packager-win32-x64-msvc') } } catch (e) { loadError = e } break case 'ia32': localFileExisted = existsSync( join(__dirname, 'packager.win32-ia32-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.win32-ia32-msvc.node') } else { nativeBinding = require('@crabnebula/packager-win32-ia32-msvc') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync( join(__dirname, 'packager.win32-arm64-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.win32-arm64-msvc.node') } else { nativeBinding = require('@crabnebula/packager-win32-arm64-msvc') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Windows: ${arch}`) } break case 'darwin': localFileExisted = existsSync(join(__dirname, 'packager.darwin-universal.node')) try { if (localFileExisted) { nativeBinding = require('./packager.darwin-universal.node') } else { nativeBinding = require('@crabnebula/packager-darwin-universal') } break } catch {} switch (arch) { case 'x64': localFileExisted = existsSync(join(__dirname, 'packager.darwin-x64.node')) try { if (localFileExisted) { nativeBinding = require('./packager.darwin-x64.node') } else { nativeBinding = require('@crabnebula/packager-darwin-x64') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync( join(__dirname, 'packager.darwin-arm64.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.darwin-arm64.node') } else { nativeBinding = require('@crabnebula/packager-darwin-arm64') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on macOS: ${arch}`) } break case 'freebsd': if (arch !== 'x64') { throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) } localFileExisted = existsSync(join(__dirname, 'packager.freebsd-x64.node')) try { if (localFileExisted) { nativeBinding = require('./packager.freebsd-x64.node') } else { nativeBinding = require('@crabnebula/packager-freebsd-x64') } } catch (e) { loadError = e } break case 'linux': switch (arch) { case 'x64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'packager.linux-x64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-x64-musl.node') } else { nativeBinding = require('@crabnebula/packager-linux-x64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'packager.linux-x64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-x64-gnu.node') } else { nativeBinding = require('@crabnebula/packager-linux-x64-gnu') } } catch (e) { loadError = e } } break case 'arm64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'packager.linux-arm64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-arm64-musl.node') } else { nativeBinding = require('@crabnebula/packager-linux-arm64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'packager.linux-arm64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-arm64-gnu.node') } else { nativeBinding = require('@crabnebula/packager-linux-arm64-gnu') } } catch (e) { loadError = e } } break case 'arm': localFileExisted = existsSync( join(__dirname, 'packager.linux-arm-gnueabihf.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-arm-gnueabihf.node') } else { nativeBinding = require('@crabnebula/packager-linux-arm-gnueabihf') } } catch (e) { loadError = e } break case 'riscv64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'packager.linux-riscv64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-riscv64-musl.node') } else { nativeBinding = require('@crabnebula/packager-linux-riscv64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'packager.linux-riscv64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-riscv64-gnu.node') } else { nativeBinding = require('@crabnebula/packager-linux-riscv64-gnu') } } catch (e) { loadError = e } } break case 's390x': localFileExisted = existsSync( join(__dirname, 'packager.linux-s390x-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./packager.linux-s390x-gnu.node') } else { nativeBinding = require('@crabnebula/packager-linux-s390x-gnu') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Linux: ${arch}`) } break default: throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) } if (!nativeBinding) { if (loadError) { throw loadError } throw new Error(`Failed to load native binding`) } const { cli, packageApp, packageAndSignApp, initTracingSubscriber, logError } = nativeBinding module.exports.cli = cli module.exports.packageApp = packageApp module.exports.packageAndSignApp = packageAndSignApp module.exports.initTracingSubscriber = initTracingSubscriber module.exports.logError = logError ================================================ FILE: bindings/packager/nodejs/npm/darwin-arm64/README.md ================================================ # `@crabnebula/packager-darwin-arm64` This is the **aarch64-apple-darwin** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/darwin-arm64/package.json ================================================ { "name": "@crabnebula/packager-darwin-arm64", "version": "0.0.0", "os": [ "darwin" ], "cpu": [ "arm64" ], "main": "packager.darwin-arm64.node", "files": [ "packager.darwin-arm64.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/packager/nodejs/npm/darwin-x64/README.md ================================================ # `@crabnebula/packager-darwin-x64` This is the **x86_64-apple-darwin** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/darwin-x64/package.json ================================================ { "name": "@crabnebula/packager-darwin-x64", "version": "0.0.0", "os": [ "darwin" ], "cpu": [ "x64" ], "main": "packager.darwin-x64.node", "files": [ "packager.darwin-x64.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/packager/nodejs/npm/linux-arm-gnueabihf/README.md ================================================ # `@crabnebula/packager-linux-arm-gnueabihf` This is the **armv7-unknown-linux-gnueabihf** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/linux-arm-gnueabihf/package.json ================================================ { "name": "@crabnebula/packager-linux-arm-gnueabihf", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm" ], "main": "packager.linux-arm-gnueabihf.node", "files": [ "packager.linux-arm-gnueabihf.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/packager/nodejs/npm/linux-arm64-gnu/README.md ================================================ # `@crabnebula/packager-linux-arm64-gnu` This is the **aarch64-unknown-linux-gnu** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/linux-arm64-gnu/package.json ================================================ { "name": "@crabnebula/packager-linux-arm64-gnu", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm64" ], "main": "packager.linux-arm64-gnu.node", "files": [ "packager.linux-arm64-gnu.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "glibc" ] } ================================================ FILE: bindings/packager/nodejs/npm/linux-arm64-musl/README.md ================================================ # `@crabnebula/packager-linux-arm64-musl` This is the **aarch64-unknown-linux-musl** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/linux-arm64-musl/package.json ================================================ { "name": "@crabnebula/packager-linux-arm64-musl", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm64" ], "main": "packager.linux-arm64-musl.node", "files": [ "packager.linux-arm64-musl.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "musl" ] } ================================================ FILE: bindings/packager/nodejs/npm/linux-x64-gnu/README.md ================================================ # `@crabnebula/packager-linux-x64-gnu` This is the **x86_64-unknown-linux-gnu** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/linux-x64-gnu/package.json ================================================ { "name": "@crabnebula/packager-linux-x64-gnu", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "x64" ], "main": "packager.linux-x64-gnu.node", "files": [ "packager.linux-x64-gnu.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "glibc" ] } ================================================ FILE: bindings/packager/nodejs/npm/linux-x64-musl/README.md ================================================ # `@crabnebula/packager-linux-x64-musl` This is the **x86_64-unknown-linux-musl** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/linux-x64-musl/package.json ================================================ { "name": "@crabnebula/packager-linux-x64-musl", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "x64" ], "main": "packager.linux-x64-musl.node", "files": [ "packager.linux-x64-musl.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "musl" ] } ================================================ FILE: bindings/packager/nodejs/npm/win32-arm64-msvc/README.md ================================================ # `@crabnebula/packager-win32-arm64-msvc` This is the **aarch64-pc-windows-msvc** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/win32-arm64-msvc/package.json ================================================ { "name": "@crabnebula/packager-win32-arm64-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "arm64" ], "main": "packager.win32-arm64-msvc.node", "files": [ "packager.win32-arm64-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/packager/nodejs/npm/win32-ia32-msvc/README.md ================================================ # `@crabnebula/packager-win32-ia32-msvc` This is the **i686-pc-windows-msvc** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/win32-ia32-msvc/package.json ================================================ { "name": "@crabnebula/packager-win32-ia32-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "ia32" ], "main": "packager.win32-ia32-msvc.node", "files": [ "packager.win32-ia32-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/packager/nodejs/npm/win32-x64-msvc/README.md ================================================ # `@crabnebula/packager-win32-x64-msvc` This is the **x86_64-pc-windows-msvc** binary for `@crabnebula/packager` ================================================ FILE: bindings/packager/nodejs/npm/win32-x64-msvc/package.json ================================================ { "name": "@crabnebula/packager-win32-x64-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "x64" ], "main": "packager.win32-x64-msvc.node", "files": [ "packager.win32-x64-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/packager/nodejs/package.json ================================================ { "name": "@crabnebula/packager", "version": "0.11.8", "main": "build/index.js", "module": "build/index.js", "types": "build/index.d.ts", "author": { "name": "CrabNebula Ltd." }, "description": "Executable packager and bundler distributed as a CLI and library", "bin": { "packager": "./packager.js" }, "napi": { "name": "packager", "triples": { "additional": [ "aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "aarch64-pc-windows-msvc", "armv7-unknown-linux-gnueabihf", "x86_64-unknown-linux-musl", "i686-pc-windows-msvc" ] } }, "license": "MIT", "scripts": { "artifacts": "napi artifacts", "build": "napi build --platform --profile release-size-optimized", "build:debug": "napi build --platform && pnpm run postbuild", "postbuild": "rm -rf ./build && node generate-config-type.js && tsc", "prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID", "test": "ava --no-worker-threads", "universal": "napi universal", "version": "napi version" }, "dependencies": { "@electron/get": "^3.0.0", "deepmerge": "^4.3.1", "extract-zip": "^2.0.1", "fs-extra": "^11.1.1", "galactus": "^1.0.0" }, "devDependencies": { "@napi-rs/cli": "^2.18.1", "@types/fs-extra": "^11.0.3", "@types/node": "^20.8.10", "ava": "^6.2.0", "json-schema-to-typescript": "^15.0.0", "typescript": "^5.4.5" }, "ava": { "timeout": "3m" }, "engines": { "node": ">= 10" } } ================================================ FILE: bindings/packager/nodejs/packager.js ================================================ #!/usr/bin/env node const cli = require("./build"); const path = require("path"); const [bin, script, ...args] = process.argv; const binStem = path.parse(bin).name.toLowerCase(); // We want to make a helpful binary name for the underlying CLI helper, if we // can successfully detect what command likely started the execution. let binName; // deno run -A --unstable --node-modules-dir npm:@crabnebula/packager if (bin === "@crabnebula/packager") { binName = "@crabnebula/packager"; } // Even if started by a package manager, the binary will be NodeJS. // Some distribution still use "nodejs" as the binary name. else if (binStem.match(/(nodejs|node|bun)\-?([0-9]*)*$/g)) { const managerStem = process.env.npm_execpath ? path.parse(process.env.npm_execpath).name.toLowerCase() : null; if (managerStem) { let manager; switch (managerStem) { // Only supported package manager that has a different filename is npm. case "npm-cli": manager = "npm"; break; // Yarn, pnpm, and bun have the same stem name as their bin. // We assume all unknown package managers do as well. default: manager = managerStem; break; } binName = `${manager} run ${process.env.npm_lifecycle_event}`; } else { // Assume running NodeJS if we didn't detect a manager from the env. // We normalize the path to prevent the script's absolute path being used. const scriptNormal = path.normalize(path.relative(process.cwd(), script)); binName = `${binStem} ${scriptNormal}`; } } else { // We don't know what started it, assume it's already stripped. args.unshift(bin); } cli.cli(args, binName).catch((err) => { cli.logError(err.message); process.exit(1); }); ================================================ FILE: bindings/packager/nodejs/schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "description": "The packaging config.", "type": "object", "properties": { "$schema": { "description": "The JSON schema for the config.\n\nSetting this field has no effect, this just exists so we can parse the JSON correctly when it has `$schema` field set.", "type": [ "string", "null" ] }, "name": { "description": "The app name, this is just an identifier that could be used to filter which app to package using `--packages` cli arg when there is multiple apps in the workspace or in the same config.\n\nThis field resembles, the `name` field in `Cargo.toml` or `package.json`\n\nIf `unset`, the CLI will try to auto-detect it from `Cargo.toml` or `package.json` otherwise, it will keep it unset.", "type": [ "string", "null" ] }, "enabled": { "description": "Whether this config is enabled or not. Defaults to `true`.", "default": true, "type": "boolean" }, "productName": { "description": "The package's product name, for example \"My Awesome App\".", "default": "", "type": "string" }, "version": { "description": "The package's version.", "default": "", "type": "string" }, "binaries": { "description": "The binaries to package.", "default": [], "type": "array", "items": { "$ref": "#/definitions/Binary" } }, "identifier": { "description": "The application identifier in reverse domain name notation (e.g. `com.packager.example`). This string must be unique across applications since it is used in some system configurations. This string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).", "type": [ "string", "null" ], "pattern": "^[a-zA-Z0-9-\\.]*$" }, "beforePackagingCommand": { "description": "The command to run before starting to package an application.\n\nThis runs only once.", "anyOf": [ { "$ref": "#/definitions/HookCommand" }, { "type": "null" } ] }, "beforeEachPackageCommand": { "description": "The command to run before packaging each format for an application.\n\nThis will run multiple times depending on the formats specifed.", "anyOf": [ { "$ref": "#/definitions/HookCommand" }, { "type": "null" } ] }, "logLevel": { "description": "The logging level.", "anyOf": [ { "$ref": "#/definitions/LogLevel" }, { "type": "null" } ] }, "formats": { "description": "The packaging formats to create, if not present, [`PackageFormat::platform_default`] is used.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/PackageFormat" } }, "outDir": { "description": "The directory where the generated packages will be placed.\n\nIf [`Config::binaries_dir`] is not set, this is also where the [`Config::binaries`] exist.", "default": "", "type": "string" }, "binariesDir": { "description": "The directory where the [`Config::binaries`] exist.\n\nDefaults to [`Config::out_dir`].", "default": null, "type": [ "string", "null" ] }, "targetTriple": { "description": "The target triple we are packaging for.\n\nDefaults to the current OS target triple.", "type": [ "string", "null" ] }, "description": { "description": "The package's description.", "type": [ "string", "null" ] }, "longDescription": { "description": "The app's long description.", "type": [ "string", "null" ] }, "homepage": { "description": "The package's homepage.", "type": [ "string", "null" ] }, "authors": { "description": "The package's authors.", "default": null, "type": [ "array", "null" ], "items": { "type": "string" } }, "publisher": { "description": "The app's publisher. Defaults to the second element in [`Config::identifier`](Config::identifier) string. Currently maps to the Manufacturer property of the Windows Installer.", "type": [ "string", "null" ] }, "licenseFile": { "description": "A path to the license file.", "type": [ "string", "null" ] }, "copyright": { "description": "The app's copyright.", "type": [ "string", "null" ] }, "category": { "description": "The app's category.", "anyOf": [ { "$ref": "#/definitions/AppCategory" }, { "type": "null" } ] }, "icons": { "description": "The app's icon list. Supports glob patterns.", "type": [ "array", "null" ], "items": { "type": "string" } }, "fileAssociations": { "description": "The file associations", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/FileAssociation" } }, "deepLinkProtocols": { "description": "Deep-link protocols.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/DeepLinkProtocol" } }, "resources": { "description": "The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory or an object of `src` and `target` paths. In the case of using an object, the `src` could be either a glob pattern, path to a file, path to a directory, and the `target` is a path inside the final resources folder in the installed package.\n\n## Format-specific:\n\n- **[PackageFormat::Nsis] / [PackageFormat::Wix]**: The resources are placed next to the executable in the root of the packager. - **[PackageFormat::Deb]**: The resources are placed in `usr/lib` of the package.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Resource" } }, "externalBinaries": { "description": "Paths to external binaries to add to the package.\n\nThe path specified should not include `-<.exe>` suffix, it will be auto-added when by the packager when reading these paths, so the actual binary name should have the target platform's target triple appended, as well as `.exe` for Windows.\n\nFor example, if you're packaging an external binary called `sqlite3`, the packager expects a binary named `sqlite3-x86_64-unknown-linux-gnu` on linux, and `sqlite3-x86_64-pc-windows-gnu.exe` on windows.\n\nIf you are building a universal binary for MacOS, the packager expects your external binary to also be universal, and named after the target triple, e.g. `sqlite3-universal-apple-darwin`. See ", "type": [ "array", "null" ], "items": { "type": "string" } }, "windows": { "description": "Windows-specific configuration.", "anyOf": [ { "$ref": "#/definitions/WindowsConfig" }, { "type": "null" } ] }, "macos": { "description": "MacOS-specific configuration.", "anyOf": [ { "$ref": "#/definitions/MacOsConfig" }, { "type": "null" } ] }, "linux": { "description": "Linux-specific configuration", "anyOf": [ { "$ref": "#/definitions/LinuxConfig" }, { "type": "null" } ] }, "deb": { "description": "Debian-specific configuration.", "anyOf": [ { "$ref": "#/definitions/DebianConfig" }, { "type": "null" } ] }, "appimage": { "description": "AppImage configuration.", "anyOf": [ { "$ref": "#/definitions/AppImageConfig" }, { "type": "null" } ] }, "pacman": { "description": "Pacman configuration.", "anyOf": [ { "$ref": "#/definitions/PacmanConfig" }, { "type": "null" } ] }, "wix": { "description": "WiX configuration.", "anyOf": [ { "$ref": "#/definitions/WixConfig" }, { "type": "null" } ] }, "nsis": { "description": "Nsis configuration.", "anyOf": [ { "$ref": "#/definitions/NsisConfig" }, { "type": "null" } ] }, "dmg": { "description": "Dmg configuration.", "anyOf": [ { "$ref": "#/definitions/DmgConfig" }, { "type": "null" } ] } }, "additionalProperties": false, "definitions": { "Binary": { "description": "A binary to package within the final package.", "type": "object", "required": [ "path" ], "properties": { "path": { "description": "Path to the binary (without `.exe` on Windows). If it's relative, it will be resolved from [`Config::out_dir`].", "type": "string" }, "main": { "description": "Whether this is the main binary or not", "default": false, "type": "boolean" } }, "additionalProperties": false }, "HookCommand": { "description": "Describes a shell command to be executed when a CLI hook is triggered.", "anyOf": [ { "description": "Run the given script with the default options.", "type": "string" }, { "description": "Run the given script with custom options.", "type": "object", "required": [ "script" ], "properties": { "script": { "description": "The script to execute.", "type": "string" }, "dir": { "description": "The working directory.", "type": [ "string", "null" ] } } } ] }, "LogLevel": { "description": "An enum representing the available verbosity levels of the logger.", "oneOf": [ { "description": "The \"error\" level.\n\nDesignates very serious errors.", "type": "string", "enum": [ "error" ] }, { "description": "The \"warn\" level.\n\nDesignates hazardous situations.", "type": "string", "enum": [ "warn" ] }, { "description": "The \"info\" level.\n\nDesignates useful information.", "type": "string", "enum": [ "info" ] }, { "description": "The \"debug\" level.\n\nDesignates lower priority information.", "type": "string", "enum": [ "debug" ] }, { "description": "The \"trace\" level.\n\nDesignates very low priority, often extremely verbose, information.", "type": "string", "enum": [ "trace" ] } ] }, "PackageFormat": { "description": "Types of supported packages by [`cargo-packager`](https://docs.rs/cargo-packager).", "oneOf": [ { "description": "All available package formats for the current platform.\n\nSee [`PackageFormat::platform_all`]", "type": "string", "enum": [ "all" ] }, { "description": "The default list of package formats for the current platform.\n\nSee [`PackageFormat::platform_default`]", "type": "string", "enum": [ "default" ] }, { "description": "The macOS application bundle (.app).", "type": "string", "enum": [ "app" ] }, { "description": "The macOS DMG package (.dmg).", "type": "string", "enum": [ "dmg" ] }, { "description": "The Microsoft Software Installer (.msi) through WiX Toolset.", "type": "string", "enum": [ "wix" ] }, { "description": "The NSIS installer (.exe).", "type": "string", "enum": [ "nsis" ] }, { "description": "The Linux Debian package (.deb).", "type": "string", "enum": [ "deb" ] }, { "description": "The Linux AppImage package (.AppImage).", "type": "string", "enum": [ "appimage" ] }, { "description": "The Linux Pacman package (.tar.gz and PKGBUILD)", "type": "string", "enum": [ "pacman" ] } ] }, "AppCategory": { "description": "The possible app categories. Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.", "type": "string", "enum": [ "Business", "DeveloperTool", "Education", "Entertainment", "Finance", "Game", "ActionGame", "AdventureGame", "ArcadeGame", "BoardGame", "CardGame", "CasinoGame", "DiceGame", "EducationalGame", "FamilyGame", "KidsGame", "MusicGame", "PuzzleGame", "RacingGame", "RolePlayingGame", "SimulationGame", "SportsGame", "StrategyGame", "TriviaGame", "WordGame", "GraphicsAndDesign", "HealthcareAndFitness", "Lifestyle", "Medical", "Music", "News", "Photography", "Productivity", "Reference", "SocialNetworking", "Sports", "Travel", "Utility", "Video", "Weather" ] }, "FileAssociation": { "description": "A file association configuration.", "type": "object", "required": [ "extensions" ], "properties": { "extensions": { "description": "File extensions to associate with this app. e.g. 'png'", "type": "array", "items": { "type": "string" } }, "mimeType": { "description": "The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**.", "type": [ "string", "null" ] }, "description": { "description": "The association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer.", "type": [ "string", "null" ] }, "name": { "description": "The name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext`", "type": [ "string", "null" ] }, "role": { "description": "The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. Defaults to [`BundleTypeRole::Editor`]", "default": "editor", "allOf": [ { "$ref": "#/definitions/BundleTypeRole" } ] } }, "additionalProperties": false }, "BundleTypeRole": { "description": "*macOS-only**. Corresponds to CFBundleTypeRole", "oneOf": [ { "description": "CFBundleTypeRole.Editor. Files can be read and edited.", "type": "string", "enum": [ "editor" ] }, { "description": "CFBundleTypeRole.Viewer. Files can be read.", "type": "string", "enum": [ "viewer" ] }, { "description": "CFBundleTypeRole.Shell", "type": "string", "enum": [ "shell" ] }, { "description": "CFBundleTypeRole.QLGenerator", "type": "string", "enum": [ "qLGenerator" ] }, { "description": "CFBundleTypeRole.None", "type": "string", "enum": [ "none" ] } ] }, "DeepLinkProtocol": { "description": "Deep link protocol", "type": "object", "required": [ "schemes" ], "properties": { "schemes": { "description": "URL schemes to associate with this app without `://`. For example `my-app`", "type": "array", "items": { "type": "string" } }, "name": { "description": "The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `.`", "type": [ "string", "null" ] }, "role": { "description": "The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.", "default": "editor", "allOf": [ { "$ref": "#/definitions/BundleTypeRole" } ] } }, "additionalProperties": false }, "Resource": { "description": "A path to a resource (with optional glob pattern) or an object of `src` and `target` paths.", "anyOf": [ { "description": "Supports glob patterns", "type": "string" }, { "description": "An object descriping the src file or directory and its target location in the final package.", "type": "object", "required": [ "src", "target" ], "properties": { "src": { "description": "The src file or directory, supports glob patterns.", "type": "string" }, "target": { "description": "A relative path from the root of the final package.\n\nIf `src` is a glob, this will always be treated as a directory where all globbed files will be placed under.", "type": "string" } } } ] }, "WindowsConfig": { "description": "The Windows configuration.", "type": "object", "properties": { "digestAlgorithm": { "description": "The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended.", "type": [ "string", "null" ] }, "certificateThumbprint": { "description": "The SHA1 hash of the signing certificate.", "type": [ "string", "null" ] }, "tsp": { "description": "Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true.", "default": false, "type": "boolean" }, "timestampUrl": { "description": "Server to use during timestamping.", "type": [ "string", "null" ] }, "allowDowngrades": { "description": "Whether to validate a second app installation, blocking the user from installing an older version if set to `false`.\n\nFor instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`.\n\nThe default value of this flag is `true`.", "default": true, "type": "boolean" }, "signCommand": { "description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.", "type": [ "string", "null" ] } }, "additionalProperties": false }, "MacOsConfig": { "description": "The macOS configuration.", "type": "object", "properties": { "frameworks": { "description": "MacOS frameworks that need to be packaged with the app.\n\nEach string can either be the name of a framework (without the `.framework` extension, e.g. `\"SDL2\"`), in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle (under `Foobar.app/Contents/Frameworks/`); you are still responsible for:\n\n- arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script)\n\n- embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath \"@executable_path/../Frameworks\" path/to/binary` after compiling)", "type": [ "array", "null" ], "items": { "type": "string" } }, "minimumSystemVersion": { "description": "A version string indicating the minimum MacOS version that the packaged app supports (e.g. `\"10.11\"`). If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`.", "type": [ "string", "null" ] }, "exceptionDomain": { "description": "The exception domain to use on the macOS .app package.\n\nThis allows communication to the outside world e.g. a web server you're shipping.", "type": [ "string", "null" ] }, "signingIdentity": { "description": "Code signing identity.\n\nThis is typically of the form: `\"Developer ID Application: TEAM_NAME (TEAM_ID)\"`.", "type": [ "string", "null" ] }, "providerShortName": { "description": "Provider short name for notarization.", "type": [ "string", "null" ] }, "entitlements": { "description": "Path to the entitlements.plist file.", "type": [ "string", "null" ] }, "infoPlistPath": { "description": "Path to the Info.plist file for the package.", "type": [ "string", "null" ] }, "embeddedProvisionprofilePath": { "description": "Path to the embedded.provisionprofile file for the package.", "type": [ "string", "null" ] }, "embeddedApps": { "description": "Apps that need to be packaged within the app.", "type": [ "array", "null" ], "items": { "type": "string" } }, "backgroundApp": { "description": "Whether this is a background application. If true, the app will not appear in the Dock.\n\nSets the `LSUIElement` flag in the macOS plist file.", "default": false, "type": "boolean" } }, "additionalProperties": false }, "LinuxConfig": { "description": "Linux configuration", "type": "object", "properties": { "generateDesktopEntry": { "description": "Flag to indicate if desktop entry should be generated.", "default": true, "type": "boolean" } }, "additionalProperties": false }, "DebianConfig": { "description": "The Linux Debian configuration.", "type": "object", "properties": { "depends": { "description": "The list of Debian dependencies.", "anyOf": [ { "$ref": "#/definitions/Dependencies" }, { "type": "null" } ] }, "desktopTemplate": { "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.\n\nDefault file contents: ```text [Desktop Entry] Categories={{categories}} {{#if comment}} Comment={{comment}} {{/if}} Exec={{exec}} {{exec_arg}} Icon={{icon}} Name={{name}} Terminal=false Type=Application {{#if mime_type}} MimeType={{mime_type}} {{/if}} ```\n\nThe `{{exec_arg}}` will be set to: * \"%F\", if at least one [Config::file_associations] was specified but no deep link protocols were given. * The \"%F\" arg means that your application can be invoked with multiple file paths. * \"%U\", if at least one [Config::deep_link_protocols] was specified. * The \"%U\" arg means that your application can be invoked with multiple URLs. * If both [Config::file_associations] and [Config::deep_link_protocols] were specified, the \"%U\" arg will be used, causing the file paths to be passed to your app as `file://` URLs. * An empty string \"\" (nothing) if neither are given. * This means that your application will never be invoked with any URLs or file paths.\n\nTo specify a custom `exec_arg`, just use plaintext directly instead of `{{exec_arg}}`: ```text Exec={{exec}} %u ```\n\nSee more here: .", "type": [ "string", "null" ] }, "section": { "description": "Define the section in Debian Control file. See : ", "type": [ "string", "null" ] }, "priority": { "description": "Change the priority of the Debian Package. By default, it is set to `optional`. Recognized Priorities as of now are : `required`, `important`, `standard`, `optional`, `extra`", "type": [ "string", "null" ] }, "files": { "description": "List of custom files to add to the deb package. Maps a dir/file to a dir/file inside the debian package.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "packageName": { "description": "Name to use for the `Package` field in the Debian Control file. Defaults to [`Config::product_name`] converted to kebab-case.", "type": [ "string", "null" ] } }, "additionalProperties": false }, "Dependencies": { "description": "A list of dependencies specified as either a list of Strings or as a path to a file that lists the dependencies, one per line.", "anyOf": [ { "description": "The list of dependencies provided directly as a vector of Strings.", "type": "array", "items": { "type": "string" } }, { "description": "A path to the file containing the list of dependences, formatted as one per line: ```text libc6 libxcursor1 libdbus-1-3 libasyncns0 ... ```", "type": "string" } ] }, "AppImageConfig": { "description": "The Linux AppImage configuration.", "type": "object", "properties": { "libs": { "description": "List of libs that exist in `/usr/lib*` to be include in the final AppImage. The libs will be searched for, using the command `find -L /usr/lib* -name `", "type": [ "array", "null" ], "items": { "type": "string" } }, "bins": { "description": "List of binary paths to include in the final AppImage. For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open`", "type": [ "array", "null" ], "items": { "type": "string" } }, "files": { "description": "List of custom files to add to the appimage package. Maps a dir/file to a dir/file inside the appimage package.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "linuxdeployPlugins": { "description": "A map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) plugin name and its URL to be downloaded and executed while packaing the appimage. For example, if you want to use the [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, you'd specify `gtk` as the key and its url as the value.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "excludedLibs": { "description": "List of globs of libraries to exclude from the final AppImage. For example, to exclude libnss3.so, you'd specify `libnss3*`", "type": [ "array", "null" ], "items": { "type": "string" } } }, "additionalProperties": false }, "PacmanConfig": { "description": "The Linux pacman configuration.", "type": "object", "properties": { "files": { "description": "List of custom files to add to the pacman package. Maps a dir/file to a dir/file inside the pacman package.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "depends": { "description": "List of softwares that must be installed for the app to build and run.\n\nSee : ", "anyOf": [ { "$ref": "#/definitions/Dependencies" }, { "type": "null" } ] }, "provides": { "description": "Additional packages that are provided by this app.\n\nSee : ", "type": [ "array", "null" ], "items": { "type": "string" } }, "conflicts": { "description": "Packages that conflict or cause problems with the app. All these packages and packages providing this item will need to be removed\n\nSee : ", "type": [ "array", "null" ], "items": { "type": "string" } }, "replaces": { "description": "Only use if this app replaces some obsolete packages. For example, if you rename any package.\n\nSee : ", "type": [ "array", "null" ], "items": { "type": "string" } }, "source": { "description": "Source of the package to be stored at PKGBUILD. PKGBUILD is a bash script, so version can be referred as ${pkgver}", "type": [ "array", "null" ], "items": { "type": "string" } } }, "additionalProperties": false }, "WixConfig": { "description": "The wix format configuration", "type": "object", "properties": { "languages": { "description": "The app languages to build. See .", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/WixLanguage" } }, "template": { "description": "By default, the packager uses an internal template. This option allows you to define your own wix file.", "type": [ "string", "null" ] }, "mergeModules": { "description": "List of merge modules to include in your installer. For example, if you want to include [C++ Redis merge modules]\n\n[C++ Redis merge modules]: https://wixtoolset.org/docs/v3/howtos/redistributables_and_install_checks/install_vcredist/", "type": [ "array", "null" ], "items": { "type": "string" } }, "fragmentPaths": { "description": "A list of paths to .wxs files with WiX fragments to use.", "type": [ "array", "null" ], "items": { "type": "string" } }, "fragments": { "description": "List of WiX fragments as strings. This is similar to `config.wix.fragments_paths` but is a string so you can define it inline in your config.\n\n```text ```", "type": [ "array", "null" ], "items": { "type": "string" } }, "componentGroupRefs": { "description": "The ComponentGroup element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "componentRefs": { "description": "The Component element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "customActionRefs": { "description": "The CustomAction element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "featureGroupRefs": { "description": "The FeatureGroup element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "featureRefs": { "description": "The Feature element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "mergeRefs": { "description": "The Merge element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "bannerPath": { "description": "Path to a bitmap file to use as the installation user interface banner. This bitmap will appear at the top of all but the first page of the installer.\n\nThe required dimensions are 493px × 58px.", "type": [ "string", "null" ] }, "dialogImagePath": { "description": "Path to a bitmap file to use on the installation user interface dialogs. It is used on the welcome and completion dialogs. The required dimensions are 493px × 312px.", "type": [ "string", "null" ] }, "fipsCompliant": { "description": "Enables FIPS compliant algorithms.", "default": false, "type": "boolean" } }, "additionalProperties": false }, "WixLanguage": { "description": "A wix language.", "anyOf": [ { "description": "Built-in wix language identifier.", "type": "string" }, { "description": "Custom wix language.", "type": "object", "required": [ "identifier" ], "properties": { "identifier": { "description": "Idenitifier of this language, for example `en-US`", "type": "string" }, "path": { "description": "The path to a locale (`.wxl`) file. See .", "type": [ "string", "null" ] } } } ] }, "NsisConfig": { "description": "The NSIS format configuration.", "type": "object", "properties": { "compression": { "description": "Set the compression algorithm used to compress files in the installer.\n\nSee ", "anyOf": [ { "$ref": "#/definitions/NsisCompression" }, { "type": "null" } ] }, "template": { "description": "A custom `.nsi` template to use.\n\nSee the default template here ", "type": [ "string", "null" ] }, "preinstallSection": { "description": "Logic of an NSIS section that will be ran before the install section.\n\nSee the available libraries, dlls and global variables here \n\n### Example ```toml [package.metadata.packager.nsis] preinstall-section = \"\"\" ; Setup custom messages LangString webview2AbortError ${LANG_ENGLISH} \"Failed to install WebView2! The app can't run without it. Try restarting the installer.\" LangString webview2DownloadError ${LANG_ARABIC} \"خطأ: فشل تنزيل WebView2 - $0\"\n\nSection PreInstall ;
SectionEnd\n\nSection AnotherPreInstall ;
SectionEnd \"\"\" ```", "type": [ "string", "null" ] }, "headerImage": { "description": "The path to a bitmap file to display on the header of installers pages.\n\nThe recommended dimensions are 150px x 57px.", "type": [ "string", "null" ] }, "sidebarImage": { "description": "The path to a bitmap file for the Welcome page and the Finish page.\n\nThe recommended dimensions are 164px x 314px.", "type": [ "string", "null" ] }, "installerIcon": { "description": "The path to an icon file used as the installer icon.", "type": [ "string", "null" ] }, "installMode": { "description": "Whether the installation will be for all users or just the current user.", "default": "currentUser", "allOf": [ { "$ref": "#/definitions/NSISInstallerMode" } ] }, "languages": { "description": "A list of installer languages. By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. To allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", "type": [ "array", "null" ], "items": { "type": "string" } }, "customLanguageFiles": { "description": "An key-value pair where the key is the language and the value is the path to a custom `.nsi` file that holds the translated text for cargo-packager's custom messages.\n\nSee for an example `.nsi` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array,", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "displayLanguageSelector": { "description": "Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array.", "default": false, "type": "boolean" }, "appdataPaths": { "description": "List of paths where your app stores data. This options tells the uninstaller to provide the user with an option (disabled by default) whether they want to rmeove your app data or keep it.\n\nThe path should use a constant from in addition to `$IDENTIFIER`, `$PUBLISHER` and `$PRODUCTNAME`, for example, if you store your app data in `C:\\\\Users\\\\\\\\AppData\\\\Local\\\\\\\\` you'd need to specify ```toml [package.metadata.packager.nsis] appdata-paths = [\"$LOCALAPPDATA/$PUBLISHER/$PRODUCTNAME\"] ```", "default": null, "type": [ "array", "null" ], "items": { "type": "string" } } }, "additionalProperties": false }, "NsisCompression": { "description": "Compression algorithms used in the NSIS installer.\n\nSee ", "oneOf": [ { "description": "ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory.", "type": "string", "enum": [ "zlib" ] }, { "description": "BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory.", "type": "string", "enum": [ "bzip2" ] }, { "description": "LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB.", "type": "string", "enum": [ "lzma" ] }, { "description": "Disable compression.", "type": "string", "enum": [ "off" ] } ] }, "NSISInstallerMode": { "description": "Install Modes for the NSIS installer.", "oneOf": [ { "description": "Default mode for the installer.\n\nInstall the app by default in a directory that doesn't require Administrator access.\n\nInstaller metadata will be saved under the `HKCU` registry path.", "type": "string", "enum": [ "currentUser" ] }, { "description": "Install the app by default in the `Program Files` folder directory requires Administrator access for the installation.\n\nInstaller metadata will be saved under the `HKLM` registry path.", "type": "string", "enum": [ "perMachine" ] }, { "description": "Combines both modes and allows the user to choose at install time whether to install for the current user or per machine. Note that this mode will require Administrator access even if the user wants to install it for the current user only.\n\nInstaller metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.", "type": "string", "enum": [ "both" ] } ] }, "DmgConfig": { "description": "The Apple Disk Image (.dmg) configuration.", "type": "object", "properties": { "background": { "description": "Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`.", "type": [ "string", "null" ] }, "windowPosition": { "description": "Position of volume window on screen.", "anyOf": [ { "$ref": "#/definitions/Position" }, { "type": "null" } ] }, "windowSize": { "description": "Size of volume window.", "anyOf": [ { "$ref": "#/definitions/Size" }, { "type": "null" } ] }, "appPosition": { "description": "Position of application file on window.", "anyOf": [ { "$ref": "#/definitions/Position" }, { "type": "null" } ] }, "appFolderPosition": { "description": "Position of application folder on window.", "anyOf": [ { "$ref": "#/definitions/Position" }, { "type": "null" } ] } }, "additionalProperties": false }, "Position": { "description": "Position coordinates struct.", "type": "object", "required": [ "x", "y" ], "properties": { "x": { "description": "X coordinate.", "type": "integer", "format": "uint32", "minimum": 0.0 }, "y": { "description": "Y coordinate.", "type": "integer", "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false }, "Size": { "description": "Size struct.", "type": "object", "required": [ "height", "width" ], "properties": { "width": { "description": "Width.", "type": "integer", "format": "uint32", "minimum": 0.0 }, "height": { "description": "Height.", "type": "integer", "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false } } } ================================================ FILE: bindings/packager/nodejs/src/lib.rs ================================================ use napi::{Error, Result, Status}; #[napi_derive::napi] pub fn cli(args: Vec, bin_name: Option) -> Result<()> { cargo_packager::cli::try_run(args, bin_name) .map_err(|e| Error::new(Status::GenericFailure, e.to_string())) } #[napi_derive::napi] pub fn package_app(config: String) -> Result<()> { let config = serde_json::from_str(&config) .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; cargo_packager::package(&config) .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; Ok(()) } #[napi_derive::napi] pub fn package_and_sign_app(config: String, signing_config: String) -> Result<()> { let config = serde_json::from_str(&config) .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; let signing_config = serde_json::from_str(&signing_config) .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; cargo_packager::package_and_sign(&config, &signing_config) .map_err(|e| Error::new(Status::GenericFailure, e.to_string()))?; Ok(()) } #[napi_derive::napi] pub fn init_tracing_subscriber(verbosity: u8) { cargo_packager::init_tracing_subscriber(verbosity); } #[napi_derive::napi] pub fn log_error(error: String) { tracing::error!("{}", error); } ================================================ FILE: bindings/packager/nodejs/src-ts/config.d.ts ================================================ /* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, * and run json-schema-to-typescript to regenerate this file. */ /** * Describes a shell command to be executed when a CLI hook is triggered. */ export type HookCommand = | string | { /** * The script to execute. */ script: string; /** * The working directory. */ dir?: string | null; [k: string]: unknown; }; /** * An enum representing the available verbosity levels of the logger. */ export type LogLevel = "error" | "warn" | "info" | "debug" | "trace"; /** * Types of supported packages by [`cargo-packager`](https://docs.rs/cargo-packager). */ export type PackageFormat = "all" | "default" | "app" | "dmg" | "wix" | "nsis" | "deb" | "appimage" | "pacman"; /** * The possible app categories. Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian. */ export type AppCategory = | "Business" | "DeveloperTool" | "Education" | "Entertainment" | "Finance" | "Game" | "ActionGame" | "AdventureGame" | "ArcadeGame" | "BoardGame" | "CardGame" | "CasinoGame" | "DiceGame" | "EducationalGame" | "FamilyGame" | "KidsGame" | "MusicGame" | "PuzzleGame" | "RacingGame" | "RolePlayingGame" | "SimulationGame" | "SportsGame" | "StrategyGame" | "TriviaGame" | "WordGame" | "GraphicsAndDesign" | "HealthcareAndFitness" | "Lifestyle" | "Medical" | "Music" | "News" | "Photography" | "Productivity" | "Reference" | "SocialNetworking" | "Sports" | "Travel" | "Utility" | "Video" | "Weather"; /** * *macOS-only**. Corresponds to CFBundleTypeRole */ export type BundleTypeRole = "editor" | "viewer" | "shell" | "qLGenerator" | "none"; /** * A path to a resource (with optional glob pattern) or an object of `src` and `target` paths. */ export type Resource = | string | { /** * The src file or directory, supports glob patterns. */ src: string; /** * A relative path from the root of the final package. * * If `src` is a glob, this will always be treated as a directory where all globbed files will be placed under. */ target: string; [k: string]: unknown; }; /** * A list of dependencies specified as either a list of Strings or as a path to a file that lists the dependencies, one per line. */ export type Dependencies = string[] | string; /** * A wix language. */ export type WixLanguage = | string | { /** * Idenitifier of this language, for example `en-US` */ identifier: string; /** * The path to a locale (`.wxl`) file. See . */ path?: string | null; [k: string]: unknown; }; /** * Compression algorithms used in the NSIS installer. * * See */ export type NsisCompression = "zlib" | "bzip2" | "lzma" | "off"; /** * Install Modes for the NSIS installer. */ export type NSISInstallerMode = "currentUser" | "perMachine" | "both"; /** * The packaging config. */ export interface Config { /** * The JSON schema for the config. * * Setting this field has no effect, this just exists so we can parse the JSON correctly when it has `$schema` field set. */ $schema?: string | null; /** * The app name, this is just an identifier that could be used to filter which app to package using `--packages` cli arg when there is multiple apps in the workspace or in the same config. * * This field resembles, the `name` field in `Cargo.toml` or `package.json` * * If `unset`, the CLI will try to auto-detect it from `Cargo.toml` or `package.json` otherwise, it will keep it unset. */ name?: string | null; /** * Whether this config is enabled or not. Defaults to `true`. */ enabled?: boolean; /** * The package's product name, for example "My Awesome App". */ productName?: string; /** * The package's version. */ version?: string; /** * The binaries to package. */ binaries?: Binary[]; /** * The application identifier in reverse domain name notation (e.g. `com.packager.example`). This string must be unique across applications since it is used in some system configurations. This string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.). */ identifier?: string | null; /** * The command to run before starting to package an application. * * This runs only once. */ beforePackagingCommand?: HookCommand | null; /** * The command to run before packaging each format for an application. * * This will run multiple times depending on the formats specifed. */ beforeEachPackageCommand?: HookCommand | null; /** * The logging level. */ logLevel?: LogLevel | null; /** * The packaging formats to create, if not present, [`PackageFormat::platform_default`] is used. */ formats?: PackageFormat[] | null; /** * The directory where the generated packages will be placed. * * If [`Config::binaries_dir`] is not set, this is also where the [`Config::binaries`] exist. */ outDir?: string; /** * The directory where the [`Config::binaries`] exist. * * Defaults to [`Config::out_dir`]. */ binariesDir?: string | null; /** * The target triple we are packaging for. * * Defaults to the current OS target triple. */ targetTriple?: string | null; /** * The package's description. */ description?: string | null; /** * The app's long description. */ longDescription?: string | null; /** * The package's homepage. */ homepage?: string | null; /** * The package's authors. */ authors?: string[] | null; /** * The app's publisher. Defaults to the second element in [`Config::identifier`](Config::identifier) string. Currently maps to the Manufacturer property of the Windows Installer. */ publisher?: string | null; /** * A path to the license file. */ licenseFile?: string | null; /** * The app's copyright. */ copyright?: string | null; /** * The app's category. */ category?: AppCategory | null; /** * The app's icon list. Supports glob patterns. */ icons?: string[] | null; /** * The file associations */ fileAssociations?: FileAssociation[] | null; /** * Deep-link protocols. */ deepLinkProtocols?: DeepLinkProtocol[] | null; /** * The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory or an object of `src` and `target` paths. In the case of using an object, the `src` could be either a glob pattern, path to a file, path to a directory, and the `target` is a path inside the final resources folder in the installed package. * * ## Format-specific: * * - **[PackageFormat::Nsis] / [PackageFormat::Wix]**: The resources are placed next to the executable in the root of the packager. - **[PackageFormat::Deb]**: The resources are placed in `usr/lib` of the package. */ resources?: Resource[] | null; /** * Paths to external binaries to add to the package. * * The path specified should not include `-<.exe>` suffix, it will be auto-added when by the packager when reading these paths, so the actual binary name should have the target platform's target triple appended, as well as `.exe` for Windows. * * For example, if you're packaging an external binary called `sqlite3`, the packager expects a binary named `sqlite3-x86_64-unknown-linux-gnu` on linux, and `sqlite3-x86_64-pc-windows-gnu.exe` on windows. * * If you are building a universal binary for MacOS, the packager expects your external binary to also be universal, and named after the target triple, e.g. `sqlite3-universal-apple-darwin`. See */ externalBinaries?: string[] | null; /** * Windows-specific configuration. */ windows?: WindowsConfig | null; /** * MacOS-specific configuration. */ macos?: MacOsConfig | null; /** * Linux-specific configuration */ linux?: LinuxConfig | null; /** * Debian-specific configuration. */ deb?: DebianConfig | null; /** * AppImage configuration. */ appimage?: AppImageConfig | null; /** * Pacman configuration. */ pacman?: PacmanConfig | null; /** * WiX configuration. */ wix?: WixConfig | null; /** * Nsis configuration. */ nsis?: NsisConfig | null; /** * Dmg configuration. */ dmg?: DmgConfig | null; } /** * A binary to package within the final package. */ export interface Binary { /** * Path to the binary (without `.exe` on Windows). If it's relative, it will be resolved from [`Config::out_dir`]. */ path: string; /** * Whether this is the main binary or not */ main?: boolean; } /** * A file association configuration. */ export interface FileAssociation { /** * File extensions to associate with this app. e.g. 'png' */ extensions: string[]; /** * The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**. */ mimeType?: string | null; /** * The association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer. */ description?: string | null; /** * The name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext` */ name?: string | null; /** * The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. Defaults to [`BundleTypeRole::Editor`] */ role?: BundleTypeRole & string; } /** * Deep link protocol */ export interface DeepLinkProtocol { /** * URL schemes to associate with this app without `://`. For example `my-app` */ schemes: string[]; /** * The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `.` */ name?: string | null; /** * The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`. */ role?: BundleTypeRole & string; } /** * The Windows configuration. */ export interface WindowsConfig { /** * The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended. */ digestAlgorithm?: string | null; /** * The SHA1 hash of the signing certificate. */ certificateThumbprint?: string | null; /** * Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true. */ tsp?: boolean; /** * Server to use during timestamping. */ timestampUrl?: string | null; /** * Whether to validate a second app installation, blocking the user from installing an older version if set to `false`. * * For instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`. * * The default value of this flag is `true`. */ allowDowngrades?: boolean; /** * Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command. * * By Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`. */ signCommand?: string | null; } /** * The macOS configuration. */ export interface MacOsConfig { /** * MacOS frameworks that need to be packaged with the app. * * Each string can either be the name of a framework (without the `.framework` extension, e.g. `"SDL2"`), in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle (under `Foobar.app/Contents/Frameworks/`); you are still responsible for: * * - arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script) * * - embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath "@executable_path/../Frameworks" path/to/binary` after compiling) */ frameworks?: string[] | null; /** * A version string indicating the minimum MacOS version that the packaged app supports (e.g. `"10.11"`). If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`. */ minimumSystemVersion?: string | null; /** * The exception domain to use on the macOS .app package. * * This allows communication to the outside world e.g. a web server you're shipping. */ exceptionDomain?: string | null; /** * Code signing identity. * * This is typically of the form: `"Developer ID Application: TEAM_NAME (TEAM_ID)"`. */ signingIdentity?: string | null; /** * Provider short name for notarization. */ providerShortName?: string | null; /** * Path to the entitlements.plist file. */ entitlements?: string | null; /** * Path to the Info.plist file for the package. */ infoPlistPath?: string | null; /** * Path to the embedded.provisionprofile file for the package. */ embeddedProvisionprofilePath?: string | null; /** * Apps that need to be packaged within the app. */ embeddedApps?: string[] | null; /** * Whether this is a background application. If true, the app will not appear in the Dock. * * Sets the `LSUIElement` flag in the macOS plist file. */ backgroundApp?: boolean; } /** * Linux configuration */ export interface LinuxConfig { /** * Flag to indicate if desktop entry should be generated. */ generateDesktopEntry?: boolean; } /** * The Linux Debian configuration. */ export interface DebianConfig { /** * The list of Debian dependencies. */ depends?: Dependencies | null; /** * Path to a custom desktop file Handlebars template. * * Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`. * * Default file contents: ```text [Desktop Entry] Categories={{categories}} {{#if comment}} Comment={{comment}} {{/if}} Exec={{exec}} {{exec_arg}} Icon={{icon}} Name={{name}} Terminal=false Type=Application {{#if mime_type}} MimeType={{mime_type}} {{/if}} ``` * * The `{{exec_arg}}` will be set to: * "%F", if at least one [Config::file_associations] was specified but no deep link protocols were given. * The "%F" arg means that your application can be invoked with multiple file paths. * "%U", if at least one [Config::deep_link_protocols] was specified. * The "%U" arg means that your application can be invoked with multiple URLs. * If both [Config::file_associations] and [Config::deep_link_protocols] were specified, the "%U" arg will be used, causing the file paths to be passed to your app as `file://` URLs. * An empty string "" (nothing) if neither are given. * This means that your application will never be invoked with any URLs or file paths. * * To specify a custom `exec_arg`, just use plaintext directly instead of `{{exec_arg}}`: ```text Exec={{exec}} %u ``` * * See more here: . */ desktopTemplate?: string | null; /** * Define the section in Debian Control file. See : */ section?: string | null; /** * Change the priority of the Debian Package. By default, it is set to `optional`. Recognized Priorities as of now are : `required`, `important`, `standard`, `optional`, `extra` */ priority?: string | null; /** * List of custom files to add to the deb package. Maps a dir/file to a dir/file inside the debian package. */ files?: { [k: string]: string; } | null; /** * Name to use for the `Package` field in the Debian Control file. Defaults to [`Config::product_name`] converted to kebab-case. */ packageName?: string | null; } /** * The Linux AppImage configuration. */ export interface AppImageConfig { /** * List of libs that exist in `/usr/lib*` to be include in the final AppImage. The libs will be searched for, using the command `find -L /usr/lib* -name ` */ libs?: string[] | null; /** * List of binary paths to include in the final AppImage. For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open` */ bins?: string[] | null; /** * List of custom files to add to the appimage package. Maps a dir/file to a dir/file inside the appimage package. */ files?: { [k: string]: string; } | null; /** * A map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) plugin name and its URL to be downloaded and executed while packaing the appimage. For example, if you want to use the [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, you'd specify `gtk` as the key and its url as the value. */ linuxdeployPlugins?: { [k: string]: string; } | null; /** * List of globs of libraries to exclude from the final AppImage. For example, to exclude libnss3.so, you'd specify `libnss3*` */ excludedLibs?: string[] | null; } /** * The Linux pacman configuration. */ export interface PacmanConfig { /** * List of custom files to add to the pacman package. Maps a dir/file to a dir/file inside the pacman package. */ files?: { [k: string]: string; } | null; /** * List of softwares that must be installed for the app to build and run. * * See : */ depends?: Dependencies | null; /** * Additional packages that are provided by this app. * * See : */ provides?: string[] | null; /** * Packages that conflict or cause problems with the app. All these packages and packages providing this item will need to be removed * * See : */ conflicts?: string[] | null; /** * Only use if this app replaces some obsolete packages. For example, if you rename any package. * * See : */ replaces?: string[] | null; /** * Source of the package to be stored at PKGBUILD. PKGBUILD is a bash script, so version can be referred as ${pkgver} */ source?: string[] | null; } /** * The wix format configuration */ export interface WixConfig { /** * The app languages to build. See . */ languages?: WixLanguage[] | null; /** * By default, the packager uses an internal template. This option allows you to define your own wix file. */ template?: string | null; /** * List of merge modules to include in your installer. For example, if you want to include [C++ Redis merge modules] * * [C++ Redis merge modules]: https://wixtoolset.org/docs/v3/howtos/redistributables_and_install_checks/install_vcredist/ */ mergeModules?: string[] | null; /** * A list of paths to .wxs files with WiX fragments to use. */ fragmentPaths?: string[] | null; /** * List of WiX fragments as strings. This is similar to `config.wix.fragments_paths` but is a string so you can define it inline in your config. * * ```text ``` */ fragments?: string[] | null; /** * The ComponentGroup element ids you want to reference from the fragments. */ componentGroupRefs?: string[] | null; /** * The Component element ids you want to reference from the fragments. */ componentRefs?: string[] | null; /** * The CustomAction element ids you want to reference from the fragments. */ customActionRefs?: string[] | null; /** * The FeatureGroup element ids you want to reference from the fragments. */ featureGroupRefs?: string[] | null; /** * The Feature element ids you want to reference from the fragments. */ featureRefs?: string[] | null; /** * The Merge element ids you want to reference from the fragments. */ mergeRefs?: string[] | null; /** * Path to a bitmap file to use as the installation user interface banner. This bitmap will appear at the top of all but the first page of the installer. * * The required dimensions are 493px × 58px. */ bannerPath?: string | null; /** * Path to a bitmap file to use on the installation user interface dialogs. It is used on the welcome and completion dialogs. The required dimensions are 493px × 312px. */ dialogImagePath?: string | null; /** * Enables FIPS compliant algorithms. */ fipsCompliant?: boolean; } /** * The NSIS format configuration. */ export interface NsisConfig { /** * Set the compression algorithm used to compress files in the installer. * * See */ compression?: NsisCompression | null; /** * A custom `.nsi` template to use. * * See the default template here */ template?: string | null; /** * Logic of an NSIS section that will be ran before the install section. * * See the available libraries, dlls and global variables here * * ### Example ```toml [package.metadata.packager.nsis] preinstall-section = """ ; Setup custom messages LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." LangString webview2DownloadError ${LANG_ARABIC} "خطأ: فشل تنزيل WebView2 - $0" * * Section PreInstall ;
SectionEnd * * Section AnotherPreInstall ;
SectionEnd """ ``` */ preinstallSection?: string | null; /** * The path to a bitmap file to display on the header of installers pages. * * The recommended dimensions are 150px x 57px. */ headerImage?: string | null; /** * The path to a bitmap file for the Welcome page and the Finish page. * * The recommended dimensions are 164px x 314px. */ sidebarImage?: string | null; /** * The path to an icon file used as the installer icon. */ installerIcon?: string | null; /** * Whether the installation will be for all users or just the current user. */ installMode?: NSISInstallerMode & string; /** * A list of installer languages. By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. To allow the user to select the language, set `display_language_selector` to `true`. * * See for the complete list of languages. */ languages?: string[] | null; /** * An key-value pair where the key is the language and the value is the path to a custom `.nsi` file that holds the translated text for cargo-packager's custom messages. * * See for an example `.nsi` file. * * **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array, */ customLanguageFiles?: { [k: string]: string; } | null; /** * Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array. */ displayLanguageSelector?: boolean; /** * List of paths where your app stores data. This options tells the uninstaller to provide the user with an option (disabled by default) whether they want to rmeove your app data or keep it. * * The path should use a constant from in addition to `$IDENTIFIER`, `$PUBLISHER` and `$PRODUCTNAME`, for example, if you store your app data in `C:\\Users\\\\AppData\\Local\\\\` you'd need to specify ```toml [package.metadata.packager.nsis] appdata-paths = ["$LOCALAPPDATA/$PUBLISHER/$PRODUCTNAME"] ``` */ appdataPaths?: string[] | null; } /** * The Apple Disk Image (.dmg) configuration. */ export interface DmgConfig { /** * Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`. */ background?: string | null; /** * Position of volume window on screen. */ windowPosition?: Position | null; /** * Size of volume window. */ windowSize?: Size | null; /** * Position of application file on window. */ appPosition?: Position | null; /** * Position of application folder on window. */ appFolderPosition?: Position | null; } /** * Position coordinates struct. */ export interface Position { /** * X coordinate. */ x: number; /** * Y coordinate. */ y: number; } /** * Size struct. */ export interface Size { /** * Width. */ width: number; /** * Height. */ height: number; } ================================================ FILE: bindings/packager/nodejs/src-ts/index.ts ================================================ import cargoPackager from "../index"; import runPlugins from "./plugins"; import merge from "deepmerge"; import type { Config } from "./config"; let tracingEnabled = false; export interface Options { verbosity?: number; } export interface SigningConfig { /** The private key to use for signing. */ privateKey: string; /** * The private key password. * * If `null`, user will be prompted to write a password. * You can skip the prompt by specifying an empty string. */ password?: string; } async function packageApp(config: Config = {}, options?: Options) { const conf = await runPlugins(); let packagerConfig = config; if (conf) { packagerConfig = merge(conf, config); } if (!tracingEnabled) { cargoPackager.initTracingSubscriber(options?.verbosity ?? 0); tracingEnabled = true; } cargoPackager.packageApp(JSON.stringify(packagerConfig)); } async function packageAndSignApp( config: Config = {}, signingConfig: SigningConfig, options?: Options, ) { const conf = await runPlugins(); let packagerConfig = config; if (conf) { packagerConfig = merge(conf, config); } if (!tracingEnabled) { cargoPackager.initTracingSubscriber(options?.verbosity ?? 0); tracingEnabled = true; } cargoPackager.packageAndSignApp( JSON.stringify(packagerConfig), JSON.stringify(signingConfig), ); } async function cli(args: string[], binName: string) { const config = await runPlugins(); if (config) { args.push("--config"); args.push(JSON.stringify(config)); } cargoPackager.cli(args, binName); } function logError(error: string) { cargoPackager.logError(error); } export { cli, packageApp, packageAndSignApp, logError }; ================================================ FILE: bindings/packager/nodejs/src-ts/plugins/electron/index.ts ================================================ import type { Config, Resource } from "../../config"; import type { PackageJson } from ".."; import fs from "fs-extra"; import path from "path"; import os from "os"; import { download as downloadElectron } from "@electron/get"; import extractZip from "extract-zip"; import { Pruner, isModule, normalizePath } from "./prune"; export default async function run( appPath: string, packageJson: PackageJson, ): Promise | null> { let electronPath; try { electronPath = require.resolve("electron", { paths: [appPath], }); } catch (e) { return null; } const userConfig = packageJson.packager || {}; const electronPackageJson = JSON.parse( ( await fs.readFile( path.resolve(path.dirname(electronPath), "package.json"), ) ).toString(), ); const zipPath = await downloadElectron(electronPackageJson.version); const zipDir = await fs.mkdtemp(path.join(os.tmpdir(), ".packager-electron")); await extractZip(zipPath, { dir: zipDir, }); const platformName = os.platform(); let resources: Resource[] = []; let frameworks: string[] = []; let debianFiles: { [k: string]: string; } | null = null; let binaryPath; const appTempPath = await fs.mkdtemp( path.join(os.tmpdir(), packageJson.name || "app-temp"), ); const pruner = new Pruner(appPath, true); const outDir = userConfig.outDir ? path.resolve(userConfig.outDir) : null; const ignoredDirs = outDir && outDir !== process.cwd() ? [outDir] : []; await fs.copy(appPath, appTempPath, { filter: async (file: string) => { const fullPath = path.resolve(file); if (ignoredDirs.includes(fullPath)) { return false; } let name = fullPath.split(appPath)[1]; if (path.sep === "\\") { name = normalizePath(name); } if (name.startsWith("/node_modules/")) { if (await isModule(file)) { return await pruner.pruneModule(name); } } return true; }, }); switch (platformName) { case "darwin": var binaryName: string = userConfig.name || packageJson.productName || packageJson.name || "Electron"; var standaloneElectronPath = path.join(zipDir, "Electron.app"); const resourcesPath = path.join( standaloneElectronPath, "Contents/Resources", ); resources = resources.concat( (await fs.readdir(resourcesPath)) .filter((p) => p !== "default_app.asar") .map((p) => path.join(resourcesPath, p)), ); resources.push({ src: appTempPath, target: "app", }); const frameworksPath = path.join( standaloneElectronPath, "Contents/Frameworks", ); frameworks = (await fs.readdir(frameworksPath)).map((p) => path.join(frameworksPath, p), ); binaryPath = path.join( standaloneElectronPath, `Contents/MacOS/${binaryName}`, ); // rename the electron binary await fs.rename( path.join(standaloneElectronPath, "Contents/MacOS/Electron"), binaryPath, ); break; case "win32": var binaryName: string = userConfig.name || packageJson.productName || packageJson.name || "Electron"; binaryPath = path.join(zipDir, `${binaryName}.exe`); resources = resources.concat( (await fs.readdir(zipDir)) // resources only contains the default_app.asar so we ignore it .filter((p) => p !== "resources" && p !== "electron.exe") .map((p) => path.join(zipDir, p)), ); // rename the electron binary await fs.rename(path.join(zipDir, "electron.exe"), binaryPath); resources.push({ src: appTempPath, target: "resources/app", }); break; default: var binaryName = toKebabCase( userConfig.name || packageJson.productName || packageJson.name || "Electron", ); // rename the electron binary await fs.rename( path.join(zipDir, "electron"), path.join(zipDir, binaryName), ); const electronFiles = await fs.readdir(zipDir); const binTmpDir = await fs.mkdtemp( path.join(os.tmpdir(), `${packageJson.name || "app-temp"}-bin`), ); binaryPath = path.join(binTmpDir, binaryName); await fs.writeFile(binaryPath, binaryScript(binaryName)); await fs.chmod(binaryPath, 0o755); // make linuxdeploy happy process.env.LD_LIBRARY_PATH = process.env.LD_LIBRARY_PATH ? `${process.env.LD_LIBRARY_PATH}:${zipDir}` : zipDir; // electron needs everything at the same level :) // resources only contains the default_app.asar so we ignore it debianFiles = electronFiles .filter((f) => f !== "resources") .reduce( (acc, file) => ({ ...acc, [path.join(zipDir, file)]: `usr/lib/${binaryName}/${file}`, }), {}, ); debianFiles[appTempPath] = `usr/lib/${binaryName}/resources/app`; } return { name: packageJson.name, productName: packageJson.productName || packageJson.name, version: packageJson.version, resources, macos: { frameworks, }, deb: { files: debianFiles, }, appimage: { files: debianFiles, }, binaries: [ { path: binaryPath, main: true, }, ], }; } const toKebabCase = (str: string) => str .split(/\.?(?=[A-Z])/) .join("-") .toLowerCase(); function binaryScript(binaryName: string): string { return `#!/usr/bin/env sh full_path=$(realpath $0) bin_dir_path=$(dirname $full_path) usr_dir_path=$(dirname $bin_dir_path) echo $usr_dir_path $usr_dir_path/lib/${binaryName}/${binaryName} `; } ================================================ FILE: bindings/packager/nodejs/src-ts/plugins/electron/prune.ts ================================================ // from https://github.com/electron/electron-packager/blob/741f3c349e7f9e11e5ae14593a3efa79d312dc4d/src/prune.js import { DestroyerOfModules, ModuleMap, DepType, Module } from "galactus"; import fs from "fs"; import path from "path"; const ELECTRON_MODULES = [ "electron", "electron-nightly", "electron-prebuilt", "electron-prebuilt-compile", ]; export function normalizePath(path: string): string { return path.replace(/\\/g, "/"); } class Pruner { baseDir: string; quiet: boolean; galactus: DestroyerOfModules; walkedTree: boolean; modules?: Set; constructor(dir: string, quiet: boolean) { this.baseDir = normalizePath(dir); this.quiet = quiet; this.galactus = new DestroyerOfModules({ rootDirectory: dir, shouldKeepModuleTest: (module, isDevDep) => this.shouldKeepModule(module, isDevDep), }); this.walkedTree = false; } setModules(moduleMap: ModuleMap) { const modulePaths = Array.from(moduleMap.keys()).map( (modulePath) => `/${normalizePath(modulePath)}`, ); this.modules = new Set(modulePaths); this.walkedTree = true; } async pruneModule(name: string) { if (this.walkedTree) { return this.isProductionModule(name); } else { const moduleMap = await this.galactus.collectKeptModules({ relativePaths: true, }); this.setModules(moduleMap); return this.isProductionModule(name); } } shouldKeepModule(module: Module, isDevDep: boolean) { if (isDevDep || module.depType === DepType.ROOT) { return false; } if (ELECTRON_MODULES.includes(module.name)) { if (!this.quiet) console.warn( `Found '${module.name}' but not as a devDependency, pruning anyway`, ); return false; } return true; } isProductionModule(name: string): boolean { return this.modules?.has(name) ?? false; } } function isNodeModuleFolder(pathToCheck: string) { return ( path.basename(path.dirname(pathToCheck)) === "node_modules" || (path.basename(path.dirname(pathToCheck)).startsWith("@") && path.basename(path.resolve(pathToCheck, `..${path.sep}..`)) === "node_modules") ); } export async function isModule(pathToCheck: string) { return ( (await fs.existsSync(path.join(pathToCheck, "package.json"))) && isNodeModuleFolder(pathToCheck) ); } export { Pruner }; ================================================ FILE: bindings/packager/nodejs/src-ts/plugins/index.ts ================================================ import path from "path"; import fs from "fs-extra"; import type { Config } from "../config"; import electron from "./electron"; import merge from "deepmerge"; export interface PackageJson { name?: string; productName?: string; version?: string; packager: Partial | null | undefined; } function getPackageJsonPath(): string | null { let appDir = process.cwd(); while (appDir.length && appDir[appDir.length - 1] !== path.sep) { const filepath = path.join(appDir, "package.json"); if (fs.existsSync(filepath)) { return filepath; } appDir = path.normalize(path.join(appDir, "..")); } return null; } export default async function run(): Promise | null> { const packageJsonPath = getPackageJsonPath(); if (packageJsonPath === null) { return null; } const packageJson = JSON.parse( (await fs.readFile(packageJsonPath)).toString(), ) as PackageJson; let config = packageJson.packager || null; try { const electronConfig = await electron( path.dirname(packageJsonPath), packageJson, ); if (electronConfig) { config = config ? merge(electronConfig, config) : electronConfig; } if (config?.outDir) { await fs.ensureDir(config.outDir); } return config; } catch { return null; } } ================================================ FILE: bindings/packager/nodejs/tsconfig.json ================================================ { "compilerOptions": { "target": "es2016", "lib": ["es6"], "module": "commonjs", "rootDir": "src-ts", "resolveJsonModule": true, "allowJs": true, "outDir": "build", "declaration": true, "declarationDir": "build", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitAny": true, "skipLibCheck": true }, "exclude": [ "build/", "__test__/", "index.js", "packager.js", "generate-config-type.js" ] } ================================================ FILE: bindings/resource-resolver/nodejs/.cargo/config.toml ================================================ [target.aarch64-unknown-linux-musl] linker = "aarch64-linux-musl-gcc" rustflags = ["-C", "target-feature=-crt-static"] ================================================ FILE: bindings/resource-resolver/nodejs/.npmignore ================================================ target Cargo.lock .cargo .github npm .eslintrc .prettierignore rustfmt.toml yarn.lock *.node .yarn __test__ renovate.json ================================================ FILE: bindings/resource-resolver/nodejs/CHANGELOG.md ================================================ # Changelog ## \[0.1.2] ### Dependencies - Upgraded to `cargo-packager-resource-resolver@0.1.2` - Upgraded to `cargo-packager-utils@0.1.1` ## \[0.1.1] ### Dependencies - Upgraded to `cargo-packager-resource-resolver@0.1.1` ## \[0.1.0] - [`cd0242b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/cd0242b8a41b2f7ecb78dfbae04b3a2e1c72c931) Initial Release. ### Dependencies - Upgraded to `cargo-packager-utils@0.1.0` - Upgraded to `cargo-packager-resource-resolver@0.1.0` ================================================ FILE: bindings/resource-resolver/nodejs/Cargo.toml ================================================ [package] name = "crabnebula_packager_resource_resolver" version = "0.0.0" publish = false edition = { workspace = true } license = { workspace = true } repository = { workspace = true } [lib] crate-type = ["cdylib"] [dependencies] # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix napi = { workspace = true, features = ["napi4"] } napi-derive = { workspace = true } cargo-packager-resource-resolver = { path = "../../../crates/resource-resolver" } dunce.workspace = true [build-dependencies] napi-build = { workspace = true } ================================================ FILE: bindings/resource-resolver/nodejs/README.md ================================================ # @crabnebula/packager-resource-resolver Resource resolver for apps that was packaged by [`@crabnebula/packager`](https://www.npmjs.com/package/@crabnebula/packager). It resolves the root path which contains resources, which was set using the `resources` field of [cargo packager configuration](https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html). ## Get the resource path ```ts import { resourcesDir, PackageFormat, } from "@crabnebula/packager-resource-resolver"; const dir = resourcesDir(PackageFormat.Nsis); ``` ## Licenses MIT or MIT/Apache 2.0 where applicable. ================================================ FILE: bindings/resource-resolver/nodejs/__test__/index.spec.mjs ================================================ import test from "ava"; import { resourcesDir, PackageFormat } from "../index.js"; test("resolve resource directory", async (t) => { const dir = resourcesDir(PackageFormat.Nsis); t.is(typeof dir, "string"); }); ================================================ FILE: bindings/resource-resolver/nodejs/build.rs ================================================ extern crate napi_build; fn main() { napi_build::setup(); } ================================================ FILE: bindings/resource-resolver/nodejs/fix-types.js ================================================ // Due to a NAPI-rs bug? still unconfirmed // index.d.ts will contain a duplicate definition of `PackageFromat` enum // and we only need the second definition. // This script sole purpose is to remove the extra definition. const { readFileSync, writeFileSync } = require("fs"); const { join } = require("path"); const typesPath = join(__dirname, "index.d.ts"); const types = readFileSync(typesPath, "utf8"); let out = ""; let inRemoval = false; for (const line of types.split("\n")) { if (inRemoval) { if (line === "}") inRemoval = false; continue; } const startOfRemoval = line.startsWith( "/** Types of supported packages by [`cargo-packager`](https://docs.rs/cargo-packager). */", ); if (startOfRemoval) { inRemoval = true; continue; } out += line + "\n"; } writeFileSync(typesPath, out); const problematicCode = `const { PackageFormat, PackageFormat, resourcesDir } = nativeBinding module.exports.PackageFormat = PackageFormat module.exports.PackageFormat = PackageFormat module.exports.resourcesDir = resourcesDir`; const correctCode = `const { PackageFormat, resourcesDir } = nativeBinding module.exports.PackageFormat = PackageFormat module.exports.resourcesDir = resourcesDir`; const indexPath = join(__dirname, "index.js"); const indexContent = readFileSync(indexPath, "utf8"); writeFileSync(indexPath, indexContent.replace(problematicCode, correctCode)); ================================================ FILE: bindings/resource-resolver/nodejs/index.d.ts ================================================ /* tslint:disable */ /* eslint-disable */ /* auto-generated by NAPI-RS */ /** Types of supported packages by [`@crabnebula/packager`](https://www.npmjs.com/package/@crabnebula/packager) */ export const enum PackageFormat { /** The macOS application bundle (.app). */ App = "App", /** The macOS DMG package (.dmg). */ Dmg = "Dmg", /** The Microsoft Software Installer (.msi) through WiX Toolset. */ Wix = "Wix", /** The NSIS installer (.exe). */ Nsis = "Nsis", /** The Linux Debian package (.deb). */ Deb = "Deb", /** The Linux AppImage package (.AppImage). */ AppImage = "AppImage", } /** Retrieve the resource path of your app, packaged with cargo packager. */ export function resourcesDir(packageFormat: PackageFormat): string; ================================================ FILE: bindings/resource-resolver/nodejs/index.js ================================================ /* tslint:disable */ /* eslint-disable */ /* prettier-ignore */ /* auto-generated by NAPI-RS */ const { existsSync, readFileSync } = require('fs') const { join } = require("path"); const { platform, arch } = process; let nativeBinding = null; let localFileExisted = false; let loadError = null; function isMusl() { // For Node 10 if (!process.report || typeof process.report.getReport !== "function") { try { const lddPath = require("child_process") .execSync("which ldd") .toString() .trim(); return readFileSync(lddPath, "utf8").includes("musl"); } catch (e) { return true; } } else { const { glibcVersionRuntime } = process.report.getReport().header; return !glibcVersionRuntime; } } switch (platform) { case "android": switch (arch) { case "arm64": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.android-arm64.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.android-arm64.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-android-arm64"); } } catch (e) { loadError = e; } break; case "arm": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.android-arm-eabi.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.android-arm-eabi.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-android-arm-eabi"); } } catch (e) { loadError = e; } break; default: throw new Error(`Unsupported architecture on Android ${arch}`); } break; case "win32": switch (arch) { case "x64": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.win32-x64-msvc.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.win32-x64-msvc.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-win32-x64-msvc"); } } catch (e) { loadError = e; } break; case "ia32": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.win32-ia32-msvc.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.win32-ia32-msvc.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-win32-ia32-msvc"); } } catch (e) { loadError = e; } break; case "arm64": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.win32-arm64-msvc.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.win32-arm64-msvc.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-win32-arm64-msvc"); } } catch (e) { loadError = e; } break; default: throw new Error(`Unsupported architecture on Windows: ${arch}`); } break; case "darwin": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.darwin-universal.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.darwin-universal.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-darwin-universal"); } break; } catch {} switch (arch) { case "x64": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.darwin-x64.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.darwin-x64.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-darwin-x64"); } } catch (e) { loadError = e; } break; case "arm64": localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.darwin-arm64.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.darwin-arm64.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-darwin-arm64"); } } catch (e) { loadError = e; } break; default: throw new Error(`Unsupported architecture on macOS: ${arch}`); } break; case "freebsd": if (arch !== "x64") { throw new Error(`Unsupported architecture on FreeBSD: ${arch}`); } localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.freebsd-x64.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.freebsd-x64.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-freebsd-x64"); } } catch (e) { loadError = e; } break; case "linux": switch (arch) { case "x64": if (isMusl()) { localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.linux-x64-musl.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.linux-x64-musl.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-linux-x64-musl"); } } catch (e) { loadError = e; } } else { localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.linux-x64-gnu.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.linux-x64-gnu.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-linux-x64-gnu"); } } catch (e) { loadError = e; } } break; case "arm64": if (isMusl()) { localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.linux-arm64-musl.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.linux-arm64-musl.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-linux-arm64-musl"); } } catch (e) { loadError = e; } } else { localFileExisted = existsSync( join(__dirname, "packager-resource-resolver.linux-arm64-gnu.node"), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.linux-arm64-gnu.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-linux-arm64-gnu"); } } catch (e) { loadError = e; } } break; case "arm": localFileExisted = existsSync( join( __dirname, "packager-resource-resolver.linux-arm-gnueabihf.node", ), ); try { if (localFileExisted) { nativeBinding = require("./packager-resource-resolver.linux-arm-gnueabihf.node"); } else { nativeBinding = require("@crabnebula/packager-resource-resolver-linux-arm-gnueabihf"); } } catch (e) { loadError = e; } break; default: throw new Error(`Unsupported architecture on Linux: ${arch}`); } break; default: throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); } if (!nativeBinding) { if (loadError) { throw loadError; } throw new Error(`Failed to load native binding`); } const { PackageFormat, resourcesDir } = nativeBinding; module.exports.PackageFormat = PackageFormat; module.exports.resourcesDir = resourcesDir; ================================================ FILE: bindings/resource-resolver/nodejs/npm/darwin-arm64/README.md ================================================ # `@crabnebula/packager-resource-resolver-darwin-arm64` This is the **aarch64-apple-darwin** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/darwin-arm64/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-darwin-arm64", "version": "0.0.0", "os": [ "darwin" ], "cpu": [ "arm64" ], "main": "packager-resource-resolver.darwin-arm64.node", "files": [ "packager-resource-resolver.darwin-arm64.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/resource-resolver/nodejs/npm/darwin-x64/README.md ================================================ # `@crabnebula/packager-resource-resolver-darwin-x64` This is the **x86_64-apple-darwin** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/darwin-x64/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-darwin-x64", "version": "0.0.0", "os": [ "darwin" ], "cpu": [ "x64" ], "main": "packager-resource-resolver.darwin-x64.node", "files": [ "packager-resource-resolver.darwin-x64.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-arm-gnueabihf/README.md ================================================ # `@crabnebula/packager-resource-resolver-linux-arm-gnueabihf` This is the **armv7-unknown-linux-gnueabihf** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-arm-gnueabihf/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-linux-arm-gnueabihf", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm" ], "main": "packager-resource-resolver.linux-arm-gnueabihf.node", "files": [ "packager-resource-resolver.linux-arm-gnueabihf.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-arm64-gnu/README.md ================================================ # `@crabnebula/packager-resource-resolver-linux-arm64-gnu` This is the **aarch64-unknown-linux-gnu** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-arm64-gnu/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-linux-arm64-gnu", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm64" ], "main": "packager-resource-resolver.linux-arm64-gnu.node", "files": [ "packager-resource-resolver.linux-arm64-gnu.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "glibc" ] } ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-arm64-musl/README.md ================================================ # `@crabnebula/packager-resource-resolver-linux-arm64-musl` This is the **aarch64-unknown-linux-musl** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-arm64-musl/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-linux-arm64-musl", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm64" ], "main": "packager-resource-resolver.linux-arm64-musl.node", "files": [ "packager-resource-resolver.linux-arm64-musl.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "musl" ] } ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-x64-gnu/README.md ================================================ # `@crabnebula/packager-resource-resolver-linux-x64-gnu` This is the **x86_64-unknown-linux-gnu** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-x64-gnu/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-linux-x64-gnu", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "x64" ], "main": "packager-resource-resolver.linux-x64-gnu.node", "files": [ "packager-resource-resolver.linux-x64-gnu.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "glibc" ] } ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-x64-musl/README.md ================================================ # `@crabnebula/packager-resource-resolver-linux-x64-musl` This is the **x86_64-unknown-linux-musl** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/linux-x64-musl/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-linux-x64-musl", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "x64" ], "main": "packager-resource-resolver.linux-x64-musl.node", "files": [ "packager-resource-resolver.linux-x64-musl.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "musl" ] } ================================================ FILE: bindings/resource-resolver/nodejs/npm/win32-arm64-msvc/README.md ================================================ # `@crabnebula/packager-resource-resolver-win32-arm64-msvc` This is the **aarch64-pc-windows-msvc** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/win32-arm64-msvc/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-win32-arm64-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "arm64" ], "main": "packager-resource-resolver.win32-arm64-msvc.node", "files": [ "packager-resource-resolver.win32-arm64-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/resource-resolver/nodejs/npm/win32-ia32-msvc/README.md ================================================ # `@crabnebula/packager-resource-resolver-win32-ia32-msvc` This is the **i686-pc-windows-msvc** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/win32-ia32-msvc/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-win32-ia32-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "ia32" ], "main": "packager-resource-resolver.win32-ia32-msvc.node", "files": [ "packager-resource-resolver.win32-ia32-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/resource-resolver/nodejs/npm/win32-x64-msvc/README.md ================================================ # `@crabnebula/packager-resource-resolver-win32-x64-msvc` This is the **x86_64-pc-windows-msvc** binary for `@crabnebula/packager-resource-resolver` ================================================ FILE: bindings/resource-resolver/nodejs/npm/win32-x64-msvc/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver-win32-x64-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "x64" ], "main": "packager-resource-resolver.win32-x64-msvc.node", "files": [ "packager-resource-resolver.win32-x64-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/resource-resolver/nodejs/package.json ================================================ { "name": "@crabnebula/packager-resource-resolver", "version": "0.1.2", "main": "./index.js", "types": "./index.d.ts", "napi": { "name": "packager-resource-resolver", "triples": { "additional": [ "aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "aarch64-pc-windows-msvc", "armv7-unknown-linux-gnueabihf", "x86_64-unknown-linux-musl", "i686-pc-windows-msvc" ] } }, "license": "MIT", "scripts": { "artifacts": "napi artifacts", "build": "napi build --platform --profile release-size-optimized", "postbuild": "node ./fix-types.js", "build:debug": "napi build --platform", "prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID", "test": "ava --no-worker-threads --timeout 30m", "universal": "napi universal", "version": "napi version" }, "devDependencies": { "@napi-rs/cli": "^2.18.1", "@types/node": "^20.8.10", "ava": "^6.0.0" }, "ava": { "timeout": "3m" }, "engines": { "node": ">= 10" } } ================================================ FILE: bindings/resource-resolver/nodejs/src/lib.rs ================================================ use napi::{Result, Status}; use cargo_packager_resource_resolver::PackageFormat as ResolverPackageFormat; /// Types of supported packages by [`@crabnebula/packager`](https://www.npmjs.com/package/@crabnebula/packager) #[derive(Debug, Eq, PartialEq)] #[napi_derive::napi(string_enum)] pub enum PackageFormat { /// The macOS application bundle (.app). App, /// The macOS DMG package (.dmg). Dmg, /// The Microsoft Software Installer (.msi) through WiX Toolset. Wix, /// The NSIS installer (.exe). Nsis, /// The Linux Debian package (.deb). Deb, /// The Linux AppImage package (.AppImage). AppImage, } impl From for ResolverPackageFormat { fn from(value: PackageFormat) -> Self { match value { PackageFormat::App => ResolverPackageFormat::App, PackageFormat::Dmg => ResolverPackageFormat::Dmg, PackageFormat::Wix => ResolverPackageFormat::Wix, PackageFormat::Nsis => ResolverPackageFormat::Nsis, PackageFormat::Deb => ResolverPackageFormat::Deb, PackageFormat::AppImage => ResolverPackageFormat::AppImage, } } } /// Retrieve the resource path of your app, packaged with cargo packager. #[napi_derive::napi] pub fn resources_dir(package_format: PackageFormat) -> Result { cargo_packager_resource_resolver::resources_dir(package_format.into()) .map_err(|e| napi::Error::new(Status::GenericFailure, e.to_string())) .map(|p| dunce::simplified(&p).to_string_lossy().to_string()) } ================================================ FILE: bindings/updater/nodejs/.cargo/config.toml ================================================ [target.aarch64-unknown-linux-musl] linker = "aarch64-linux-musl-gcc" rustflags = ["-C", "target-feature=-crt-static"] ================================================ FILE: bindings/updater/nodejs/.npmignore ================================================ target Cargo.lock .cargo .github npm .eslintrc .prettierignore rustfmt.toml yarn.lock *.node .yarn __test__ renovate.json ================================================ FILE: bindings/updater/nodejs/CHANGELOG.md ================================================ # Changelog ## \[0.2.3] - [`d861f1a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/d861f1a6b1dfe585014e04234b33d49b1a895219) ([#356](https://www.github.com/crabnebula-dev/cargo-packager/pull/356)) Pull enhancements from tauri-plugin-updater. ### Dependencies - Upgraded to `cargo-packager-updater@0.2.3` ## \[0.2.2] ### Dependencies - Upgraded to `cargo-packager-updater@0.2.2` ## \[0.2.1] ### Dependencies - Upgraded to `cargo-packager-updater@0.2.1` - Upgraded to `cargo-packager-utils@0.1.1` ## \[0.2.0] - [`c16d17a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c16d17ae190f49be3f9e78c5441bee16c0f8fc69) Enable `rustls-tls` feature flag by default. ### Dependencies - Upgraded to `cargo-packager-updater@0.2.0` ## \[0.1.4] - [`3ee2290`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3ee2290df518103056b295dae426b38a65293048)([#147](https://www.github.com/crabnebula-dev/cargo-packager/pull/147)) Prevent powershell window from opening when the msi and nsis installer are executed. ### Dependencies - Upgraded to `cargo-packager-updater@0.1.4` ## \[0.1.3] - [`0e00ca2`](https://www.github.com/crabnebula-dev/cargo-packager/commit/0e00ca25fc0e71cad4bb7085edda067a184e5ec7)([#146](https://www.github.com/crabnebula-dev/cargo-packager/pull/146)) Enable native certificates via `rustls-native-certs`. ### Dependencies - Upgraded to `cargo-packager-updater@0.1.3` ## \[0.1.2] - [`005a55f`](https://www.github.com/crabnebula-dev/cargo-packager/commit/005a55fb27b92503b3d6f936cffb088ccf346c40)([#143](https://www.github.com/crabnebula-dev/cargo-packager/pull/143)) Fix the download callback parameters to be accurate to typescript definitions ### Dependencies - Upgraded to `cargo-packager-utils@0.1.0` - Upgraded to `cargo-packager-updater@0.1.2` ## \[0.1.1] - [`feb53a2`](https://www.github.com/crabnebula-dev/cargo-packager/commit/feb53a2f16ef2c8d93ff2d73a4eb318490f33471)([#102](https://www.github.com/crabnebula-dev/cargo-packager/pull/102)) Fix NSIS updater failing to launch when using `basicUi` mode. ### Dependencies - Upgraded to `cargo-packager-updater@0.1.1` ## \[0.1.0] - [`c4fa8fd`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c4fa8fd6334b7fd0c32710ea2df0b54aa6bde713) Initial release. ### Dependencies - Upgraded to `cargo-packager-updater@0.1.0` ================================================ FILE: bindings/updater/nodejs/Cargo.toml ================================================ [package] name = "crabnebula_updater" version = "0.0.0" publish = false edition = { workspace = true } license = { workspace = true } repository = { workspace = true } [lib] crate-type = ["cdylib"] [dependencies] # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix napi = { workspace = true, features = ["napi4", "async"] } napi-derive = { workspace = true } cargo-packager-updater = { path = "../../../crates/updater/", default-features = false } serde_json = { workspace = true } time = { workspace = true, features = ["formatting"] } [build-dependencies] napi-build = { workspace = true } [features] default = ["cargo-packager-updater/rustls-tls"] rustls-tls = ["cargo-packager-updater/rustls-tls"] native-tls = ["cargo-packager-updater/native-tls"] native-tls-vendored = ["cargo-packager-updater/native-tls-vendored"] ================================================ FILE: bindings/updater/nodejs/README.md ================================================ # @crabnebula/updater Updater for apps that was packaged by [`@crabnebula/packager`](https://www.npmjs.com/package/@crabnebula/packager). ```sh # pnpm pnpm add @crabnebula/updater # yarn yarn add @crabnebula/updater # npm npm i @crabnebula/updater ``` ## Checking for an update you can check for an update using `checkUpdate` function which require the current version of the app and an options object that specifies the endpoints to request updates from and the public key of the update signature. ```js import { checkUpdate } from "@crabnebula/updater"; let update = await checkUpdate("0.1.0", { endpoints: ["http://myserver.com/updates"], pubkey: "", }); if (update !== null) { update.downloadAndInstall(); } else { // there is no updates } ``` ## Endpoints Each endpoint optionally could have `{{arch}}`, `{{target}}` or `{{current_version}}` which will be detected and replaced with the appropriate value before making a request to the endpoint. - `{{current_version}}`: The version of the app that is requesting the update. - `{{target}}`: The operating system name (one of `linux`, `windows` or `macos`). - `{{arch}}`: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`). for example: ``` https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}} ``` will turn into ``` https://releases.myapp.com/windows/x86_64/0.1.0 ``` if you need more data, you can set additional request headers in the options object pass to `checkUpdate` to your liking. ## Endpoint Response The updater expects the endpoint to respond with 2 possible reponses: 1. [`204 No Content`](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2.5) in case there is no updates available. 2. [`200 OK`](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2.1) and a JSON response that could be either a JSON representing all available platform updates or if using endpoints variables (see above) or a header to attach the current updater target, then it can just return information for the requested target. The JSON response is expected to have these fields set: - `version`: must be a valid semver, with or without a leading `v``, meaning that both `1.0.0`and`v1.0.0`are valid. - `url`or`platforms.[target].url`: must be a valid url to the update bundle. - `signature`or`platforms.[target].signature`: must be the content of the generated `.sig`file. The signature may change each time you run build your app so make sure to always update it. - `format`or`platforms.[target].format`: must be one of `app`, `appimage`, `nsis`or`wix`. > [!NOTE] > if using `platforms` object, each key is in the `OS-ARCH` format, where `OS` is one of `linux`, `macos` or `windows`, and `ARCH` is one of `x86_64`, `aarch64`, `i686` or `armv7`, see the example below. It can also contain these optional fields: - `notes`: Here you can add notes about the update, like release notes. - `pub_date`: must be formatted according to [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.8) if present. Here is an example of the two expected JSON formats: - **JSON for all platforms** ```json { "version": "v1.0.0", "notes": "Test version", "pub_date": "2020-06-22T19:25:57Z", "platforms": { "macos-x86_64": { "signature": "Content of app.tar.gz.sig", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz", "format": "app" }, "macos-aarch64": { "signature": "Content of app.tar.gz.sig", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-aarch64.app.tar.gz", "format": "app" }, "linux-x86_64": { "signature": "Content of app.AppImage.sig", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-amd64.AppImage.tar.gz", "format": "appimage" }, "windows-x86_64": { "signature": "Content of app-setup.exe.sig or app.msi.sig, depending on the chosen format", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x64-setup.nsis.zip", "format": "nsis or wix depending on the chosen format" } } } ``` - **JSON for one platform** ```json { "version": "0.2.0", "pub_date": "2020-09-18T12:29:53+01:00", "url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz", "signature": "Content of the relevant .sig file", "format": "app or nsis or wix or appimage depending on the release target and the chosen format", "notes": "These are some release notes" } ``` ## Update install mode on Windows You can specify which install mode to use on Windows using `windows.installMode` in the options object which can be one of: - `"Passive"`: There will be a small window with a progress bar. The update will be installed without requiring any user interaction. Generally recommended and the default mode. - `"BasicUi"`: There will be a basic user interface shown which requires user interaction to finish the installation. - `"Quiet"`: There will be no progress feedback to the user. With this mode the installer cannot request admin privileges by itself so it only works in user-wide installations or when your app itself already runs with admin privileges. Generally not recommended. ## Licenses MIT or MIT/Apache 2.0 where applicable. ================================================ FILE: bindings/updater/nodejs/__test__/app/index.html ================================================ Hello World!

Hello World!

We are using Node.js , Chromium , and Electron . ================================================ FILE: bindings/updater/nodejs/__test__/app/main.js ================================================ const { app } = require("electron"); const { join } = require("node:path"); const { checkUpdate } = require("@crabnebula/updater"); const UPDATER_PUB_KEY = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQ2Njc0OTE5Mzk2Q0ExODkKUldTSm9XdzVHVWxuUmtJdjB4RnRXZGVqR3NQaU5SVitoTk1qNFFWQ3pjL2hZWFVDOFNrcEVvVlcK"; const CURRENT_VERSION = "{{version}}"; app.whenReady().then(async () => { console.log(CURRENT_VERSION); const updaterFormat = process.env["UPDATER_FORMAT"]; const appimg = process.env["APPIMAGE"]; const isLinux = process.platfrom !== "win32" && process.platfrom !== "darwin"; try { const update = await checkUpdate(CURRENT_VERSION, { pubkey: UPDATER_PUB_KEY, endpoints: ["http://localhost:3007"], executablePath: isLinux && appimg ? appimg : undefined, windows: { installerArgs: // /D sets the default installation directory ($INSTDIR), // overriding InstallDir and InstallDirRegKey. // It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces. // Only absolute paths are supported. // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder updaterFormat === "nsis" ? [`/D=${join(process.execPath, "..")}`] : undefined, }, }); if (update) { try { await update.downloadAndInstall(); process.exit(0); } catch (e) { console.error(e); process.exit(1); } } else { process.exit(0); } } catch (e) { console.error(e); process.exit(1); } }); ================================================ FILE: bindings/updater/nodejs/__test__/app/package.json ================================================ { "name": "electron-app", "productName": "ElectronApp", "version": "1.0.0", "description": "Hello World!", "main": "main.js", "repository": { "type": "git", "url": "git+https://github.com/crabnebula-dev/cargo-packager.git" }, "author": "CrabNebula Ltd.", "license": "MIT", "bugs": { "url": "https://github.com/crabnebula-dev/cargo-packager/issues" }, "homepage": "https://github.com/crabnebula-dev/cargo-packager#readme", "dependencies": { "@crabnebula/updater": "../.." }, "devDependencies": { "electron": "^35.7.5" }, "packager": { "outDir": "./dist", "identifier": "com.electron.example", "icons": [ "electron.png" ] } } ================================================ FILE: bindings/updater/nodejs/__test__/app/preload.js ================================================ window.addEventListener("DOMContentLoaded", () => { const replaceText = (selector, text) => { const element = document.getElementById(selector); if (element) element.innerText = text; }; for (const dependency of ["chrome", "node", "electron"]) { replaceText(`${dependency}-version`, process.versions[dependency]); } }); ================================================ FILE: bindings/updater/nodejs/__test__/index.spec.mjs ================================================ import test from "ava"; import * as fs from "fs/promises"; import { existsSync } from "fs"; import * as path from "path"; import { execa } from "execa"; import { fileURLToPath } from "url"; import { App } from "@tinyhttp/app"; import { packageAndSignApp } from "@crabnebula/packager"; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const __dirname = fileURLToPath(new URL(".", import.meta.url)); const isWin = process.platform === "win32"; const isMac = process.platform === "darwin"; test("it updates correctly", async (t) => { const UPDATER_PRIVATE_KEY = await fs.readFile( path.join(__dirname, "../../../../crates/updater/tests/dummy.key"), { encoding: "utf8", }, ); process.chdir(path.join(__dirname, "app")); await execa("yarn", ["install"]); const buildApp = async (version, updaterFormats) => { const content = await fs.readFile("main.js", { encoding: "utf8", }); await fs.writeFile("main.js", content.replace("{{version}}", version)); try { await packageAndSignApp( { formats: updaterFormats, version, }, { privateKey: UPDATER_PRIVATE_KEY, password: "", }, { verbosity: 2, }, ); } catch (e) { console.error("failed to package app"); console.error(e); } finally { const content = await fs.readFile("main.js", { encoding: "utf8", }); await fs.writeFile("main.js", content.replace(version, "{{version}}")); } }; // bundle app update const formats = isWin ? ["nsis", "wix"] : isMac ? ["app"] : ["appimage"]; await buildApp("1.0.0", formats); const generatedPackages = isWin ? [ ["nsis", path.join("dist", `ElectronApp_1.0.0_x64-setup.exe`)], ["wix", path.join("dist", `ElectronApp_1.0.0_x64_en-US.msi`)], ] : isMac ? [["app", path.join("dist", "ElectronApp.app.tar.gz")]] : [["appimage", path.join("dist", `electron-app_1.0.0_x86_64.AppImage`)]]; for (let [format, updatePackagePath] of generatedPackages) { const signaturePath = path.format({ name: updatePackagePath, ext: ".sig", }); const signature = await fs.readFile(signaturePath, { encoding: "utf8", }); // on macOS, generated bundle doesn't have the version in its name // so we need to move it otherwise it'll be overwritten when we build the next app if (isMac) { const info = path.parse(updatePackagePath); const newPath = path.format({ dir: info.dir, base: `update-1.0.0-${info.base}`, }); await fs.rename(updatePackagePath, newPath); updatePackagePath = newPath; } // start the updater server const server = new App() .get("/", (_, res) => { const platforms = {}; const target = `${isWin ? "windows" : isMac ? "macos" : "linux"}-${ process.arch === "x64" ? "x86_64" : process.arch === "arm64" ? "aarch64" : "i686" }`; platforms[target] = { signature, url: "http://localhost:3007/download", format, }; res.status(200).json({ version: "1.0.0", date: new Date().toISOString(), platforms, }); }) .get("/download", (_req, res) => { res .status(200) .sendFile(path.join(__dirname, "app", updatePackagePath)); }) .listen(3007); // bundle initial app version await buildApp("0.1.0", [format]); // install the inital app on Windows to `installdir` if (isWin) { const installDir = path.join(__dirname, "app", "dist", "installdir"); if (existsSync(installDir)) await fs.rm(installDir, { recursive: true, }); await fs.mkdir(installDir); const isNsis = format === "nsis"; const installerArg = `"${path.join( "dist", isNsis ? `ElectronApp_0.1.0_x64-setup.exe` : `ElectronApp_0.1.0_x64_en-US.msi`, )}"`; await execa("powershell.exe", [ "-NoProfile", "-WindowStyle", "Hidden", "Start-Process", installerArg, "-Wait", "-ArgumentList", `${isNsis ? "/P" : "/passive"}, ${ isNsis ? "/D" : "INSTALLDIR" }=${installDir}`, ]); } const app = path.join( "dist", isWin ? "installdir/ElectronApp.exe" : isMac ? "ElectronApp.app/Contents/MacOS/ElectronApp" : `electron-app_0.1.0_x86_64.AppImage`, ); // save the current creation time const stats = await fs.stat(app); const ctime1 = stats.birthtime; // run initial app try { await execa(app, { stdio: "inherit", // This is read by the updater app test env: { UPDATER_FORMAT: format, }, }); } catch (e) { console.error(`failed to start initial app: ${e}`); } // the test app is electron which is huge in size // and the installation takes a who;e // so wait 30 secs to make sure the installer has finished await sleep(30000); // wait until the update is finished and the new version has been installed // before starting another updater test, this is because we use the same starting binary // and we can't use it while the updater is installing it let counter = 0; while (true) { // check if the main binary creation time has changed since `ctime1` const stats = await fs.stat(app); if (ctime1 !== stats.birthtime) { try { const { stdout, stderr } = await execa(app); const lines = stdout.split(isWin ? "\r\n" : "\n"); const version = lines.filter((l) => l)[0]; if (version === "1.0.0") { console.log(`app is updated, new version: ${version}`); break; } console.log(`unexpected output (stdout): ${stdout}`); console.log(`stderr: ${stderr}`); } catch (e) { console.error(`failed to check if app was updated: ${e}`); } } counter += 1; if (counter == 10) { console.error( "updater test timedout and couldn't verify the update has happened", ); break; } await sleep(5000); } server.close(); } t.pass("Test successful"); }); ================================================ FILE: bindings/updater/nodejs/build.rs ================================================ extern crate napi_build; fn main() { napi_build::setup(); } ================================================ FILE: bindings/updater/nodejs/index.d.ts ================================================ /* tslint:disable */ /* eslint-disable */ /* auto-generated by NAPI-RS */ export const enum WindowsUpdateInstallMode { /** Specifies there's a basic UI during the installation process, including a final dialog box at the end. */ BasicUi = 'BasicUi', /** * The quiet mode means there's no user interaction required. * Requires admin privileges if the installer does. */ Quiet = 'Quiet', /** Specifies unattended mode, which means the installation only shows a progress bar. */ Passive = 'Passive' } export interface UpdaterWindowsOptions { /** Additional arguments given to the NSIS or WiX installer. */ installerArgs?: Array /** The installation mode for the update on Windows. Defaults to `passive`. */ installMode?: WindowsUpdateInstallMode } export interface Options { /** The updater endpoints. */ endpoints: Array /** Signature public key. */ pubkey: string /** The Windows options for the updater. */ windows?: UpdaterWindowsOptions /** The target of the executable. */ target?: string /** Path to the executable file. */ executablePath?: string /** Headers to use when checking and when downloading the update. */ headers?: Record /** Request timeout in milliseconds. */ timeout?: number } /** Supported update format */ export const enum UpdateFormat { /** The NSIS installer (.exe). */ Nsis = 0, /** The Microsoft Software Installer (.msi) through WiX Toolset. */ Wix = 1, /** The Linux AppImage package (.AppImage). */ AppImage = 2, /** The macOS application bundle (.app). */ App = 3 } export function checkUpdate(currentVersion: string, options: Options): Promise export class Update { /** Signing public key */ pubkey: string /** Version used to check for update */ currentVersion: string /** Version announced */ version: string /** Target */ target: string /** Extract path */ extractPath: string /** Download URL announced */ downloadUrl: string /** Signature announced */ signature: string /** Request headers */ headers: Record /** Update format */ format: UpdateFormat /** The Windows options for the updater. */ windows?: UpdaterWindowsOptions /** Update description */ body?: string /** Update publish date */ date?: string /** Request timeout */ timeout?: number download(onChunk?: (chunkLength: number, contentLength: number | null) => void, onDownloadFinished?: () => void): Promise install(buffer: ArrayBuffer): Promise downloadAndInstall(onChunk?: (chunkLength: number, contentLength?: number) => void, onDownloadFinished?: () => void): Promise } ================================================ FILE: bindings/updater/nodejs/index.js ================================================ /* tslint:disable */ /* eslint-disable */ /* prettier-ignore */ /* auto-generated by NAPI-RS */ const { existsSync, readFileSync } = require('fs') const { join } = require('path') const { platform, arch } = process let nativeBinding = null let localFileExisted = false let loadError = null function isMusl() { // For Node 10 if (!process.report || typeof process.report.getReport !== 'function') { try { const lddPath = require('child_process').execSync('which ldd').toString().trim() return readFileSync(lddPath, 'utf8').includes('musl') } catch (e) { return true } } else { const { glibcVersionRuntime } = process.report.getReport().header return !glibcVersionRuntime } } switch (platform) { case 'android': switch (arch) { case 'arm64': localFileExisted = existsSync(join(__dirname, 'updater.android-arm64.node')) try { if (localFileExisted) { nativeBinding = require('./updater.android-arm64.node') } else { nativeBinding = require('@crabnebula/updater-android-arm64') } } catch (e) { loadError = e } break case 'arm': localFileExisted = existsSync(join(__dirname, 'updater.android-arm-eabi.node')) try { if (localFileExisted) { nativeBinding = require('./updater.android-arm-eabi.node') } else { nativeBinding = require('@crabnebula/updater-android-arm-eabi') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Android ${arch}`) } break case 'win32': switch (arch) { case 'x64': localFileExisted = existsSync( join(__dirname, 'updater.win32-x64-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.win32-x64-msvc.node') } else { nativeBinding = require('@crabnebula/updater-win32-x64-msvc') } } catch (e) { loadError = e } break case 'ia32': localFileExisted = existsSync( join(__dirname, 'updater.win32-ia32-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.win32-ia32-msvc.node') } else { nativeBinding = require('@crabnebula/updater-win32-ia32-msvc') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync( join(__dirname, 'updater.win32-arm64-msvc.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.win32-arm64-msvc.node') } else { nativeBinding = require('@crabnebula/updater-win32-arm64-msvc') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Windows: ${arch}`) } break case 'darwin': localFileExisted = existsSync(join(__dirname, 'updater.darwin-universal.node')) try { if (localFileExisted) { nativeBinding = require('./updater.darwin-universal.node') } else { nativeBinding = require('@crabnebula/updater-darwin-universal') } break } catch {} switch (arch) { case 'x64': localFileExisted = existsSync(join(__dirname, 'updater.darwin-x64.node')) try { if (localFileExisted) { nativeBinding = require('./updater.darwin-x64.node') } else { nativeBinding = require('@crabnebula/updater-darwin-x64') } } catch (e) { loadError = e } break case 'arm64': localFileExisted = existsSync( join(__dirname, 'updater.darwin-arm64.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.darwin-arm64.node') } else { nativeBinding = require('@crabnebula/updater-darwin-arm64') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on macOS: ${arch}`) } break case 'freebsd': if (arch !== 'x64') { throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) } localFileExisted = existsSync(join(__dirname, 'updater.freebsd-x64.node')) try { if (localFileExisted) { nativeBinding = require('./updater.freebsd-x64.node') } else { nativeBinding = require('@crabnebula/updater-freebsd-x64') } } catch (e) { loadError = e } break case 'linux': switch (arch) { case 'x64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'updater.linux-x64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.linux-x64-musl.node') } else { nativeBinding = require('@crabnebula/updater-linux-x64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'updater.linux-x64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.linux-x64-gnu.node') } else { nativeBinding = require('@crabnebula/updater-linux-x64-gnu') } } catch (e) { loadError = e } } break case 'arm64': if (isMusl()) { localFileExisted = existsSync( join(__dirname, 'updater.linux-arm64-musl.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.linux-arm64-musl.node') } else { nativeBinding = require('@crabnebula/updater-linux-arm64-musl') } } catch (e) { loadError = e } } else { localFileExisted = existsSync( join(__dirname, 'updater.linux-arm64-gnu.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.linux-arm64-gnu.node') } else { nativeBinding = require('@crabnebula/updater-linux-arm64-gnu') } } catch (e) { loadError = e } } break case 'arm': localFileExisted = existsSync( join(__dirname, 'updater.linux-arm-gnueabihf.node') ) try { if (localFileExisted) { nativeBinding = require('./updater.linux-arm-gnueabihf.node') } else { nativeBinding = require('@crabnebula/updater-linux-arm-gnueabihf') } } catch (e) { loadError = e } break default: throw new Error(`Unsupported architecture on Linux: ${arch}`) } break default: throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) } if (!nativeBinding) { if (loadError) { throw loadError } throw new Error(`Failed to load native binding`) } const { WindowsUpdateInstallMode, UpdateFormat, Update, checkUpdate } = nativeBinding module.exports.WindowsUpdateInstallMode = WindowsUpdateInstallMode module.exports.UpdateFormat = UpdateFormat module.exports.Update = Update module.exports.checkUpdate = checkUpdate ================================================ FILE: bindings/updater/nodejs/npm/darwin-arm64/README.md ================================================ # `@crabnebula/updater-darwin-arm64` This is the **aarch64-apple-darwin** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/darwin-arm64/package.json ================================================ { "name": "@crabnebula/updater-darwin-arm64", "version": "0.0.0", "os": [ "darwin" ], "cpu": [ "arm64" ], "main": "updater.darwin-arm64.node", "files": [ "updater.darwin-arm64.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/updater/nodejs/npm/darwin-x64/README.md ================================================ # `@crabnebula/updater-darwin-x64` This is the **x86_64-apple-darwin** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/darwin-x64/package.json ================================================ { "name": "@crabnebula/updater-darwin-x64", "version": "0.0.0", "os": [ "darwin" ], "cpu": [ "x64" ], "main": "updater.darwin-x64.node", "files": [ "updater.darwin-x64.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/updater/nodejs/npm/linux-arm-gnueabihf/README.md ================================================ # `@crabnebula/updater-linux-arm-gnueabihf` This is the **armv7-unknown-linux-gnueabihf** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/linux-arm-gnueabihf/package.json ================================================ { "name": "@crabnebula/updater-linux-arm-gnueabihf", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm" ], "main": "updater.linux-arm-gnueabihf.node", "files": [ "updater.linux-arm-gnueabihf.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/updater/nodejs/npm/linux-arm64-gnu/README.md ================================================ # `@crabnebula/updater-linux-arm64-gnu` This is the **aarch64-unknown-linux-gnu** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/linux-arm64-gnu/package.json ================================================ { "name": "@crabnebula/updater-linux-arm64-gnu", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm64" ], "main": "updater.linux-arm64-gnu.node", "files": [ "updater.linux-arm64-gnu.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "glibc" ] } ================================================ FILE: bindings/updater/nodejs/npm/linux-arm64-musl/README.md ================================================ # `@crabnebula/updater-linux-arm64-musl` This is the **aarch64-unknown-linux-musl** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/linux-arm64-musl/package.json ================================================ { "name": "@crabnebula/updater-linux-arm64-musl", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "arm64" ], "main": "updater.linux-arm64-musl.node", "files": [ "updater.linux-arm64-musl.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "musl" ] } ================================================ FILE: bindings/updater/nodejs/npm/linux-x64-gnu/README.md ================================================ # `@crabnebula/updater-linux-x64-gnu` This is the **x86_64-unknown-linux-gnu** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/linux-x64-gnu/package.json ================================================ { "name": "@crabnebula/updater-linux-x64-gnu", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "x64" ], "main": "updater.linux-x64-gnu.node", "files": [ "updater.linux-x64-gnu.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "glibc" ] } ================================================ FILE: bindings/updater/nodejs/npm/linux-x64-musl/README.md ================================================ # `@crabnebula/updater-linux-x64-musl` This is the **x86_64-unknown-linux-musl** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/linux-x64-musl/package.json ================================================ { "name": "@crabnebula/updater-linux-x64-musl", "version": "0.0.0", "os": [ "linux" ], "cpu": [ "x64" ], "main": "updater.linux-x64-musl.node", "files": [ "updater.linux-x64-musl.node" ], "license": "MIT", "engines": { "node": ">= 10" }, "libc": [ "musl" ] } ================================================ FILE: bindings/updater/nodejs/npm/win32-arm64-msvc/README.md ================================================ # `@crabnebula/updater-win32-arm64-msvc` This is the **aarch64-pc-windows-msvc** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/win32-arm64-msvc/package.json ================================================ { "name": "@crabnebula/updater-win32-arm64-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "arm64" ], "main": "updater.win32-arm64-msvc.node", "files": [ "updater.win32-arm64-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/updater/nodejs/npm/win32-ia32-msvc/README.md ================================================ # `@crabnebula/updater-win32-ia32-msvc` This is the **i686-pc-windows-msvc** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/win32-ia32-msvc/package.json ================================================ { "name": "@crabnebula/updater-win32-ia32-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "ia32" ], "main": "updater.win32-ia32-msvc.node", "files": [ "updater.win32-ia32-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/updater/nodejs/npm/win32-x64-msvc/README.md ================================================ # `@crabnebula/updater-win32-x64-msvc` This is the **x86_64-pc-windows-msvc** binary for `@crabnebula/updater` ================================================ FILE: bindings/updater/nodejs/npm/win32-x64-msvc/package.json ================================================ { "name": "@crabnebula/updater-win32-x64-msvc", "version": "0.0.0", "os": [ "win32" ], "cpu": [ "x64" ], "main": "updater.win32-x64-msvc.node", "files": [ "updater.win32-x64-msvc.node" ], "license": "MIT", "engines": { "node": ">= 10" } } ================================================ FILE: bindings/updater/nodejs/package.json ================================================ { "name": "@crabnebula/updater", "version": "0.2.3", "main": "./index.js", "types": "./index.d.ts", "napi": { "name": "updater", "triples": { "additional": [ "aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "aarch64-pc-windows-msvc", "armv7-unknown-linux-gnueabihf", "x86_64-unknown-linux-musl", "i686-pc-windows-msvc" ] } }, "license": "MIT", "scripts": { "artifacts": "napi artifacts", "build": "napi build --platform --profile release-size-optimized", "build:debug": "napi build --platform", "prepublishOnly": "napi prepublish -t npm --gh-release-id $RELEASE_ID", "test": "ava --no-worker-threads --timeout 30m", "universal": "napi universal", "version": "napi version" }, "devDependencies": { "@napi-rs/cli": "^2.18.1", "@tinyhttp/app": "^2.2.1", "@types/node": "^20.8.10", "ava": "^6.2.0", "execa": "^9.0.0", "@crabnebula/packager": "workspace:" }, "ava": { "timeout": "3m" }, "engines": { "node": ">= 10" } } ================================================ FILE: bindings/updater/nodejs/src/from_impls.rs ================================================ use crate::{Options, Update, UpdateFormat, UpdaterWindowsOptions, WindowsUpdateInstallMode}; impl From for cargo_packager_updater::WindowsUpdateInstallMode { fn from(value: WindowsUpdateInstallMode) -> Self { match value { WindowsUpdateInstallMode::BasicUi => Self::BasicUi, WindowsUpdateInstallMode::Quiet => Self::Quiet, WindowsUpdateInstallMode::Passive => Self::Passive, } } } impl From for WindowsUpdateInstallMode { fn from(value: cargo_packager_updater::WindowsUpdateInstallMode) -> Self { match value { cargo_packager_updater::WindowsUpdateInstallMode::BasicUi => Self::BasicUi, cargo_packager_updater::WindowsUpdateInstallMode::Quiet => Self::Quiet, cargo_packager_updater::WindowsUpdateInstallMode::Passive => Self::Passive, } } } impl From for UpdaterWindowsOptions { fn from(value: cargo_packager_updater::WindowsConfig) -> Self { Self { installer_args: value.installer_args, install_mode: value.install_mode.map(Into::into), } } } impl From for cargo_packager_updater::WindowsConfig { fn from(value: UpdaterWindowsOptions) -> Self { Self { installer_args: value.installer_args, install_mode: value.install_mode.map(Into::into), } } } impl From for cargo_packager_updater::Config { fn from(value: Options) -> Self { Self { endpoints: value .endpoints .into_iter() .filter_map(|e| e.parse().ok()) .collect(), pubkey: value.pubkey, windows: value.windows.map(Into::into), } } } impl From for UpdateFormat { fn from(value: cargo_packager_updater::UpdateFormat) -> Self { match value { cargo_packager_updater::UpdateFormat::Nsis => Self::Nsis, cargo_packager_updater::UpdateFormat::Wix => Self::Wix, cargo_packager_updater::UpdateFormat::AppImage => Self::AppImage, cargo_packager_updater::UpdateFormat::App => Self::App, } } } impl From for cargo_packager_updater::UpdateFormat { fn from(value: UpdateFormat) -> Self { match value { UpdateFormat::Nsis => Self::Nsis, UpdateFormat::Wix => Self::Wix, UpdateFormat::AppImage => Self::AppImage, UpdateFormat::App => Self::App, } } } impl From for Update { fn from(value: cargo_packager_updater::Update) -> Self { Self { pubkey: value.config.pubkey, body: value.body, current_version: value.current_version, version: value.version, date: value.date.and_then(|d| { d.format(&time::format_description::well_known::Rfc3339) .ok() }), target: value.target, extract_path: value.extract_path.to_string_lossy().to_string(), download_url: value.download_url.to_string(), signature: value.signature, timeout: value.timeout.map(|t| t.as_millis() as u32), headers: value .headers .into_iter() .map(|(k, v)| { ( k.map(|k| k.to_string()).unwrap_or_default(), v.to_str().unwrap_or_default().to_string(), ) }) .collect(), format: value.format.into(), windows: value.config.windows.map(Into::into), } } } ================================================ FILE: bindings/updater/nodejs/src/lib.rs ================================================ use std::{collections::HashMap, str::FromStr, time::Duration}; use cargo_packager_updater::{ http::{HeaderMap, HeaderName, HeaderValue}, semver::Version, Updater, UpdaterBuilder, }; use napi::{ bindgen_prelude::AsyncTask, threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode}, Env, Error, JsArrayBuffer, Result, Status, Task, }; mod from_impls; #[napi_derive::napi(string_enum)] #[derive(Default)] pub enum WindowsUpdateInstallMode { /// Specifies there's a basic UI during the installation process, including a final dialog box at the end. BasicUi, /// The quiet mode means there's no user interaction required. /// Requires admin privileges if the installer does. Quiet, /// Specifies unattended mode, which means the installation only shows a progress bar. #[default] Passive, } #[derive(Clone)] #[napi_derive::napi(object)] pub struct UpdaterWindowsOptions { /// Additional arguments given to the NSIS or WiX installer. pub installer_args: Option>, /// The installation mode for the update on Windows. Defaults to `passive`. pub install_mode: Option, } #[napi_derive::napi(object)] pub struct Options { /// The updater endpoints. pub endpoints: Vec, /// Signature public key. pub pubkey: String, /// The Windows options for the updater. pub windows: Option, /// The target of the executable. pub target: Option, /// Path to the executable file. pub executable_path: Option, /// Headers to use when checking and when downloading the update. pub headers: Option>, /// Request timeout in milliseconds. pub timeout: Option, } impl Options { fn into_updater(mut self, current_version: Version) -> Result { let target = self.target.take(); let executable_path = self.executable_path.take(); let headers = self.headers.take(); let timeout = self.timeout.take(); let config: cargo_packager_updater::Config = self.into(); let mut builder = UpdaterBuilder::new(current_version, config); if let Some(target) = target { builder = builder.target(target); } if let Some(executable_path) = executable_path { builder = builder.executable_path(executable_path); } if let Some(timeout) = timeout { builder = builder.timeout(Duration::from_millis(timeout as u64)); } if let Some(headers) = headers { for (key, value) in headers { builder = builder.header(key, value).map_err(|e| { Error::new( Status::InvalidArg, format!("Failed to set header, probably invalid header values, {e}"), ) })?; } } builder.build().map_err(|e| { Error::new( Status::GenericFailure, format!("Failed to construct updater, {e}"), ) }) } } /// Supported update format #[napi_derive::napi] pub enum UpdateFormat { /// The NSIS installer (.exe). Nsis, /// The Microsoft Software Installer (.msi) through WiX Toolset. Wix, /// The Linux AppImage package (.AppImage). AppImage, /// The macOS application bundle (.app). App, } #[napi_derive::napi] pub struct Update { /// Signing public key pub pubkey: String, /// Version used to check for update pub current_version: String, /// Version announced pub version: String, /// Target pub target: String, /// Extract path pub extract_path: String, /// Download URL announced pub download_url: String, /// Signature announced pub signature: String, /// Request headers pub headers: HashMap, /// Update format pub format: UpdateFormat, /// The Windows options for the updater. pub windows: Option, /// Update description pub body: Option, /// Update publish date pub date: Option, /// Request timeout pub timeout: Option, } impl Update { fn create_update(&self) -> Result { Ok(cargo_packager_updater::Update { config: cargo_packager_updater::Config { pubkey: self.pubkey.clone(), windows: self.windows.clone().map(Into::into), ..Default::default() }, body: self.body.clone(), current_version: self.current_version.clone(), version: self.version.clone(), date: None, target: self.target.clone(), extract_path: self.extract_path.clone().into(), download_url: self.download_url.parse().map_err(|e| { Error::new( Status::GenericFailure, format!("Internal error, couldn't convert string to Url struct, {e}"), ) })?, signature: self.signature.clone(), timeout: self.timeout.map(|t| Duration::from_millis(t as u64)), headers: { let mut map = HeaderMap::new(); for (key, value) in &self.headers { map.insert(HeaderName::from_str(key).map_err(|e| { Error::new( Status::GenericFailure, format!("Internal error, couldn't construct header name from str , {e}"), ) })?, HeaderValue::from_str(value).map_err(|e| { Error::new( Status::GenericFailure, format!("Internal error, couldn't construct header value from str , {e}"), ) })?); } map }, format: self.format.into(), }) } } type TaskCallbackFunction = Option>; pub struct DownloadTask { update: cargo_packager_updater::Update, on_chunk: TaskCallbackFunction<(u32, Option)>, on_download_finished: TaskCallbackFunction<()>, } impl DownloadTask { pub fn create( update: &Update, on_chunk: TaskCallbackFunction<(u32, Option)>, on_download_finished: TaskCallbackFunction<()>, ) -> Result { Ok(Self { update: update.create_update()?, on_chunk, on_download_finished, }) } } impl Task for DownloadTask { type Output = Vec; type JsValue = JsArrayBuffer; fn compute(&mut self) -> Result { let on_chunk = |chunk_len: usize, content_len: Option| { if let Some(on_chunk) = &self.on_chunk { on_chunk.call( (chunk_len as _, content_len.map(|v| v as _)), ThreadsafeFunctionCallMode::NonBlocking, ); } }; let on_finish = || { if let Some(on_download_finished) = &self.on_download_finished { on_download_finished.call((), ThreadsafeFunctionCallMode::NonBlocking); } }; self.update .download_extended(on_chunk, on_finish) .map_err(|e| Error::new(Status::GenericFailure, e)) } fn resolve(&mut self, env: Env, output: Self::Output) -> Result { let mut buffer = env.create_arraybuffer(output.len())?; unsafe { std::ptr::copy(output.as_ptr(), buffer.as_mut_ptr(), output.len()) }; Ok(buffer.into_raw()) } } pub struct InstallTask { update: cargo_packager_updater::Update, bytes: Option>, } impl InstallTask { pub fn create(update: &Update, bytes: Vec) -> Result { Ok(Self { update: update.create_update()?, bytes: Some(bytes), }) } } impl Task for InstallTask { type Output = (); type JsValue = (); fn compute(&mut self) -> Result { self.update .install(self.bytes.take().unwrap()) .map_err(|e| Error::new(Status::GenericFailure, e)) } fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result { Ok(()) } } pub struct DownloadAndInstallTask { download_task: DownloadTask, } impl DownloadAndInstallTask { pub fn new(download_task: DownloadTask) -> Self { Self { download_task } } } impl Task for DownloadAndInstallTask { type Output = (); type JsValue = (); fn compute(&mut self) -> Result { let bytes = self.download_task.compute()?; self.download_task .update .install(bytes) .map_err(|e| Error::new(Status::GenericFailure, e)) } fn resolve(&mut self, _env: Env, _output: Self::Output) -> Result { Ok(()) } } pub struct CheckUpdateTask { updater: Updater, } impl CheckUpdateTask { pub fn create(current_version: String, options: Options) -> Result { let current_version = current_version.parse().map_err(|e| { Error::new( Status::InvalidArg, format!("Failed to parse string as a valid semver, {e}"), ) })?; let updater = options.into_updater(current_version)?; Ok(Self { updater }) } } impl Task for CheckUpdateTask { type Output = Option; type JsValue = Option; fn compute(&mut self) -> Result { self.updater.check().map_err(|e| { Error::new( Status::GenericFailure, format!("Failed to check for update, {e}"), ) }) } fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { Ok(output.map(Into::into)) } } #[napi_derive::napi] impl Update { #[napi( ts_args_type = "onChunk?: (chunkLength: number, contentLength: number | null) => void, onDownloadFinished?: () => void", ts_return_type = "Promise" )] pub fn download( &self, on_chunk: TaskCallbackFunction<(u32, Option)>, on_download_finish: TaskCallbackFunction<()>, ) -> Result> { DownloadTask::create(self, on_chunk, on_download_finish).map(AsyncTask::new) } #[napi(ts_return_type = "Promise", ts_args_type = "buffer: ArrayBuffer")] pub fn install(&self, bytes: JsArrayBuffer) -> Result> { let bytes = bytes.into_value()?; let bytes = bytes.as_ref().to_vec(); InstallTask::create(self, bytes).map(AsyncTask::new) } #[napi( ts_args_type = "onChunk?: (chunkLength: number, contentLength?: number) => void, onDownloadFinished?: () => void", ts_return_type = "Promise" )] pub fn download_and_install( &self, on_chunk: TaskCallbackFunction<(u32, Option)>, on_download_finish: TaskCallbackFunction<()>, ) -> Result> { let download_task = DownloadTask::create(self, on_chunk, on_download_finish)?; Ok(AsyncTask::new(DownloadAndInstallTask::new(download_task))) } } #[napi_derive::napi(ts_return_type = "Promise")] pub fn check_update( current_version: String, options: Options, ) -> Result> { Ok(AsyncTask::new(CheckUpdateTask::create( current_version, options, )?)) } ================================================ FILE: crates/config-schema-generator/Cargo.toml ================================================ [package] name = "cargo-packager-config-schema-generator" version = "0.0.0" publish = false authors.workspace = true edition.workspace = true license.workspace = true repository.workspace = true [build-dependencies] cargo-packager = { path = "../packager", features = ["schema"] } serde_json = { workspace = true } schemars = { workspace = true } ================================================ FILE: crates/config-schema-generator/build.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ error::Error, fs::File, io::{BufWriter, Write}, path::PathBuf, process::Command, }; pub fn main() -> Result<(), Box> { let schema = schemars::schema_for!(cargo_packager::Config); let schema_str = serde_json::to_string_pretty(&schema).unwrap(); let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?); for path in [ "../packager/schema.json", "../../bindings/packager/nodejs/schema.json", ] { let mut schema_file = BufWriter::new(File::create(crate_dir.join(path))?); write!(schema_file, "{schema_str}")?; } let _ = Command::new("node") .arg("./generate-config-type.js") .current_dir("../../bindings/packager/nodejs") .output(); Ok(()) } ================================================ FILE: crates/config-schema-generator/src/main.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT fn main() {} ================================================ FILE: crates/packager/CHANGELOG.md ================================================ # Changelog ## \[0.11.8] - [`6e6a10c`](https://www.github.com/crabnebula-dev/cargo-packager/commit/6e6a10cc1692973293966034dc4b798e3976d094) ([#321](https://www.github.com/crabnebula-dev/cargo-packager/pull/321)) Allow explicitly specifying the Package name for the .deb bundle. - [`8488d86`](https://www.github.com/crabnebula-dev/cargo-packager/commit/8488d868935166e873474743c346c2724205d73e) ([#377](https://www.github.com/crabnebula-dev/cargo-packager/pull/377)) Fixed a bug where "binaries" parameter in Cargo.toml would be ignored and all targets would be packaged. - [`b2b4916`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b2b4916d1b062272fc7e34b5ed55b4fe8c8cd03a) ([#376](https://www.github.com/crabnebula-dev/cargo-packager/pull/376)) Fix bug that prevents reading macos signing certificates from environment variables. - [`c34de36`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c34de365705db150eb101caa94adf42eff74f71a) ([#365](https://www.github.com/crabnebula-dev/cargo-packager/pull/365)) Change nsi template from using `association.ext` to `association.extensions`, to match struct field in `FileAssociation`. This allows file associations to be generated in `.nsi` files, and therefore in the final NSIS installer. ## \[0.11.7] - [`d49b606`](https://www.github.com/crabnebula-dev/cargo-packager/commit/d49b606ba8a612c833233ec8a6061481a2118639) ([#353](https://www.github.com/crabnebula-dev/cargo-packager/pull/353)) Allow using notarization credentials stored on the Keychain by providing the `APPLE_KEYCHAIN_PROFILE` environment variable. See `xcrun notarytool store-credentials` for more information. - [`b337564`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b337564c0e5a9de966b4124890dddea1e353acb4) ([#362](https://www.github.com/crabnebula-dev/cargo-packager/pull/362)) Updated linuxdeploy's AppImage plugin to not require libfuse on the user's system anymore. ## \[0.11.6] - [`b81b81f`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b81b81fbd7fd185edfc7652f535d0cfacb786ac9) ([#354](https://www.github.com/crabnebula-dev/cargo-packager/pull/354)) Changed the download URL of a dependency of the AppImage bundler to Tauri's mirror to resolve 404 errors. - [`735d6c4`](https://www.github.com/crabnebula-dev/cargo-packager/commit/735d6c4745911793cbcf5d929d8da288840bcf24) ([#345](https://www.github.com/crabnebula-dev/cargo-packager/pull/345)) Fixed a typo on the `digest_algorithm` config (was `digest-algorithim`). - [`5205088`](https://www.github.com/crabnebula-dev/cargo-packager/commit/5205088cd78412fb6cbe5e48a715524fcc5a2ee7) ([#340](https://www.github.com/crabnebula-dev/cargo-packager/pull/340)) Enhance sign error message. - [`55924d3`](https://www.github.com/crabnebula-dev/cargo-packager/commit/55924d3522c4ab1cfcb4436044e5ebad8adf241c) ([#334](https://www.github.com/crabnebula-dev/cargo-packager/pull/334)) Migrate from `winreg` crate to `windows-registry`. This adds new variants to the packager's `Error` type. ## \[0.11.5] - [`17194a9`](https://www.github.com/crabnebula-dev/cargo-packager/commit/17194a92aabd59c9e075105072ff939f5d55a107) ([#313](https://www.github.com/crabnebula-dev/cargo-packager/pull/313)) Added `linux > generateDesktopEntry` config to allow disabling generating a .desktop file on Linux bundles (defaults to true). - [`17c52f0`](https://www.github.com/crabnebula-dev/cargo-packager/commit/17c52f057d78340983689af3c00b1f2aeff3c417) ([#289](https://www.github.com/crabnebula-dev/cargo-packager/pull/289)) Added support to embedding additional apps in the macOS app bundle. - [`17c52f0`](https://www.github.com/crabnebula-dev/cargo-packager/commit/17c52f057d78340983689af3c00b1f2aeff3c417) ([#289](https://www.github.com/crabnebula-dev/cargo-packager/pull/289)) Added support to adding an `embedded.provisionprofile` file to the macOS bundle. - [`e010574`](https://www.github.com/crabnebula-dev/cargo-packager/commit/e010574c2efa4a1aa6b8e475a62bec46f24f2bc5) ([#318](https://www.github.com/crabnebula-dev/cargo-packager/pull/318)) Add `background-app` config setting for macOS to set `LSUIElement` to `true`. ## \[0.11.4] - [`29b60a9`](https://www.github.com/crabnebula-dev/cargo-packager/commit/29b60a97ec14ef87aee7537fa7fbd848f853ac32) ([#305](https://www.github.com/crabnebula-dev/cargo-packager/pull/305)) Fix AppImage bundle when main binary name has spaces. ## \[0.11.3] - [`82e690d`](https://www.github.com/crabnebula-dev/cargo-packager/commit/82e690dfce6109531391e683c8b486d0f39ea335) ([#300](https://www.github.com/crabnebula-dev/cargo-packager/pull/300)) Fix the `Exec` entry on the Linux .desktop file when the binary name contains spaces. ## \[0.11.2] - [`fea80d5`](https://www.github.com/crabnebula-dev/cargo-packager/commit/fea80d5760882e6cdc21c8ed2f82d323e0598926) ([#264](https://www.github.com/crabnebula-dev/cargo-packager/pull/264)) Fix `pacman` package failing to install when source directory contained whitespace. ## \[0.11.1] - [`4523722`](https://www.github.com/crabnebula-dev/cargo-packager/commit/4523722d0808faef4a91dbb227badd0354f4c71a) ([#283](https://www.github.com/crabnebula-dev/cargo-packager/pull/283)) Fixes resources paths on NSIS when cross compiling. ## \[0.11.0] - [`41b05d0`](https://www.github.com/crabnebula-dev/cargo-packager/commit/41b05d08a635d593df4cf4eefbe921b92ace77b7) ([#277](https://www.github.com/crabnebula-dev/cargo-packager/pull/277)) Respect `target-triple` config option when packaging rust binaries. - [`41b05d0`](https://www.github.com/crabnebula-dev/cargo-packager/commit/41b05d08a635d593df4cf4eefbe921b92ace77b7) ([#277](https://www.github.com/crabnebula-dev/cargo-packager/pull/277)) Add `--target` flag to specify target triple to package. ## \[0.10.3] - [`3ee764d`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3ee764d9193ae22331aa5894a1821453e9542992) ([#270](https://www.github.com/crabnebula-dev/cargo-packager/pull/270)) Fixes AppImage bundling failing due to missing `/usr/lib64` directory. - [`ab41e6d`](https://www.github.com/crabnebula-dev/cargo-packager/commit/ab41e6d94af89ec721a0047636597682bd6d90f6) ([#269](https://www.github.com/crabnebula-dev/cargo-packager/pull/269)) Fix using the crate as a library without `cli` feature flag ## \[0.10.2] - [`f836afa`](https://www.github.com/crabnebula-dev/cargo-packager/commit/f836afa699b2da8a55432ce9de1cbccbffb705fb) ([#267](https://www.github.com/crabnebula-dev/cargo-packager/pull/267)) Include notarytool log output on error message in case notarization fails. - [`21441f3`](https://www.github.com/crabnebula-dev/cargo-packager/commit/21441f30c5a258b73926ba7a7d8126d6bf47a662) ([#262](https://www.github.com/crabnebula-dev/cargo-packager/pull/262)) Fixed dmg failed to bundle the application when out-dir does not exist. ### Dependencies - Upgraded to `cargo-packager-utils@0.1.1` ## \[0.10.1] - [`522f23b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/522f23bd867b037eeec81c43295aafd38ebe60ec) ([#258](https://www.github.com/crabnebula-dev/cargo-packager/pull/258)) Update NSIS installer template URL. - [`bce99ae`](https://www.github.com/crabnebula-dev/cargo-packager/commit/bce99aecb4160291a026dcd4750055f9079099f8) ([#260](https://www.github.com/crabnebula-dev/cargo-packager/pull/260)) Fix NSIS uninstaller removing the uninstall directory even if it was not empty. ## \[0.10.0] - [`c6207bb`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c6207bba042a8a0184ddb7e12650a4cd8f415c23) ([#254](https://www.github.com/crabnebula-dev/cargo-packager/pull/254)) Allow Linux dependencies to be specified via a file path instead of just a direct String. This enables the list of dependencies to by dynamically generated for both Debian `.deb` packages and pacman packages, which can relieve the app developer from the burden of manually maintaining a fixed list of dependencies. - [`de4dcca`](https://www.github.com/crabnebula-dev/cargo-packager/commit/de4dccaca4ae758d3adde517cc415a002873e642) ([#256](https://www.github.com/crabnebula-dev/cargo-packager/pull/256)) Automatically add an Exec arg (field code) in the `.desktop` file. This adds an `{exec_arg}` field to the default `main.desktop` template. This field is populated with a sane default value, based on the `deep_link_protocols` or `file_associations` in the `Config` struct. This allows an installed Debian package to be invoked by other applications with URLs or files as arguments, as expected. ## \[0.9.1] - [`44a19ea`](https://www.github.com/crabnebula-dev/cargo-packager/commit/44a19eae1f5f26b1bd10ba84dd6eb3d856609a67) ([#246](https://www.github.com/crabnebula-dev/cargo-packager/pull/246)) On macOS, fix notarization skipping needed environment variables when macos specific config has been specified in the config file. ## \[0.9.0] - [`ab53974`](https://www.github.com/crabnebula-dev/cargo-packager/commit/ab53974b683ce282202e1a550c551eed951e9ca7) ([#235](https://www.github.com/crabnebula-dev/cargo-packager/pull/235)) Added deep link support. ## \[0.8.1] - [`1375380`](https://www.github.com/crabnebula-dev/cargo-packager/commit/1375380c7c9d2adf55ab18a2ce23917849967995)([#196](https://www.github.com/crabnebula-dev/cargo-packager/pull/196)) Always show shell commands output for `beforePackageCommand` and `beforeEachPackagingCommand` . ## \[0.8.0] - [`2164d02`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2164d022f5705e59a189007aec7c99cce98136d8)([#198](https://www.github.com/crabnebula-dev/cargo-packager/pull/198)) Allow packaging the macOS app bundle on Linux and Windows hosts (without codesign support). - [`3057a4a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3057a4a8440bc4dc897f3038ac821ed181644d43)([#197](https://www.github.com/crabnebula-dev/cargo-packager/pull/197)) Added `Config::binaries_dir` and `--binaries-dir` so you can specify the location of the binaries without modifying the output directory. - [`4c4d919`](https://www.github.com/crabnebula-dev/cargo-packager/commit/4c4d9194fb0bd2a814f46336747e643b1e208b52)([#195](https://www.github.com/crabnebula-dev/cargo-packager/pull/195)) Error out if we cannot find a configuration file. - [`b04332c`](https://www.github.com/crabnebula-dev/cargo-packager/commit/b04332c8fc61427dc002a40d9d46bc5f930025c2)([#194](https://www.github.com/crabnebula-dev/cargo-packager/pull/194)) Fixes a crash when packaging `.app` if an empty file is included in the bundle. - [`3057a4a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3057a4a8440bc4dc897f3038ac821ed181644d43)([#197](https://www.github.com/crabnebula-dev/cargo-packager/pull/197)) Added `--out-dir/-o` flags and removed the positional argument to specify where to ouput packages, use the newly added flags instead. - [`2164d02`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2164d022f5705e59a189007aec7c99cce98136d8)([#198](https://www.github.com/crabnebula-dev/cargo-packager/pull/198)) Renamed `PackageOuput` to `PackageOutput` and added `PackageOutput::new`. ## \[0.7.0] - [`cd8898a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/cd8898a93b66a4aae050fa1006089c3c3b5646f9)([#187](https://www.github.com/crabnebula-dev/cargo-packager/pull/187)) Added codesign certificate and notarization credentials configuration options under the `macos` config (for programatic usage, taking precedence over environment variables). ## \[0.6.1] - [`2f1029b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2f1029b2032ac44fd3f3df34307554feb17043b7)([#185](https://www.github.com/crabnebula-dev/cargo-packager/pull/185)) Fix bundling NSIS on Linux and macOS failing due to the verbose flag. ## \[0.6.0] - [`57b379a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/57b379ad1d9029e767848fda99d4eb6415afe51a)([#148](https://www.github.com/crabnebula-dev/cargo-packager/pull/148)) Added config option to control excluded libs when packaging AppImage - [`947e032`](https://www.github.com/crabnebula-dev/cargo-packager/commit/947e0328c89d6f043c3ef1b1db5d2252d4f072a5) Fix CLI failing with `Failed to read cargo metadata: cargo metadata` for non-rust projects. ## \[0.5.0] - [`9bdb953`](https://www.github.com/crabnebula-dev/cargo-packager/commit/9bdb953f1b48c8d69d86e9e42295cd36453c1648)([#137](https://www.github.com/crabnebula-dev/cargo-packager/pull/137)) Add Arch Linux package manager, `pacman` support for cargo packager. - [`a29943e`](https://www.github.com/crabnebula-dev/cargo-packager/commit/a29943e8c95d70e8b77c23021ce52f6ee13314c8)([#140](https://www.github.com/crabnebula-dev/cargo-packager/pull/140)) Fix codesigning failing on macOS under certain circumstances when the order in which files were signed was not deterministic and nesting required signing files nested more deeply first. ### Dependencies - Upgraded to `cargo-packager-utils@0.1.0` ## \[0.4.5] - [`f08e4b8`](https://www.github.com/crabnebula-dev/cargo-packager/commit/f08e4b8972b072617fdb78f11e222427e49ebe8e) Fix the signing and notarization process for MacOS bundles - [`bfa3b00`](https://www.github.com/crabnebula-dev/cargo-packager/commit/bfa3b00cf1087b2ee1e93d9c57b6b577f6294891)([#126](https://www.github.com/crabnebula-dev/cargo-packager/pull/126)) Add `priority` and `section` options in Debian config ## \[0.4.4] - [`3b3ce76`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3b3ce76da0581cf8d553d6edeb0df24f896c62a6)([#128](https://www.github.com/crabnebula-dev/cargo-packager/pull/128)) Fix file download not working on macOS and Windows (arm). ## \[0.4.3] - [`2a50c8e`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2a50c8ea734193036db0ab461f9005ea904cf4b7)([#124](https://www.github.com/crabnebula-dev/cargo-packager/pull/124)) Fix packaing of external binaries. ## \[0.4.2] - [`c18bf3e`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c18bf3e77f91c1c4797992b25902753deee5c986)([#117](https://www.github.com/crabnebula-dev/cargo-packager/pull/117)) Fix the `non-standard-file-perm` and `non-standard-dir-perm` issue in Debian packages ## \[0.4.1] - [`7b083a8`](https://www.github.com/crabnebula-dev/cargo-packager/commit/7b083a8c2ae709659c03a1069d96c3a8391e0674)([#99](https://www.github.com/crabnebula-dev/cargo-packager/pull/99)) Add glob patterns support for the icons option in config. - [`7e05d24`](https://www.github.com/crabnebula-dev/cargo-packager/commit/7e05d24a697230b1f53ee5ee2f7d217047089d97)([#109](https://www.github.com/crabnebula-dev/cargo-packager/pull/109)) Check if required files/tools for packaging are outdated or mis-hashed and redownload them. - [`ea6c31b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/ea6c31b1a3b56bb5408a78f1b2d6b2a2d9ce1161)([#114](https://www.github.com/crabnebula-dev/cargo-packager/pull/114)) Fix NSIS uninstaller leaving resources behind and failing to remove the installation directory. ## \[0.4.0] - [`ecde3fb`](https://www.github.com/crabnebula-dev/cargo-packager/commit/ecde3fb71a8f120e71d4781c11214db750042cc4)([#58](https://www.github.com/crabnebula-dev/cargo-packager/pull/58)) Added `files` configuration under `AppImageConfig` for adding custom files on the AppImage's AppDir. - [`ecde3fb`](https://www.github.com/crabnebula-dev/cargo-packager/commit/ecde3fb71a8f120e71d4781c11214db750042cc4)([#58](https://www.github.com/crabnebula-dev/cargo-packager/pull/58)) Renamed binary `filename` property to `path`, which supports absolute paths. - [`f04c17f`](https://www.github.com/crabnebula-dev/cargo-packager/commit/f04c17f72a4af306f47065aff405c4bd0f7b6442)([#87](https://www.github.com/crabnebula-dev/cargo-packager/pull/87)) Add `config.dmg` to configure the DMG on macOS. - [`21a6c9e`](https://www.github.com/crabnebula-dev/cargo-packager/commit/21a6c9ef4ddbefe9a6e6c5abf287f2ad993edffb)([#84](https://www.github.com/crabnebula-dev/cargo-packager/pull/84)) Mark most of the types as `non_exhaustive` to allow adding more field later on without having to break downstream users use the newly added helper methods on these types to modify the corresponding fields in-place. - [`db75777`](https://www.github.com/crabnebula-dev/cargo-packager/commit/db75777d2799ca37217d568befad39b9377cfa2a) Add `config.windows.sign_command` which can be used to override signing command on windows and allows usage of tools other than `signtool.exe`. ## \[0.3.0] - [`65b8c20`](https://www.github.com/crabnebula-dev/cargo-packager/commit/65b8c20a96877038daa4907b80cd96f96e0bfe33)([#54](https://www.github.com/crabnebula-dev/cargo-packager/pull/54)) Code sign binaries and frameworks on macOS. - [`7ef6b7c`](https://www.github.com/crabnebula-dev/cargo-packager/commit/7ef6b7c0186e79243240cb2d1a1846fda41a1b54)([#50](https://www.github.com/crabnebula-dev/cargo-packager/pull/50)) Set `root` as the owner of control files and package files in `deb` package. - [`8cc5b05`](https://www.github.com/crabnebula-dev/cargo-packager/commit/8cc5b05eb3eb124b385d406329eee379349faa86)([#53](https://www.github.com/crabnebula-dev/cargo-packager/pull/53)) Fixed an error message that the source path does not exist when packaging .app - [`274a6be`](https://www.github.com/crabnebula-dev/cargo-packager/commit/274a6bec553f273934347a18e0d6e2e1ec61bbeb)([#49](https://www.github.com/crabnebula-dev/cargo-packager/pull/49)) Read `HTTP_PROXY` env var when downloading resources. - [`6ed1312`](https://www.github.com/crabnebula-dev/cargo-packager/commit/6ed1312926d70cf449e7beddacb56a17e51a25ac)([#52](https://www.github.com/crabnebula-dev/cargo-packager/pull/52)) Read the `APPLE_TEAM_ID` environment variable for macOS notarization arguments. - [`65b8c20`](https://www.github.com/crabnebula-dev/cargo-packager/commit/65b8c20a96877038daa4907b80cd96f96e0bfe33)([#54](https://www.github.com/crabnebula-dev/cargo-packager/pull/54)) Remove extended attributes on the macOS app bundle using `xattr -cr $PATH`. ## \[0.2.0] - [`dde1ab3`](https://www.github.com/crabnebula-dev/cargo-packager/commit/dde1ab34b59ee614fc24e47a5caa8ebc04d92a08)([#43](https://www.github.com/crabnebula-dev/cargo-packager/pull/43)) Remove the deprecated `cargo-packager-config` dependency. ## \[0.1.2] - [`1809f10`](https://www.github.com/crabnebula-dev/cargo-packager/commit/1809f10b4fd1720fd740196f67c3c980ade0a6bd) Respect the `config.enabled` option. ### Dependencies - Upgraded to `cargo-packager-config@0.2.0` ## \[0.1.1] - [`2d8b8d7`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2d8b8d7c1af73202639449a00dbc51bf171effc7) Initial Release ================================================ FILE: crates/packager/Cargo.toml ================================================ [package] name = "cargo-packager" version = "0.11.8" description = "Executable packager and bundler distributed as a CLI and library." authors = [ "CrabNebula Ltd.", "Tauri Programme within The Commons Conservancy", "George Burton ", ] keywords = ["bundle", "package", "cargo"] categories = [ "command-line-interface", "command-line-utilities", "development-tools::cargo-plugins", "development-tools::build-utils", "os", ] edition = { workspace = true } license = { workspace = true } repository = { workspace = true } [[bin]] name = "cargo-packager" # path = "src/bins/cli.rs" required-features = ["cli"] [package.metadata.docs.rs] rustdoc-args = ["--cfg", "doc_cfg"] default-target = "x86_64-unknown-linux-gnu" targets = [ "x86_64-pc-windows-msvc", "x86_64-unknown-linux-gnu", "x86_64-apple-darwin", ] [lints.rust] # cfg(doc_cfg) is used for docs.rs detection. see above unexpected_cfgs = { level = "warn", check-cfg = ['cfg(doc_cfg)'] } [features] default = ["cli", "rustls-tls"] cli = ["clap", "dep:tracing-subscriber"] schema = ["schemars", "cargo-packager-utils/schema"] clap = ["dep:clap", "cargo-packager-utils/clap"] native-tls = ["ureq/native-tls"] native-tls-vendored = ["native-tls", "native-tls/vendored"] rustls-tls = ["ureq/tls"] [dependencies] thiserror = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } dunce = { workspace = true } dirs = { workspace = true } semver = { workspace = true } base64 = { workspace = true } clap = { workspace = true, optional = true, features = ["env"] } tracing = { workspace = true } tracing-subscriber = { version = "0.3", optional = true, features = [ "env-filter", ] } toml = "0.9" cargo_metadata = "0.18" ureq = { version = "2.10", default-features = false } hex = "0.4" sha1 = "0.10" sha2 = "0.10" zip = { version = "0.6", default-features = false, features = ["deflate"] } handlebars = "6.0" glob = "0.3" relative-path = "2" walkdir = "2" os_pipe = "1" minisign = "0.7" tar = { workspace = true } flate2 = "1.0" strsim = "0.11" schemars = { workspace = true, optional = true } native-tls = { version = "0.2", optional = true } cargo-packager-utils = { version = "0.1.1", path = "../utils", features = [ "serde", ] } icns = { package = "tauri-icns", version = "0.1" } time = { workspace = true, features = ["formatting"] } image = { version = "0.25", default-features = false, features = ["rayon", "bmp", "ico", "png", "jpeg"] } tempfile = "3" plist = "1" [target."cfg(target_os = \"windows\")".dependencies] windows-registry = "0.6" once_cell = "1" uuid = { version = "1", features = ["v4", "v5"] } regex = "1" [target."cfg(target_os = \"windows\")".dependencies.windows-sys] version = "0.61" features = ["Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug"] [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies] md5 = "0.8" heck = "0.5" ar = "0.9" ================================================ FILE: crates/packager/LICENSE_APACHE-2.0 ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: crates/packager/LICENSE_MIT ================================================ MIT License Copyright (c) 2023 - Present CrabNebula Ltd. Copyright (c) 2019 - 2023 Tauri Programme within The Commons Conservancy Copyright (c) 2017 - 2019 Cargo-Bundle developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: crates/packager/README.md ================================================ # cargo-packager cargo-packager splash Executable packager, bundler and updater. A cli tool and library to generate installers or app bundles for your executables. It also comes with useful addons: - an [updater](https://docs.rs/cargo-packager-updater) - a [resource resolver](https://docs.rs/cargo-packager-resource-resolver) #### Supported packages: - macOS - DMG (.dmg) - Bundle (.app) - Linux - Debian package (.deb) - AppImage (.AppImage) - Pacman (.tar.gz and PKGBUILD) - Windows - NSIS (.exe) - MSI using WiX Toolset (.msi) ### CLI The packager is distributed on crates.io as a cargo subcommand, you can install it using cargo: ```sh cargo install cargo-packager --locked ``` You then need to configure your app so the cli can recognize it. Configuration can be done in `Packager.toml` or `packager.json` in your project or modify Cargo.toml and include this snippet: ```toml [package.metadata.packager] before-packaging-command = "cargo build --release" ``` Once, you are done configuring your app, run: ```sh cargo packager --release ``` ### Configuration By default, the packager reads its configuration from `Packager.toml` or `packager.json` if it exists, and from `package.metadata.packager` table in `Cargo.toml`. You can also specify a custom configuration using the `-c/--config` cli argument. For a full list of configuration options, see https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html. You could also use the [schema](./schema.json) file from GitHub to validate your configuration or have auto completions in your IDE. ### Building your application before packaging By default, the packager doesn't build your application, so if your app requires a compilation step, the packager has an option to specify a shell command to be executed before packaing your app, `beforePackagingCommand`. ### Cargo profiles By default, the packager looks for binaries built using the `debug` profile, if your `beforePackagingCommand` builds your app using `cargo build --release`, you will also need to run the packager in release mode `cargo packager --release`, otherwise, if you have a custom cargo profile, you will need to specify it using `--profile` cli arg `cargo packager --profile custom-release-profile`. ### Library This crate is also published to crates.io as a library that you can integrate into your tooling, just make sure to disable the default-feature flags. ```sh cargo add cargo-packager --no-default-features ``` #### Feature flags - **`cli`**: Enables the cli specifc features and dependencies. Enabled by default. - **`tracing`**: Enables `tracing` crate integration. ## Licenses MIT or MIT/Apache 2.0 where applicable. ================================================ FILE: crates/packager/schema.json ================================================ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "description": "The packaging config.", "type": "object", "properties": { "$schema": { "description": "The JSON schema for the config.\n\nSetting this field has no effect, this just exists so we can parse the JSON correctly when it has `$schema` field set.", "type": [ "string", "null" ] }, "name": { "description": "The app name, this is just an identifier that could be used to filter which app to package using `--packages` cli arg when there is multiple apps in the workspace or in the same config.\n\nThis field resembles, the `name` field in `Cargo.toml` or `package.json`\n\nIf `unset`, the CLI will try to auto-detect it from `Cargo.toml` or `package.json` otherwise, it will keep it unset.", "type": [ "string", "null" ] }, "enabled": { "description": "Whether this config is enabled or not. Defaults to `true`.", "default": true, "type": "boolean" }, "productName": { "description": "The package's product name, for example \"My Awesome App\".", "default": "", "type": "string" }, "version": { "description": "The package's version.", "default": "", "type": "string" }, "binaries": { "description": "The binaries to package.", "default": [], "type": "array", "items": { "$ref": "#/definitions/Binary" } }, "identifier": { "description": "The application identifier in reverse domain name notation (e.g. `com.packager.example`). This string must be unique across applications since it is used in some system configurations. This string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).", "type": [ "string", "null" ], "pattern": "^[a-zA-Z0-9-\\.]*$" }, "beforePackagingCommand": { "description": "The command to run before starting to package an application.\n\nThis runs only once.", "anyOf": [ { "$ref": "#/definitions/HookCommand" }, { "type": "null" } ] }, "beforeEachPackageCommand": { "description": "The command to run before packaging each format for an application.\n\nThis will run multiple times depending on the formats specifed.", "anyOf": [ { "$ref": "#/definitions/HookCommand" }, { "type": "null" } ] }, "logLevel": { "description": "The logging level.", "anyOf": [ { "$ref": "#/definitions/LogLevel" }, { "type": "null" } ] }, "formats": { "description": "The packaging formats to create, if not present, [`PackageFormat::platform_default`] is used.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/PackageFormat" } }, "outDir": { "description": "The directory where the generated packages will be placed.\n\nIf [`Config::binaries_dir`] is not set, this is also where the [`Config::binaries`] exist.", "default": "", "type": "string" }, "binariesDir": { "description": "The directory where the [`Config::binaries`] exist.\n\nDefaults to [`Config::out_dir`].", "default": null, "type": [ "string", "null" ] }, "targetTriple": { "description": "The target triple we are packaging for.\n\nDefaults to the current OS target triple.", "type": [ "string", "null" ] }, "description": { "description": "The package's description.", "type": [ "string", "null" ] }, "longDescription": { "description": "The app's long description.", "type": [ "string", "null" ] }, "homepage": { "description": "The package's homepage.", "type": [ "string", "null" ] }, "authors": { "description": "The package's authors.", "default": null, "type": [ "array", "null" ], "items": { "type": "string" } }, "publisher": { "description": "The app's publisher. Defaults to the second element in [`Config::identifier`](Config::identifier) string. Currently maps to the Manufacturer property of the Windows Installer.", "type": [ "string", "null" ] }, "licenseFile": { "description": "A path to the license file.", "type": [ "string", "null" ] }, "copyright": { "description": "The app's copyright.", "type": [ "string", "null" ] }, "category": { "description": "The app's category.", "anyOf": [ { "$ref": "#/definitions/AppCategory" }, { "type": "null" } ] }, "icons": { "description": "The app's icon list. Supports glob patterns.", "type": [ "array", "null" ], "items": { "type": "string" } }, "fileAssociations": { "description": "The file associations", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/FileAssociation" } }, "deepLinkProtocols": { "description": "Deep-link protocols.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/DeepLinkProtocol" } }, "resources": { "description": "The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory or an object of `src` and `target` paths. In the case of using an object, the `src` could be either a glob pattern, path to a file, path to a directory, and the `target` is a path inside the final resources folder in the installed package.\n\n## Format-specific:\n\n- **[PackageFormat::Nsis] / [PackageFormat::Wix]**: The resources are placed next to the executable in the root of the packager. - **[PackageFormat::Deb]**: The resources are placed in `usr/lib` of the package.", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/Resource" } }, "externalBinaries": { "description": "Paths to external binaries to add to the package.\n\nThe path specified should not include `-<.exe>` suffix, it will be auto-added when by the packager when reading these paths, so the actual binary name should have the target platform's target triple appended, as well as `.exe` for Windows.\n\nFor example, if you're packaging an external binary called `sqlite3`, the packager expects a binary named `sqlite3-x86_64-unknown-linux-gnu` on linux, and `sqlite3-x86_64-pc-windows-gnu.exe` on windows.\n\nIf you are building a universal binary for MacOS, the packager expects your external binary to also be universal, and named after the target triple, e.g. `sqlite3-universal-apple-darwin`. See ", "type": [ "array", "null" ], "items": { "type": "string" } }, "windows": { "description": "Windows-specific configuration.", "anyOf": [ { "$ref": "#/definitions/WindowsConfig" }, { "type": "null" } ] }, "macos": { "description": "MacOS-specific configuration.", "anyOf": [ { "$ref": "#/definitions/MacOsConfig" }, { "type": "null" } ] }, "linux": { "description": "Linux-specific configuration", "anyOf": [ { "$ref": "#/definitions/LinuxConfig" }, { "type": "null" } ] }, "deb": { "description": "Debian-specific configuration.", "anyOf": [ { "$ref": "#/definitions/DebianConfig" }, { "type": "null" } ] }, "appimage": { "description": "AppImage configuration.", "anyOf": [ { "$ref": "#/definitions/AppImageConfig" }, { "type": "null" } ] }, "pacman": { "description": "Pacman configuration.", "anyOf": [ { "$ref": "#/definitions/PacmanConfig" }, { "type": "null" } ] }, "wix": { "description": "WiX configuration.", "anyOf": [ { "$ref": "#/definitions/WixConfig" }, { "type": "null" } ] }, "nsis": { "description": "Nsis configuration.", "anyOf": [ { "$ref": "#/definitions/NsisConfig" }, { "type": "null" } ] }, "dmg": { "description": "Dmg configuration.", "anyOf": [ { "$ref": "#/definitions/DmgConfig" }, { "type": "null" } ] } }, "additionalProperties": false, "definitions": { "Binary": { "description": "A binary to package within the final package.", "type": "object", "required": [ "path" ], "properties": { "path": { "description": "Path to the binary (without `.exe` on Windows). If it's relative, it will be resolved from [`Config::out_dir`].", "type": "string" }, "main": { "description": "Whether this is the main binary or not", "default": false, "type": "boolean" } }, "additionalProperties": false }, "HookCommand": { "description": "Describes a shell command to be executed when a CLI hook is triggered.", "anyOf": [ { "description": "Run the given script with the default options.", "type": "string" }, { "description": "Run the given script with custom options.", "type": "object", "required": [ "script" ], "properties": { "script": { "description": "The script to execute.", "type": "string" }, "dir": { "description": "The working directory.", "type": [ "string", "null" ] } } } ] }, "LogLevel": { "description": "An enum representing the available verbosity levels of the logger.", "oneOf": [ { "description": "The \"error\" level.\n\nDesignates very serious errors.", "type": "string", "enum": [ "error" ] }, { "description": "The \"warn\" level.\n\nDesignates hazardous situations.", "type": "string", "enum": [ "warn" ] }, { "description": "The \"info\" level.\n\nDesignates useful information.", "type": "string", "enum": [ "info" ] }, { "description": "The \"debug\" level.\n\nDesignates lower priority information.", "type": "string", "enum": [ "debug" ] }, { "description": "The \"trace\" level.\n\nDesignates very low priority, often extremely verbose, information.", "type": "string", "enum": [ "trace" ] } ] }, "PackageFormat": { "description": "Types of supported packages by [`cargo-packager`](https://docs.rs/cargo-packager).", "oneOf": [ { "description": "All available package formats for the current platform.\n\nSee [`PackageFormat::platform_all`]", "type": "string", "enum": [ "all" ] }, { "description": "The default list of package formats for the current platform.\n\nSee [`PackageFormat::platform_default`]", "type": "string", "enum": [ "default" ] }, { "description": "The macOS application bundle (.app).", "type": "string", "enum": [ "app" ] }, { "description": "The macOS DMG package (.dmg).", "type": "string", "enum": [ "dmg" ] }, { "description": "The Microsoft Software Installer (.msi) through WiX Toolset.", "type": "string", "enum": [ "wix" ] }, { "description": "The NSIS installer (.exe).", "type": "string", "enum": [ "nsis" ] }, { "description": "The Linux Debian package (.deb).", "type": "string", "enum": [ "deb" ] }, { "description": "The Linux AppImage package (.AppImage).", "type": "string", "enum": [ "appimage" ] }, { "description": "The Linux Pacman package (.tar.gz and PKGBUILD)", "type": "string", "enum": [ "pacman" ] } ] }, "AppCategory": { "description": "The possible app categories. Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.", "type": "string", "enum": [ "Business", "DeveloperTool", "Education", "Entertainment", "Finance", "Game", "ActionGame", "AdventureGame", "ArcadeGame", "BoardGame", "CardGame", "CasinoGame", "DiceGame", "EducationalGame", "FamilyGame", "KidsGame", "MusicGame", "PuzzleGame", "RacingGame", "RolePlayingGame", "SimulationGame", "SportsGame", "StrategyGame", "TriviaGame", "WordGame", "GraphicsAndDesign", "HealthcareAndFitness", "Lifestyle", "Medical", "Music", "News", "Photography", "Productivity", "Reference", "SocialNetworking", "Sports", "Travel", "Utility", "Video", "Weather" ] }, "FileAssociation": { "description": "A file association configuration.", "type": "object", "required": [ "extensions" ], "properties": { "extensions": { "description": "File extensions to associate with this app. e.g. 'png'", "type": "array", "items": { "type": "string" } }, "mimeType": { "description": "The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**.", "type": [ "string", "null" ] }, "description": { "description": "The association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer.", "type": [ "string", "null" ] }, "name": { "description": "The name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext`", "type": [ "string", "null" ] }, "role": { "description": "The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. Defaults to [`BundleTypeRole::Editor`]", "default": "editor", "allOf": [ { "$ref": "#/definitions/BundleTypeRole" } ] } }, "additionalProperties": false }, "BundleTypeRole": { "description": "*macOS-only**. Corresponds to CFBundleTypeRole", "oneOf": [ { "description": "CFBundleTypeRole.Editor. Files can be read and edited.", "type": "string", "enum": [ "editor" ] }, { "description": "CFBundleTypeRole.Viewer. Files can be read.", "type": "string", "enum": [ "viewer" ] }, { "description": "CFBundleTypeRole.Shell", "type": "string", "enum": [ "shell" ] }, { "description": "CFBundleTypeRole.QLGenerator", "type": "string", "enum": [ "qLGenerator" ] }, { "description": "CFBundleTypeRole.None", "type": "string", "enum": [ "none" ] } ] }, "DeepLinkProtocol": { "description": "Deep link protocol", "type": "object", "required": [ "schemes" ], "properties": { "schemes": { "description": "URL schemes to associate with this app without `://`. For example `my-app`", "type": "array", "items": { "type": "string" } }, "name": { "description": "The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `.`", "type": [ "string", "null" ] }, "role": { "description": "The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`.", "default": "editor", "allOf": [ { "$ref": "#/definitions/BundleTypeRole" } ] } }, "additionalProperties": false }, "Resource": { "description": "A path to a resource (with optional glob pattern) or an object of `src` and `target` paths.", "anyOf": [ { "description": "Supports glob patterns", "type": "string" }, { "description": "An object descriping the src file or directory and its target location in the final package.", "type": "object", "required": [ "src", "target" ], "properties": { "src": { "description": "The src file or directory, supports glob patterns.", "type": "string" }, "target": { "description": "A relative path from the root of the final package.\n\nIf `src` is a glob, this will always be treated as a directory where all globbed files will be placed under.", "type": "string" } } } ] }, "WindowsConfig": { "description": "The Windows configuration.", "type": "object", "properties": { "digestAlgorithm": { "description": "The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended.", "type": [ "string", "null" ] }, "certificateThumbprint": { "description": "The SHA1 hash of the signing certificate.", "type": [ "string", "null" ] }, "tsp": { "description": "Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true.", "default": false, "type": "boolean" }, "timestampUrl": { "description": "Server to use during timestamping.", "type": [ "string", "null" ] }, "allowDowngrades": { "description": "Whether to validate a second app installation, blocking the user from installing an older version if set to `false`.\n\nFor instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`.\n\nThe default value of this flag is `true`.", "default": true, "type": "boolean" }, "signCommand": { "description": "Specify a custom command to sign the binaries. This command needs to have a `%1` in it which is just a placeholder for the binary path, which we will detect and replace before calling the command.\n\nBy Default we use `signtool.exe` which can be found only on Windows so if you are on another platform and want to cross-compile and sign you will need to use another tool like `osslsigncode`.", "type": [ "string", "null" ] } }, "additionalProperties": false }, "MacOsConfig": { "description": "The macOS configuration.", "type": "object", "properties": { "frameworks": { "description": "MacOS frameworks that need to be packaged with the app.\n\nEach string can either be the name of a framework (without the `.framework` extension, e.g. `\"SDL2\"`), in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle (under `Foobar.app/Contents/Frameworks/`); you are still responsible for:\n\n- arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script)\n\n- embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath \"@executable_path/../Frameworks\" path/to/binary` after compiling)", "type": [ "array", "null" ], "items": { "type": "string" } }, "minimumSystemVersion": { "description": "A version string indicating the minimum MacOS version that the packaged app supports (e.g. `\"10.11\"`). If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`.", "type": [ "string", "null" ] }, "exceptionDomain": { "description": "The exception domain to use on the macOS .app package.\n\nThis allows communication to the outside world e.g. a web server you're shipping.", "type": [ "string", "null" ] }, "signingIdentity": { "description": "Code signing identity.\n\nThis is typically of the form: `\"Developer ID Application: TEAM_NAME (TEAM_ID)\"`.", "type": [ "string", "null" ] }, "providerShortName": { "description": "Provider short name for notarization.", "type": [ "string", "null" ] }, "entitlements": { "description": "Path to the entitlements.plist file.", "type": [ "string", "null" ] }, "infoPlistPath": { "description": "Path to the Info.plist file for the package.", "type": [ "string", "null" ] }, "embeddedProvisionprofilePath": { "description": "Path to the embedded.provisionprofile file for the package.", "type": [ "string", "null" ] }, "embeddedApps": { "description": "Apps that need to be packaged within the app.", "type": [ "array", "null" ], "items": { "type": "string" } }, "backgroundApp": { "description": "Whether this is a background application. If true, the app will not appear in the Dock.\n\nSets the `LSUIElement` flag in the macOS plist file.", "default": false, "type": "boolean" } }, "additionalProperties": false }, "LinuxConfig": { "description": "Linux configuration", "type": "object", "properties": { "generateDesktopEntry": { "description": "Flag to indicate if desktop entry should be generated.", "default": true, "type": "boolean" } }, "additionalProperties": false }, "DebianConfig": { "description": "The Linux Debian configuration.", "type": "object", "properties": { "depends": { "description": "The list of Debian dependencies.", "anyOf": [ { "$ref": "#/definitions/Dependencies" }, { "type": "null" } ] }, "desktopTemplate": { "description": "Path to a custom desktop file Handlebars template.\n\nAvailable variables: `categories`, `comment` (optional), `exec`, `icon` and `name`.\n\nDefault file contents: ```text [Desktop Entry] Categories={{categories}} {{#if comment}} Comment={{comment}} {{/if}} Exec={{exec}} {{exec_arg}} Icon={{icon}} Name={{name}} Terminal=false Type=Application {{#if mime_type}} MimeType={{mime_type}} {{/if}} ```\n\nThe `{{exec_arg}}` will be set to: * \"%F\", if at least one [Config::file_associations] was specified but no deep link protocols were given. * The \"%F\" arg means that your application can be invoked with multiple file paths. * \"%U\", if at least one [Config::deep_link_protocols] was specified. * The \"%U\" arg means that your application can be invoked with multiple URLs. * If both [Config::file_associations] and [Config::deep_link_protocols] were specified, the \"%U\" arg will be used, causing the file paths to be passed to your app as `file://` URLs. * An empty string \"\" (nothing) if neither are given. * This means that your application will never be invoked with any URLs or file paths.\n\nTo specify a custom `exec_arg`, just use plaintext directly instead of `{{exec_arg}}`: ```text Exec={{exec}} %u ```\n\nSee more here: .", "type": [ "string", "null" ] }, "section": { "description": "Define the section in Debian Control file. See : ", "type": [ "string", "null" ] }, "priority": { "description": "Change the priority of the Debian Package. By default, it is set to `optional`. Recognized Priorities as of now are : `required`, `important`, `standard`, `optional`, `extra`", "type": [ "string", "null" ] }, "files": { "description": "List of custom files to add to the deb package. Maps a dir/file to a dir/file inside the debian package.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "packageName": { "description": "Name to use for the `Package` field in the Debian Control file. Defaults to [`Config::product_name`] converted to kebab-case.", "type": [ "string", "null" ] } }, "additionalProperties": false }, "Dependencies": { "description": "A list of dependencies specified as either a list of Strings or as a path to a file that lists the dependencies, one per line.", "anyOf": [ { "description": "The list of dependencies provided directly as a vector of Strings.", "type": "array", "items": { "type": "string" } }, { "description": "A path to the file containing the list of dependences, formatted as one per line: ```text libc6 libxcursor1 libdbus-1-3 libasyncns0 ... ```", "type": "string" } ] }, "AppImageConfig": { "description": "The Linux AppImage configuration.", "type": "object", "properties": { "libs": { "description": "List of libs that exist in `/usr/lib*` to be include in the final AppImage. The libs will be searched for, using the command `find -L /usr/lib* -name `", "type": [ "array", "null" ], "items": { "type": "string" } }, "bins": { "description": "List of binary paths to include in the final AppImage. For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open`", "type": [ "array", "null" ], "items": { "type": "string" } }, "files": { "description": "List of custom files to add to the appimage package. Maps a dir/file to a dir/file inside the appimage package.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "linuxdeployPlugins": { "description": "A map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) plugin name and its URL to be downloaded and executed while packaing the appimage. For example, if you want to use the [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, you'd specify `gtk` as the key and its url as the value.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "excludedLibs": { "description": "List of globs of libraries to exclude from the final AppImage. For example, to exclude libnss3.so, you'd specify `libnss3*`", "type": [ "array", "null" ], "items": { "type": "string" } } }, "additionalProperties": false }, "PacmanConfig": { "description": "The Linux pacman configuration.", "type": "object", "properties": { "files": { "description": "List of custom files to add to the pacman package. Maps a dir/file to a dir/file inside the pacman package.", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "depends": { "description": "List of softwares that must be installed for the app to build and run.\n\nSee : ", "anyOf": [ { "$ref": "#/definitions/Dependencies" }, { "type": "null" } ] }, "provides": { "description": "Additional packages that are provided by this app.\n\nSee : ", "type": [ "array", "null" ], "items": { "type": "string" } }, "conflicts": { "description": "Packages that conflict or cause problems with the app. All these packages and packages providing this item will need to be removed\n\nSee : ", "type": [ "array", "null" ], "items": { "type": "string" } }, "replaces": { "description": "Only use if this app replaces some obsolete packages. For example, if you rename any package.\n\nSee : ", "type": [ "array", "null" ], "items": { "type": "string" } }, "source": { "description": "Source of the package to be stored at PKGBUILD. PKGBUILD is a bash script, so version can be referred as ${pkgver}", "type": [ "array", "null" ], "items": { "type": "string" } } }, "additionalProperties": false }, "WixConfig": { "description": "The wix format configuration", "type": "object", "properties": { "languages": { "description": "The app languages to build. See .", "type": [ "array", "null" ], "items": { "$ref": "#/definitions/WixLanguage" } }, "template": { "description": "By default, the packager uses an internal template. This option allows you to define your own wix file.", "type": [ "string", "null" ] }, "mergeModules": { "description": "List of merge modules to include in your installer. For example, if you want to include [C++ Redis merge modules]\n\n[C++ Redis merge modules]: https://wixtoolset.org/docs/v3/howtos/redistributables_and_install_checks/install_vcredist/", "type": [ "array", "null" ], "items": { "type": "string" } }, "fragmentPaths": { "description": "A list of paths to .wxs files with WiX fragments to use.", "type": [ "array", "null" ], "items": { "type": "string" } }, "fragments": { "description": "List of WiX fragments as strings. This is similar to `config.wix.fragments_paths` but is a string so you can define it inline in your config.\n\n```text ```", "type": [ "array", "null" ], "items": { "type": "string" } }, "componentGroupRefs": { "description": "The ComponentGroup element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "componentRefs": { "description": "The Component element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "customActionRefs": { "description": "The CustomAction element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "featureGroupRefs": { "description": "The FeatureGroup element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "featureRefs": { "description": "The Feature element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "mergeRefs": { "description": "The Merge element ids you want to reference from the fragments.", "type": [ "array", "null" ], "items": { "type": "string" } }, "bannerPath": { "description": "Path to a bitmap file to use as the installation user interface banner. This bitmap will appear at the top of all but the first page of the installer.\n\nThe required dimensions are 493px × 58px.", "type": [ "string", "null" ] }, "dialogImagePath": { "description": "Path to a bitmap file to use on the installation user interface dialogs. It is used on the welcome and completion dialogs. The required dimensions are 493px × 312px.", "type": [ "string", "null" ] }, "fipsCompliant": { "description": "Enables FIPS compliant algorithms.", "default": false, "type": "boolean" } }, "additionalProperties": false }, "WixLanguage": { "description": "A wix language.", "anyOf": [ { "description": "Built-in wix language identifier.", "type": "string" }, { "description": "Custom wix language.", "type": "object", "required": [ "identifier" ], "properties": { "identifier": { "description": "Idenitifier of this language, for example `en-US`", "type": "string" }, "path": { "description": "The path to a locale (`.wxl`) file. See .", "type": [ "string", "null" ] } } } ] }, "NsisConfig": { "description": "The NSIS format configuration.", "type": "object", "properties": { "compression": { "description": "Set the compression algorithm used to compress files in the installer.\n\nSee ", "anyOf": [ { "$ref": "#/definitions/NsisCompression" }, { "type": "null" } ] }, "template": { "description": "A custom `.nsi` template to use.\n\nSee the default template here ", "type": [ "string", "null" ] }, "preinstallSection": { "description": "Logic of an NSIS section that will be ran before the install section.\n\nSee the available libraries, dlls and global variables here \n\n### Example ```toml [package.metadata.packager.nsis] preinstall-section = \"\"\" ; Setup custom messages LangString webview2AbortError ${LANG_ENGLISH} \"Failed to install WebView2! The app can't run without it. Try restarting the installer.\" LangString webview2DownloadError ${LANG_ARABIC} \"خطأ: فشل تنزيل WebView2 - $0\"\n\nSection PreInstall ;
SectionEnd\n\nSection AnotherPreInstall ;
SectionEnd \"\"\" ```", "type": [ "string", "null" ] }, "headerImage": { "description": "The path to a bitmap file to display on the header of installers pages.\n\nThe recommended dimensions are 150px x 57px.", "type": [ "string", "null" ] }, "sidebarImage": { "description": "The path to a bitmap file for the Welcome page and the Finish page.\n\nThe recommended dimensions are 164px x 314px.", "type": [ "string", "null" ] }, "installerIcon": { "description": "The path to an icon file used as the installer icon.", "type": [ "string", "null" ] }, "installMode": { "description": "Whether the installation will be for all users or just the current user.", "default": "currentUser", "allOf": [ { "$ref": "#/definitions/NSISInstallerMode" } ] }, "languages": { "description": "A list of installer languages. By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. To allow the user to select the language, set `display_language_selector` to `true`.\n\nSee for the complete list of languages.", "type": [ "array", "null" ], "items": { "type": "string" } }, "customLanguageFiles": { "description": "An key-value pair where the key is the language and the value is the path to a custom `.nsi` file that holds the translated text for cargo-packager's custom messages.\n\nSee for an example `.nsi` file.\n\n**Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array,", "type": [ "object", "null" ], "additionalProperties": { "type": "string" } }, "displayLanguageSelector": { "description": "Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. By default the OS language is selected, with a fallback to the first language in the `languages` array.", "default": false, "type": "boolean" }, "appdataPaths": { "description": "List of paths where your app stores data. This options tells the uninstaller to provide the user with an option (disabled by default) whether they want to rmeove your app data or keep it.\n\nThe path should use a constant from in addition to `$IDENTIFIER`, `$PUBLISHER` and `$PRODUCTNAME`, for example, if you store your app data in `C:\\\\Users\\\\\\\\AppData\\\\Local\\\\\\\\` you'd need to specify ```toml [package.metadata.packager.nsis] appdata-paths = [\"$LOCALAPPDATA/$PUBLISHER/$PRODUCTNAME\"] ```", "default": null, "type": [ "array", "null" ], "items": { "type": "string" } } }, "additionalProperties": false }, "NsisCompression": { "description": "Compression algorithms used in the NSIS installer.\n\nSee ", "oneOf": [ { "description": "ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory.", "type": "string", "enum": [ "zlib" ] }, { "description": "BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory.", "type": "string", "enum": [ "bzip2" ] }, { "description": "LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB.", "type": "string", "enum": [ "lzma" ] }, { "description": "Disable compression.", "type": "string", "enum": [ "off" ] } ] }, "NSISInstallerMode": { "description": "Install Modes for the NSIS installer.", "oneOf": [ { "description": "Default mode for the installer.\n\nInstall the app by default in a directory that doesn't require Administrator access.\n\nInstaller metadata will be saved under the `HKCU` registry path.", "type": "string", "enum": [ "currentUser" ] }, { "description": "Install the app by default in the `Program Files` folder directory requires Administrator access for the installation.\n\nInstaller metadata will be saved under the `HKLM` registry path.", "type": "string", "enum": [ "perMachine" ] }, { "description": "Combines both modes and allows the user to choose at install time whether to install for the current user or per machine. Note that this mode will require Administrator access even if the user wants to install it for the current user only.\n\nInstaller metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice.", "type": "string", "enum": [ "both" ] } ] }, "DmgConfig": { "description": "The Apple Disk Image (.dmg) configuration.", "type": "object", "properties": { "background": { "description": "Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`.", "type": [ "string", "null" ] }, "windowPosition": { "description": "Position of volume window on screen.", "anyOf": [ { "$ref": "#/definitions/Position" }, { "type": "null" } ] }, "windowSize": { "description": "Size of volume window.", "anyOf": [ { "$ref": "#/definitions/Size" }, { "type": "null" } ] }, "appPosition": { "description": "Position of application file on window.", "anyOf": [ { "$ref": "#/definitions/Position" }, { "type": "null" } ] }, "appFolderPosition": { "description": "Position of application folder on window.", "anyOf": [ { "$ref": "#/definitions/Position" }, { "type": "null" } ] } }, "additionalProperties": false }, "Position": { "description": "Position coordinates struct.", "type": "object", "required": [ "x", "y" ], "properties": { "x": { "description": "X coordinate.", "type": "integer", "format": "uint32", "minimum": 0.0 }, "y": { "description": "Y coordinate.", "type": "integer", "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false }, "Size": { "description": "Size struct.", "type": "object", "required": [ "height", "width" ], "properties": { "width": { "description": "Width.", "type": "integer", "format": "uint32", "minimum": 0.0 }, "height": { "description": "Height.", "type": "integer", "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false } } } ================================================ FILE: crates/packager/src/bin/cargo-packager.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{env::args_os, ffi::OsStr, path::Path, process::exit}; fn main() { let mut args = args_os().peekable(); let bin_name = match args .next() .as_deref() .map(Path::new) .and_then(Path::file_stem) .and_then(OsStr::to_str) { Some("cargo-packager") => { if args.peek().and_then(|s| s.to_str()) == Some("packager") { // remove the extra cargo subcommand args.next(); Some("cargo packager".into()) } else { Some("cargo-packager".into()) } } Some(stem) => Some(stem.to_string()), None => { eprintln!("cargo-packager wrapper unable to read first argument"); exit(1); } }; cargo_packager::cli::run(args, bin_name) } ================================================ FILE: crates/packager/src/cli/config.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ fmt::Debug, fs, path::{Path, PathBuf}, }; use super::{Error, Result}; use crate::{config::Binary, Config}; impl Config { pub(crate) fn name(&self) -> &str { self.name.as_deref().unwrap_or_default() } /// Whether this config should be packaged or skipped pub(crate) fn should_pacakge(&self, cli: &super::Cli) -> bool { // Should be packaged when it is enabled and this package was in the explicit packages list specified on the CLI, // or the packages list specified on the CLI is empty which means build all self.enabled && cli .packages .as_ref() .map(|packages| packages.iter().any(|p| p == self.name())) .unwrap_or(true) } } fn find_nearset_pkg_name(path: &Path) -> Result> { fn find_nearset_pkg_name_inner() -> Result> { if let Ok(contents) = fs::read_to_string("Cargo.toml") { let toml = toml::from_str::(&contents) .map_err(|e| Error::FailedToParseCargoToml(Box::new(e)))?; if let Some(name) = toml.get("package").and_then(|p| p.get("name")) { return Ok(Some(name.to_string())); } } if let Ok(contents) = fs::read("package.json") { let json = serde_json::from_slice::(&contents) .map_err(Error::FailedToParsePacakgeJson)?; if let Some(name) = json.get("name") { return Ok(Some(name.to_string())); } } Ok(None) } let cwd = std::env::current_dir()?; std::env::set_current_dir(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; let res = find_nearset_pkg_name_inner(); std::env::set_current_dir(&cwd).map_err(|e| Error::IoWithPath(cwd, e))?; res } #[tracing::instrument(level = "trace")] fn parse_config_file + Debug>(path: P) -> Result, Config)>> { let p = path.as_ref().to_path_buf(); let path = p.canonicalize().map_err(|e| Error::IoWithPath(p, e))?; let content = fs::read_to_string(&path).map_err(|e| Error::IoWithPath(path.clone(), e))?; let mut configs = match path.extension().and_then(|e| e.to_str()) { Some("toml") => { if let Ok(configs) = toml::from_str::>(&content) { configs .into_iter() .map(|c| (Some(path.clone()), c)) .collect() } else { toml::from_str::(&content) .map_err(|e| Error::FailedToParseTomlConfigFromPath(path.clone(), Box::new(e))) .map(|config| vec![(Some(path), config)])? } } _ => { if let Ok(configs) = serde_json::from_str::>(&content) { configs .into_iter() .map(|c| (Some(path.clone()), c)) .collect() } else { serde_json::from_str::(&content) .map_err(|e| Error::FailedToParseJsonConfigFromPath(path.clone(), e)) .map(|config| vec![(Some(path), config)])? } } }; for (path, config) in &mut configs { // fill config.name if unset if config.name.is_none() { // and config wasn't passed using `--config` cli arg if let Some(path) = &path { let name = find_nearset_pkg_name(path)?; config.name = name; } } } Ok(configs) } #[tracing::instrument(level = "trace")] fn find_config_files() -> crate::Result> { let opts = glob::MatchOptions { case_sensitive: false, ..Default::default() }; Ok([ glob::glob_with("**/packager.toml", opts)? .flatten() .collect::>(), glob::glob_with("**/packager.json", opts)? .flatten() .collect::>(), ] .concat()) } #[tracing::instrument(level = "trace")] fn load_configs_from_cargo_workspace(cli: &super::Cli) -> Result, Config)>> { let profile = if cli.release { "release" } else if let Some(profile) = &cli.profile { profile.as_str() } else { "debug" }; let mut metadata_cmd = cargo_metadata::MetadataCommand::new(); if let Some(manifest_path) = &cli.manifest_path { metadata_cmd.manifest_path(manifest_path); } let metadata = match metadata_cmd.exec() { Ok(m) => m, Err(e) => { tracing::debug!("cargo metadata failed: {e}"); return Ok(Vec::new()); } }; let mut configs = Vec::new(); for package in metadata.workspace_packages().iter() { if let Some(config) = package.metadata.get("packager") { let mut config: Config = serde_json::from_value(config.to_owned()) .map_err(Error::FailedToParseJsonConfigCargoToml)?; if config.name.is_none() { config.name.replace(package.name.clone()); } if config.product_name.is_empty() { config.product_name.clone_from(&package.name); } if config.version.is_empty() { config.version = package.version.to_string(); } if config.identifier.is_none() { let author = package .authors .first() .map(|a| { let a = a.replace(['_', ' ', '.'], "-").to_lowercase(); a.strip_suffix('_').map(ToString::to_string).unwrap_or(a) }) .unwrap_or_else(|| format!("{}-author", package.name)); config .identifier .replace(format!("com.{}.{}", author, package.name)); } let mut cargo_out_dir = metadata.target_directory.as_std_path().to_path_buf(); if let Some(target_triple) = cli.target.as_ref().or(config.target_triple.as_ref()) { cargo_out_dir.push(target_triple); } cargo_out_dir.push(profile); if config.binaries_dir.is_none() { config.binaries_dir.replace(cargo_out_dir.clone()); } if config.out_dir.as_os_str().is_empty() { config.out_dir = cargo_out_dir; } if config.description.is_none() { config.description.clone_from(&package.description); } if config.authors.is_none() { config.authors = Some(package.authors.clone()); } if config.license_file.is_none() { config.license_file = package .license_file .as_ref() .map(|p| p.as_std_path().to_owned()); } // Auto-detect binaries if none were explicitly configured if config.binaries.is_empty() { let targets = package .targets .iter() .filter(|t| t.is_bin()) .collect::>(); for target in &targets { config.binaries.push(Binary { path: target.name.clone().into(), main: match targets.len() { 1 => true, _ => target.name == package.name, }, }) } } configs.push(( Some(package.manifest_path.as_std_path().to_path_buf()), config, )); } } Ok(configs) } pub fn detect_configs(cli: &super::Cli) -> Result, Config)>> { let configs = match &cli.config { // if a raw json object Some(c) if c.starts_with('{') => serde_json::from_str::(c) .map(|c| vec![(None, c)]) .map_err(Error::FailedToParseJsonConfig)?, // if a raw json array Some(c) if c.starts_with('[') => serde_json::from_str::>(c) .map_err(Error::FailedToParseJsonConfig)? .into_iter() .map(|c| (None, c)) .collect(), // if a path to config file Some(c) => parse_config_file(c)?, // fallback to config files and cargo workspaces configs _ => { let config_files = find_config_files()? .into_iter() .filter_map(|c| parse_config_file(c).ok()) .collect::>() .concat(); let cargo_configs = load_configs_from_cargo_workspace(cli)?; [config_files, cargo_configs] .concat() .into_iter() .filter(|(_, c)| c.should_pacakge(cli)) .collect() } }; Ok(configs) } ================================================ FILE: crates/packager/src/cli/error.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::path::PathBuf; use thiserror::Error; #[non_exhaustive] #[derive(Error, Debug)] /// Errors returned by cargo-packager. pub enum Error { /// Clap error. #[error(transparent)] #[cfg(feature = "cli")] Clap(#[from] clap::error::Error), /// Error while reading cargo metadata. #[error("Failed to read cargo metadata: {0}")] Metadata(#[from] cargo_metadata::Error), /// JSON parsing error. #[error(transparent)] Json(#[from] serde_json::Error), /// TOML parsing error. #[error(transparent)] Toml(#[from] toml::de::Error), /// JSON Config parsing error. #[error("Failed to parse config: {0}")] FailedToParseJsonConfig(serde_json::Error), #[error("Failed to deserialize config from `package.metadata.packager` in Cargo.toml: {0}")] FailedToParseJsonConfigCargoToml(serde_json::Error), /// TOML Config parsing error. #[error("Failed to parse config: {0}")] FailedToParseTomlConfig(Box), /// Cargo.toml parsing error. #[error("Failed to parse Cargo.toml: {0}")] FailedToParseCargoToml(Box), /// package.json parsing error. #[error("Failed to parse package.json: {0}")] FailedToParsePacakgeJson(serde_json::Error), /// JSON Config parsing error. #[error("Failed to parse config at {0}: {1}")] FailedToParseJsonConfigFromPath(PathBuf, serde_json::Error), /// TOML Config parsing error. #[error("Failed to parse config at {0}: {1}")] FailedToParseTomlConfigFromPath(PathBuf, Box), /// I/O errors with path. #[error("I/O Error ({0}): {1}")] IoWithPath(PathBuf, std::io::Error), /// I/O errors. #[error(transparent)] Io(#[from] std::io::Error), /// Packaging error #[error(transparent)] Packaging(#[from] crate::Error), } /// Convenient type alias of Result type for cargo-packager. pub type Result = std::result::Result; ================================================ FILE: crates/packager/src/cli/mod.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! The cli entry point use std::{ffi::OsString, fmt::Write, fs, path::PathBuf}; use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand}; use crate::{ config::{LogLevel, PackageFormat}, init_tracing_subscriber, package, parse_log_level, sign_outputs, util, SigningConfig, }; mod config; mod error; mod signer; use self::error::{Error, Result}; #[derive(Debug, Clone, Subcommand)] enum Commands { Signer(signer::Options), } #[derive(Parser, Debug)] #[clap( author, version, about, bin_name("cargo-packager"), propagate_version(true), no_binary_name(true) )] pub(crate) struct Cli { /// Enables verbose logging. #[clap(short, long, global = true, action = ArgAction::Count)] verbose: u8, /// Disables logging #[clap(short, long, global = true)] quite: bool, /// The package fromats to build. #[clap(short, long, value_enum, value_delimiter = ',')] formats: Option>, /// A configuration to read, which could be a JSON file, /// TOML file, or a raw JSON string. /// /// By default, cargo-packager looks for `{p,P}ackager.{toml,json}` and /// `[package.metadata.packager]` in `Cargo.toml` files. #[clap(short, long)] config: Option, /// Load a private key from a file or a string to sign the generated ouptuts. #[clap(short = 'k', long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY")] private_key: Option, /// The password for the signing private key. #[clap(long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD")] password: Option, /// Which packages to use from the current workspace. #[clap(short, long, value_delimiter = ',')] pub(crate) packages: Option>, /// The directory where the packages will be placed. /// /// If [`Config::binaries_dir`] is not defined, it is also the path where the binaries are located if they use relative paths. #[clap(short, long, alias = "out")] out_dir: Option, /// The directory where the [`Config::binaries`] exist. /// /// Defaults to [`Config::out_dir`] #[clap(long)] binaries_dir: Option, /// Package the release version of your app. /// Ignored when `--config` is used. #[clap(short, long, group = "cargo-profile")] release: bool, /// Cargo profile to use for packaging your app. /// Ignored when `--config` is used. #[clap(long, group = "cargo-profile")] profile: Option, /// Path to Cargo.toml manifest path to use for reading the configuration. /// Ignored when `--config` is used. #[clap(long)] manifest_path: Option, /// Target triple to use for detecting your app binaries. #[clap(long)] target: Option, #[command(subcommand)] command: Option, } #[tracing::instrument(level = "trace", skip(cli))] fn run_cli(cli: Cli) -> Result<()> { tracing::trace!(cli= ?cli); // run subcommand and exit if one was specified, // otherwise run the default packaging command if let Some(command) = cli.command { match command { Commands::Signer(opts) => signer::command(opts)?, } return Ok(()); } let configs = config::detect_configs(&cli)?; if configs.is_empty() { tracing::error!("Couldn't detect a valid configuration file or all configurations are disabled! Nothing to do here."); std::process::exit(1); } let cli_out_dir = cli .out_dir .as_ref() .map(|p| { if p.exists() { dunce::canonicalize(p).map_err(|e| Error::IoWithPath(p.clone(), e)) } else { fs::create_dir_all(p).map_err(|e| Error::IoWithPath(p.clone(), e))?; Ok(p.to_owned()) } }) .transpose()?; let private_key = match cli.private_key { Some(path) if PathBuf::from(&path).exists() => Some( fs::read_to_string(&path).map_err(|e| Error::IoWithPath(PathBuf::from(&path), e))?, ), k => k, }; let signing_config = private_key.map(|k| SigningConfig { private_key: k, password: cli.password, }); let mut outputs = Vec::new(); let mut signatures = Vec::new(); for (config_dir, mut config) in configs { tracing::trace!(config = ?config); if let Some(dir) = &cli_out_dir { config.out_dir.clone_from(dir) } if let Some(formats) = &cli.formats { config.formats.replace(formats.clone()); } if let Some(target_triple) = &cli.target { config.target_triple.replace(target_triple.clone()); } if config.log_level.is_none() && !cli.quite { let level = match parse_log_level(cli.verbose) { tracing::Level::ERROR => LogLevel::Error, tracing::Level::WARN => LogLevel::Warn, tracing::Level::INFO => LogLevel::Info, tracing::Level::DEBUG => LogLevel::Debug, tracing::Level::TRACE => LogLevel::Trace, }; config.log_level.replace(level); } if let Some(path) = config_dir { // change the directory to the config being built // so paths will be read relative to it let parent = path .parent() .ok_or_else(|| crate::Error::ParentDirNotFound(path.clone()))?; std::env::set_current_dir(parent) .map_err(|e| Error::IoWithPath(parent.to_path_buf(), e))?; } // create the packages let mut packages = package(&config)?; // sign the packages if let Some(signing_config) = &signing_config { let s = sign_outputs(signing_config, &mut packages)?; signatures.extend(s); } outputs.extend(packages); } // flatten paths let outputs = outputs .into_iter() .flat_map(|o| o.paths) .collect::>(); // print information when finished let len = outputs.len(); if len >= 1 { let pluralised = if len == 1 { "package" } else { "packages" }; let mut printable_paths = String::new(); for path in outputs { let _ = writeln!(printable_paths, " {}", util::display_path(path)); } tracing::info!( "Finished packaging {} {} at:\n{}", len, pluralised, printable_paths ); } let len = signatures.len(); if len >= 1 { let pluralised = if len == 1 { "signature" } else { "signatures" }; let mut printable_paths = String::new(); for path in signatures { let _ = writeln!(printable_paths, " {}", util::display_path(path)); } tracing::info!( "Finished signing packages, {} {} at:\n{}", len, pluralised, printable_paths ); } Ok(()) } /// Run the packager CLI pub fn run(args: I, bin_name: Option) where I: IntoIterator, A: Into + Clone, { if let Err(e) = try_run(args, bin_name) { tracing::error!("{}", e); std::process::exit(1); } } /// Try run the packager CLI pub fn try_run(args: I, bin_name: Option) -> Result<()> where I: IntoIterator, A: Into + Clone, { let cli = match &bin_name { Some(bin_name) => Cli::command().bin_name(bin_name), None => Cli::command(), }; let matches = cli.get_matches_from(args); let cli = Cli::from_arg_matches(&matches).map_err(|e| { e.format(&mut match &bin_name { Some(bin_name) => Cli::command().bin_name(bin_name), None => Cli::command(), }) })?; if !cli.quite { init_tracing_subscriber(cli.verbose); if std::env::var_os("CARGO_TERM_COLOR").is_none() { std::env::set_var("CARGO_TERM_COLOR", "always"); } } run_cli(cli) } ================================================ FILE: crates/packager/src/cli/signer/generate.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::path::PathBuf; use clap::Parser; use crate::cli::Result; #[derive(Debug, Clone, Parser)] #[clap(about = "Generate a new signing key to sign files")] pub struct Options { /// Set a password for the new signing key. #[clap(long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD")] password: Option, #[clap(long)] /// A path where the private key will be stored. path: Option, /// Overwrite the private key even if it exists on the specified path. #[clap(short, long)] force: bool, /// Run in CI mode and skip prompting for values. #[clap(long)] ci: bool, } pub fn command(mut options: Options) -> Result<()> { options.ci = options.ci || std::env::var("CI").is_ok(); if options.ci && options.password.is_none() { tracing::warn!("Generating a new private key without a password, for security reasons, we recommend setting a password instead."); options.password.replace("".into()); } tracing::info!("Generating a new signing key."); let keypair = crate::sign::generate_key(options.password)?; match options.path { Some(path) => { let keys = crate::sign::save_keypair(&keypair, path, options.force)?; tracing::info!( "Finished generating and saving the keys:\n {}\n {}", keys.0.display(), keys.1.display() ); } None => { tracing::info!("Finished generating secret key:\n{}", keypair.sk); tracing::info!("Finished generating public key:\n{}", keypair.pk); } } Ok(()) } ================================================ FILE: crates/packager/src/cli/signer/mod.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use clap::{Parser, Subcommand}; use super::Result; mod generate; mod sign; #[derive(Debug, Clone, Subcommand)] enum Commands { Sign(sign::Options), Generate(generate::Options), } #[derive(Debug, Clone, Parser)] #[clap(about = "Sign a file or generate a new signing key to sign files")] pub struct Options { #[command(subcommand)] command: Commands, } pub fn command(options: Options) -> Result<()> { match options.command { Commands::Sign(opts) => sign::command(opts), Commands::Generate(opts) => generate::command(opts), } } ================================================ FILE: crates/packager/src/cli/signer/sign.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{fs, path::PathBuf}; use clap::Parser; use crate::cli::{Error, Result}; #[derive(Debug, Clone, Parser)] #[clap(about = "Sign a file")] pub struct Options { /// Load the private key from a file or a string. #[clap(short = 'k', long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY")] private_key: Option, /// The password for the private key. #[clap(long, env = "CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD")] password: Option, /// The file to be signed. file: PathBuf, } pub fn command(options: Options) -> Result<()> { let private_key = match options.private_key { Some(path) if PathBuf::from(&path).exists() => { fs::read_to_string(&path).map_err(|e| Error::IoWithPath(PathBuf::from(&path), e))? } Some(key) => key, None => { tracing::error!("--private-key was not specified, aborting signign."); std::process::exit(1); } }; let config = crate::sign::SigningConfig { private_key, password: Some(options.password.unwrap_or_default()), }; let signature_path = crate::sign::sign_file(&config, options.file)?; tracing::info!( "Signed the file successfully! find the signature at: {}", signature_path.display() ); Ok(()) } ================================================ FILE: crates/packager/src/codesign/macos.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ cmp::Ordering, ffi::OsString, fs::File, io::prelude::*, path::{Path, PathBuf}, process::Command, }; use serde::Deserialize; use crate::{config::MacOsNotarizationCredentials, shell::CommandExt, Config, Error}; const KEYCHAIN_ID: &str = "cargo-packager.keychain"; const KEYCHAIN_PWD: &str = "cargo-packager"; // Import certificate from ENV variables. // APPLE_CERTIFICATE is the p12 certificate base64 encoded. // By example you can use; openssl base64 -in MyCertificate.p12 -out MyCertificate-base64.txt // Then use the value of the base64 in APPLE_CERTIFICATE env variable. // You need to set APPLE_CERTIFICATE_PASSWORD to the password you set when you exported your certificate. // https://help.apple.com/xcode/mac/current/#/dev154b28f09 see: `Export a signing certificate` #[tracing::instrument(level = "trace")] pub fn setup_keychain( certificate_encoded: OsString, certificate_password: OsString, ) -> crate::Result<()> { // we delete any previous version of our keychain if present delete_keychain(); tracing::info!("Setting up keychain from environment variables..."); let keychain_list_output = Command::new("security") .args(["list-keychain", "-d", "user"]) .output() .map_err(Error::FailedToListKeyChain)?; let tmp_dir = tempfile::tempdir()?; let cert_path = tmp_dir .path() .join("cert.p12") .to_string_lossy() .to_string(); let cert_path_tmp = tmp_dir.path().join("cert.p12.tmp"); let cert_path_tmp_str = cert_path_tmp.to_string_lossy().to_string(); let certificate_encoded = certificate_encoded .to_str() .expect("failed to convert APPLE_CERTIFICATE to string") .as_bytes(); let certificate_password = certificate_password .to_str() .expect("failed to convert APPLE_CERTIFICATE_PASSWORD to string") .to_string(); // as certificate contain whitespace decoding may be broken // https://github.com/marshallpierce/rust-base64/issues/105 // we'll use builtin base64 command from the OS let mut tmp_cert = File::create(&cert_path_tmp).map_err(|e| Error::IoWithPath(cert_path_tmp, e))?; tmp_cert.write_all(certificate_encoded)?; Command::new("base64") .args(["--decode", "-i", &cert_path_tmp_str, "-o", &cert_path]) .output_ok() .map_err(Error::FailedToDecodeCert)?; Command::new("security") .args(["create-keychain", "-p", KEYCHAIN_PWD, KEYCHAIN_ID]) .output_ok() .map_err(Error::FailedToCreateKeyChain)?; Command::new("security") .args(["unlock-keychain", "-p", KEYCHAIN_PWD, KEYCHAIN_ID]) .output_ok() .map_err(Error::FailedToUnlockKeyChain)?; Command::new("security") .args([ "import", &cert_path, "-k", KEYCHAIN_ID, "-P", &certificate_password, "-T", "/usr/bin/codesign", "-T", "/usr/bin/pkgbuild", "-T", "/usr/bin/productbuild", ]) .output_ok() .map_err(Error::FailedToImportCert)?; Command::new("security") .args(["set-keychain-settings", "-t", "3600", "-u", KEYCHAIN_ID]) .output_ok() .map_err(Error::FailedToSetKeychainSettings)?; Command::new("security") .args([ "set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", KEYCHAIN_PWD, KEYCHAIN_ID, ]) .output_ok() .map_err(Error::FailedToSetKeyPartitionList)?; let current_keychains = String::from_utf8_lossy(&keychain_list_output.stdout) .split('\n') .map(|line| { line.trim_matches(|c: char| c.is_whitespace() || c == '"') .to_string() }) .filter(|l| !l.is_empty()) .collect::>(); Command::new("security") .args(["list-keychain", "-d", "user", "-s"]) .args(current_keychains) .arg(KEYCHAIN_ID) .output_ok() .map_err(Error::FailedToListKeyChain)?; Ok(()) } #[tracing::instrument(level = "trace")] pub fn delete_keychain() { // delete keychain if needed and skip any error let _ = Command::new("security") .arg("delete-keychain") .arg(KEYCHAIN_ID) .output_ok(); } #[derive(Debug, PartialEq, Eq)] pub struct SignTarget { pub path: PathBuf, pub is_native_binary: bool, } impl Ord for SignTarget { fn cmp(&self, other: &Self) -> Ordering { let self_count = self.path.components().count(); let other_count = other.path.components().count(); // Sort by path depth (note that we compare other to self, not self to other) so // that longer paths have a smaller value! other_count.cmp(&self_count) } } impl PartialOrd for SignTarget { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } #[tracing::instrument(level = "trace", skip(config))] pub fn try_sign(targets: Vec, identity: &str, config: &Config) -> crate::Result<()> { let certificate_encoded = config .macos() .and_then(|m| m.signing_certificate.clone()) .or_else(|| std::env::var_os("APPLE_CERTIFICATE")); let certificate_password = config .macos() .and_then(|m| m.signing_certificate_password.clone()) .or_else(|| std::env::var_os("APPLE_CERTIFICATE_PASSWORD")); let packager_keychain = if let (Some(certificate_encoded), Some(certificate_password)) = (certificate_encoded, certificate_password) { // setup keychain allow you to import your certificate // for CI build setup_keychain(certificate_encoded, certificate_password)?; true } else { false }; for target in targets { sign( &target.path, identity, config, target.is_native_binary, packager_keychain, )?; } if packager_keychain { // delete the keychain again after signing delete_keychain(); } Ok(()) } #[tracing::instrument(level = "trace", skip(config))] fn sign( path_to_sign: &Path, identity: &str, config: &Config, is_native_binary: bool, packager_keychain: bool, ) -> crate::Result<()> { tracing::info!( "Codesigning {} with identity \"{}\"", path_to_sign.display(), identity ); let mut args = vec!["--force", "-s", identity]; if packager_keychain { args.push("--keychain"); args.push(KEYCHAIN_ID); } if let Some(entitlements_path) = config.macos().and_then(|macos| macos.entitlements.as_ref()) { args.push("--entitlements"); args.push(entitlements_path); } if is_native_binary { args.push("--options"); args.push("runtime"); } args.push("--timestamp"); Command::new("codesign") .args(args) .arg(path_to_sign) .output_ok() .map_err(Error::FailedToRunCodesign)?; Ok(()) } #[derive(Deserialize, Debug)] struct NotarytoolSubmitOutput { id: String, status: String, message: String, } #[tracing::instrument(level = "trace", skip(config))] pub fn notarize( app_bundle_path: PathBuf, auth: MacOsNotarizationCredentials, config: &Config, ) -> crate::Result<()> { let bundle_stem = app_bundle_path .file_stem() .ok_or_else(|| Error::FailedToExtractFilename(app_bundle_path.clone()))?; let tmp_dir = tempfile::tempdir()?; let zip_path = tmp_dir .path() .join(format!("{}.zip", bundle_stem.to_string_lossy())); let app_bundle_path_str = app_bundle_path.to_string_lossy().to_string(); let zip_path_str = zip_path.to_string_lossy().to_string(); let zip_args = vec![ "-c", "-k", "--keepParent", "--sequesterRsrc", &app_bundle_path_str, &zip_path_str, ]; // use ditto to create a PKZip almost identical to Finder // this remove almost 99% of false alarm in notarization Command::new("ditto") .args(zip_args) .output_ok() .map_err(Error::FailedToRunDitto)?; // sign the zip file if let Some(identity) = &config .macos() .and_then(|macos| macos.signing_identity.as_ref()) { try_sign( vec![SignTarget { path: zip_path.clone(), is_native_binary: false, }], identity, config, )?; }; let zip_path_str = zip_path.to_string_lossy().to_string(); let notarize_args = vec![ "notarytool", "submit", &zip_path_str, "--wait", "--output-format", "json", ]; tracing::info!("Notarizing {}", app_bundle_path.display()); let output = Command::new("xcrun") .args(notarize_args) .notarytool_args(&auth) .output_ok() .map_err(Error::FailedToRunXcrun)?; if !output.status.success() { return Err(Error::FailedToNotarize); } let output_str = String::from_utf8_lossy(&output.stdout); if let Ok(submit_output) = serde_json::from_str::(&output_str) { let log_message = format!( "Finished with status {} for id {} ({})", submit_output.status, submit_output.id, submit_output.message ); if submit_output.status == "Accepted" { tracing::info!("Notarizing {}", log_message); staple_app(app_bundle_path)?; Ok(()) } else if let Ok(output) = Command::new("xcrun") .args(["notarytool", "log"]) .arg(&submit_output.id) .notarytool_args(&auth) .output_ok() { Err(Error::NotarizeRejected(format!( "{log_message}\nLog:\n{}", String::from_utf8_lossy(&output.stdout), ))) } else { Err(Error::NotarizeRejected(log_message)) } } else { Err(Error::FailedToParseNotarytoolOutput( output_str.into_owned(), )) } } fn staple_app(app_bundle_path: PathBuf) -> crate::Result<()> { let filename = app_bundle_path .file_name() .ok_or_else(|| Error::FailedToExtractFilename(app_bundle_path.clone()))? .to_string_lossy() .to_string(); let app_bundle_path_dir = app_bundle_path .parent() .ok_or_else(|| Error::ParentDirNotFound(app_bundle_path.clone()))?; Command::new("xcrun") .args(vec!["stapler", "staple", "-v", &filename]) .current_dir(app_bundle_path_dir) .output_ok() .map_err(Error::FailedToRunXcrun)?; Ok(()) } pub trait NotarytoolCmdExt { fn notarytool_args(&mut self, auth: &MacOsNotarizationCredentials) -> &mut Self; } impl NotarytoolCmdExt for Command { fn notarytool_args(&mut self, auth: &MacOsNotarizationCredentials) -> &mut Self { match auth { MacOsNotarizationCredentials::AppleId { apple_id, password, team_id, } => { self.arg("--apple-id") .arg(apple_id) .arg("--password") .arg(password) .arg("--team-id") .arg(team_id); self } MacOsNotarizationCredentials::ApiKey { key_id, key_path, issuer, } => self .arg("--key-id") .arg(key_id) .arg("--key") .arg(key_path) .arg("--issuer") .arg(issuer), MacOsNotarizationCredentials::KeychainProfile { keychain_profile } => { self.arg("--keychain-profile").arg(keychain_profile) } } } } #[tracing::instrument(level = "trace")] pub fn notarize_auth() -> crate::Result { if let Some(keychain_profile) = std::env::var_os("APPLE_KEYCHAIN_PROFILE") { Ok(MacOsNotarizationCredentials::KeychainProfile { keychain_profile }) } else { match ( std::env::var_os("APPLE_ID"), std::env::var_os("APPLE_PASSWORD"), std::env::var_os("APPLE_TEAM_ID"), ) { (Some(apple_id), Some(password), Some(team_id)) => { Ok(MacOsNotarizationCredentials::AppleId { apple_id, password, team_id, }) } _ => { match ( std::env::var_os("APPLE_API_KEY"), std::env::var_os("APPLE_API_ISSUER"), std::env::var("APPLE_API_KEY_PATH"), ) { (Some(key_id), Some(issuer), Ok(key_path)) => { Ok(MacOsNotarizationCredentials::ApiKey { key_id, key_path: key_path.into(), issuer, }) } (Some(key_id), Some(issuer), Err(_)) => { let mut api_key_file_name = OsString::from("AuthKey_"); api_key_file_name.push(&key_id); api_key_file_name.push(".p8"); let mut key_path = None; let mut search_paths = vec!["./private_keys".into()]; if let Some(home_dir) = dirs::home_dir() { search_paths.push(home_dir.join("private_keys")); search_paths.push(home_dir.join(".private_keys")); search_paths.push(home_dir.join(".appstoreconnect/private_keys")); } for folder in search_paths { if let Some(path) = find_api_key(folder, &api_key_file_name) { key_path = Some(path); break; } } if let Some(key_path) = key_path { Ok(MacOsNotarizationCredentials::ApiKey { key_id, key_path, issuer, }) } else { Err(Error::ApiKeyMissing { filename: api_key_file_name .into_string() .expect("failed to convert api_key_file_name to string"), }) } } _ => Err(Error::MissingNotarizeAuthVars), } } } } } fn find_api_key(folder: PathBuf, file_name: &OsString) -> Option { let path = folder.join(file_name); if path.exists() { Some(path) } else { None } } ================================================ FILE: crates/packager/src/codesign/mod.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT #[cfg(target_os = "macos")] pub mod macos; pub mod windows; ================================================ FILE: crates/packager/src/codesign/windows.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{fmt::Debug, path::Path, process::Command}; #[cfg(windows)] use once_cell::sync::Lazy; #[cfg(windows)] use std::path::PathBuf; use crate::{config::Config, shell::CommandExt, util}; #[cfg(windows)] use crate::util::Bitness; #[derive(Debug)] #[allow(dead_code)] pub struct SignParams { pub product_name: String, pub digest_algorithm: String, pub certificate_thumbprint: String, pub timestamp_url: Option, pub tsp: bool, pub sign_command: Option, } impl Config { pub(crate) fn can_sign(&self) -> bool { self.windows() .and_then(|w| w.certificate_thumbprint.as_ref()) .is_some() || self.custom_sign_command() } pub(crate) fn custom_sign_command(&self) -> bool { self.windows() .and_then(|w| w.sign_command.as_ref()) .is_some() } pub(crate) fn sign_params(&self) -> SignParams { let windows = self.windows(); SignParams { product_name: self.product_name.clone(), digest_algorithm: windows .and_then(|w| w.digest_algorithm.as_ref()) .cloned() .unwrap_or_else(|| "sha256".to_string()), certificate_thumbprint: windows .and_then(|w| w.certificate_thumbprint.as_ref()) .cloned() .unwrap_or_default(), timestamp_url: windows.and_then(|w| w.timestamp_url.as_ref()).cloned(), tsp: windows.map(|w| w.tsp).unwrap_or_default(), sign_command: windows.and_then(|w| w.sign_command.as_ref()).cloned(), } } } #[cfg(windows)] static SIGN_TOOL: Lazy> = Lazy::new(|| { let _s = tracing::span!(tracing::Level::TRACE, "locate_signtool"); const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots"; const KITS_ROOT_REGVALUE_NAME: &str = r"KitsRoot10"; // Open 32-bit HKLM "Installed Roots" key let installed_roots_key = windows_registry::LOCAL_MACHINE .open(INSTALLED_ROOTS_REGKEY_PATH) .map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?; // Get the Windows SDK root path let kits_root_10_path: String = installed_roots_key .get_string(KITS_ROOT_REGVALUE_NAME) .map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?; // Construct Windows SDK bin path let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin"); let mut installed_kits: Vec = installed_roots_key .keys() .map_err(|_| crate::Error::FailedToEnumerateRegKeys)? .collect(); // Sort installed kits installed_kits.sort(); /* Iterate through installed kit version keys in reverse (from newest to oldest), adding their bin paths to the list. Windows SDK 10 v10.0.15063.468 and later will have their signtools located there. */ let mut kit_bin_paths: Vec = installed_kits .iter() .rev() .map(|kit| kits_root_10_bin_path.join(kit)) .collect(); /* Add kits root bin path. For Windows SDK 10 versions earlier than v10.0.15063.468, signtool will be located there. */ kit_bin_paths.push(kits_root_10_bin_path); // Choose which version of SignTool to use based on OS bitness let arch_dir = match util::os_bitness().expect("failed to get os bitness") { Bitness::X86_32 => "x86", Bitness::X86_64 => "x64", _ => return Err(crate::Error::UnsupportedBitness), }; /* Iterate through all bin paths, checking for existence of a SignTool executable. */ for kit_bin_path in &kit_bin_paths { /* Construct SignTool path. */ let signtool_path = kit_bin_path.join(arch_dir).join("signtool.exe"); /* Check if SignTool exists at this location. */ if signtool_path.exists() { // SignTool found. Return it. return Ok(signtool_path); } } Err(crate::Error::SignToolNotFound) }); #[cfg(windows)] fn signtool() -> Option { (*SIGN_TOOL).as_ref().ok().cloned() } #[tracing::instrument(level = "trace")] pub fn sign_command_custom + Debug>( path: P, command: &str, ) -> crate::Result { let mut args = command.trim().split(' '); let bin = args .next() .expect("custom signing command doesn't contain a bin?"); let mut cmd = Command::new(bin); for arg in args { if arg == "%1" { cmd.arg(path.as_ref()); } else { cmd.arg(arg); } } Ok(cmd) } #[cfg(windows)] #[tracing::instrument(level = "trace")] pub fn sign_command_default + Debug>( path: P, params: &SignParams, ) -> crate::Result { let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?; let mut cmd = Command::new(signtool); cmd.arg("sign"); cmd.args(["/fd", ¶ms.digest_algorithm]); cmd.args(["/sha1", ¶ms.certificate_thumbprint]); cmd.args(["/d", ¶ms.product_name]); if let Some(ref timestamp_url) = params.timestamp_url { if params.tsp { cmd.args(["/tr", timestamp_url]); cmd.args(["/td", ¶ms.digest_algorithm]); } else { cmd.args(["/t", timestamp_url]); } } cmd.arg(path.as_ref()); Ok(cmd) } #[tracing::instrument(level = "trace")] pub fn sign_command + Debug>( path: P, params: &SignParams, ) -> crate::Result { match ¶ms.sign_command { Some(custom_command) => sign_command_custom(path, custom_command), #[cfg(windows)] None => sign_command_default(path, params), // should not be reachable #[cfg(not(windows))] None => Ok(Command::new("")), } } #[tracing::instrument(level = "trace")] pub fn sign_custom + Debug>(path: P, custom_command: &str) -> crate::Result<()> { let path = path.as_ref(); tracing::info!( "Codesigning {} with a custom signing command", util::display_path(path), ); let mut cmd = sign_command_custom(path, custom_command)?; let output = cmd .output_ok() .map_err(crate::Error::CustomSignCommandFailed)?; let stdout = String::from_utf8_lossy(output.stdout.as_slice()); tracing::info!("{:?}", stdout); Ok(()) } #[tracing::instrument(level = "trace")] #[cfg(windows)] pub fn sign_default + Debug>(path: P, params: &SignParams) -> crate::Result<()> { let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?; let path = path.as_ref(); tracing::info!( "Codesigning {} with certificate \"{}\"", util::display_path(path), params.certificate_thumbprint ); let mut cmd = sign_command_default(path, params)?; tracing::debug!("Running signtool {:?}", signtool); let output = cmd.output_ok().map_err(crate::Error::SignToolFailed)?; let stdout = String::from_utf8_lossy(output.stdout.as_slice()); tracing::debug!("{:?}", stdout); Ok(()) } #[tracing::instrument(level = "trace")] pub fn sign + Debug>(path: P, params: &SignParams) -> crate::Result<()> { match ¶ms.sign_command { Some(custom_command) => sign_custom(path, custom_command), #[cfg(windows)] None => sign_default(path, params), // should not be reachable #[cfg(not(windows))] None => Ok(()), } } #[tracing::instrument(level = "trace", skip(config))] pub fn try_sign( file_path: &std::path::PathBuf, config: &crate::config::Config, ) -> crate::Result<()> { if config.can_sign() { sign(file_path, &config.sign_params())?; } Ok(()) } ================================================ FILE: crates/packager/src/config/builder.rs ================================================ use std::path::PathBuf; use crate::{Config, PackageFormat}; use super::{ AppImageConfig, Binary, DebianConfig, FileAssociation, HookCommand, LogLevel, MacOsConfig, NsisConfig, PacmanConfig, Resource, WindowsConfig, WixConfig, }; /// A builder type for [`Config`]. #[derive(Default)] pub struct ConfigBuilder(Config); impl ConfigBuilder { /// Creates a new config builder. pub fn new() -> Self { Self::default() } /// Returns a reference to the config used by this builder. pub fn config(&self) -> &Config { &self.0 } /// Sets [`Config::product_name`]. pub fn product_name>(mut self, product_name: S) -> Self { self.0.product_name = product_name.into(); self } /// Sets [`Config::version`]. pub fn version>(mut self, version: S) -> Self { self.0.version = version.into(); self } /// Sets [`Config::binaries`]. pub fn binaries>(mut self, binaries: I) -> Self { self.0.binaries = binaries.into_iter().collect(); self } /// Sets [`Config::identifier`]. pub fn identifier>(mut self, identifier: S) -> Self { self.0.identifier.replace(identifier.into()); self } /// Sets [`Config::before_packaging_command`]. pub fn before_packaging_command(mut self, command: HookCommand) -> Self { self.0.before_packaging_command.replace(command); self } /// Sets [`Config::before_each_package_command`]. pub fn before_each_package_command(mut self, command: HookCommand) -> Self { self.0.before_each_package_command.replace(command); self } /// Sets [`Config::log_level`]. pub fn log_level(mut self, level: LogLevel) -> Self { self.0.log_level.replace(level); self } /// Sets [`Config::formats`]. pub fn formats>(mut self, formats: I) -> Self { self.0.formats = Some(formats.into_iter().collect()); self } /// Sets [`Config::out_dir`]. pub fn out_dir>(mut self, path: P) -> Self { self.0.out_dir = path.into(); self } /// Sets [`Config::target_triple`]. pub fn target_triple>(mut self, target_triple: S) -> Self { self.0.target_triple.replace(target_triple.into()); self } /// Sets [`Config::description`]. pub fn description>(mut self, description: S) -> Self { self.0.description.replace(description.into()); self } /// Sets [`Config::long_description`]. pub fn long_description>(mut self, long_description: S) -> Self { self.0.long_description.replace(long_description.into()); self } /// Sets [`Config::homepage`]. pub fn homepage>(mut self, homepage: S) -> Self { self.0.homepage.replace(homepage.into()); self } /// Sets [`Config::authors`]. pub fn authors(mut self, authors: I) -> Self where I: IntoIterator, S: Into, { self.0 .authors .replace(authors.into_iter().map(Into::into).collect()); self } /// Sets [`Config::publisher`]. pub fn publisher>(mut self, publisher: S) -> Self { self.0.publisher.replace(publisher.into()); self } /// Sets [`Config::license_file`]. pub fn license_file>(mut self, license_file: P) -> Self { self.0.license_file.replace(license_file.into()); self } /// Sets [`Config::copyright`]. pub fn copyright>(mut self, copyright: S) -> Self { self.0.copyright.replace(copyright.into()); self } /// Sets [`Config::icons`]. pub fn icons(mut self, icons: I) -> Self where I: IntoIterator, S: Into, { self.0 .icons .replace(icons.into_iter().map(Into::into).collect()); self } /// Sets [`Config::file_associations`]. pub fn file_associations>( mut self, file_associations: I, ) -> Self { self.0 .file_associations .replace(file_associations.into_iter().collect()); self } /// Sets [`Config::resources`]. pub fn resources>(mut self, resources: I) -> Self { self.0.resources.replace(resources.into_iter().collect()); self } /// Sets [`Config::external_binaries`]. pub fn external_binaries(mut self, external_binaries: I) -> Self where I: IntoIterator, P: Into, { self.0 .external_binaries .replace(external_binaries.into_iter().map(Into::into).collect()); self } /// Set the [Windows](Config::windows) specific configuration. pub fn windows(mut self, windows: WindowsConfig) -> Self { self.0.windows.replace(windows); self } /// Set the [MacOS](Config::macos) specific configuration. pub fn macos(mut self, macos: MacOsConfig) -> Self { self.0.macos.replace(macos); self } /// Set the [WiX](Config::wix) specific configuration. pub fn wix(mut self, wix: WixConfig) -> Self { self.0.wix.replace(wix); self } /// Set the [Nsis](Config::nsis) specific configuration. pub fn nsis(mut self, nsis: NsisConfig) -> Self { self.0.nsis.replace(nsis); self } /// Set the [Debian](Config::deb) specific configuration. pub fn deb(mut self, deb: DebianConfig) -> Self { self.0.deb.replace(deb); self } /// Set the [Appimage](Config::appimage) specific configuration. pub fn appimage(mut self, appimage: AppImageConfig) -> Self { self.0.appimage.replace(appimage); self } /// Set the [Pacman](Config::pacman) specific configuration. pub fn pacman(mut self, pacman: PacmanConfig) -> Self { self.0.pacman.replace(pacman); self } } ================================================ FILE: crates/packager/src/config/category.rs ================================================ // Copyright 2016-2019 Cargo-Bundle developers // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{fmt, str::FromStr}; use serde::Serialize; const CONFIDENCE_THRESHOLD: f64 = 0.8; const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category."; /// The possible app categories. /// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian. #[allow(missing_docs)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[non_exhaustive] pub enum AppCategory { Business, DeveloperTool, Education, Entertainment, Finance, Game, ActionGame, AdventureGame, ArcadeGame, BoardGame, CardGame, CasinoGame, DiceGame, EducationalGame, FamilyGame, KidsGame, MusicGame, PuzzleGame, RacingGame, RolePlayingGame, SimulationGame, SportsGame, StrategyGame, TriviaGame, WordGame, GraphicsAndDesign, HealthcareAndFitness, Lifestyle, Medical, Music, News, Photography, Productivity, Reference, SocialNetworking, Sports, Travel, Utility, Video, Weather, } impl FromStr for AppCategory { type Err = Option<&'static str>; /// Given a string, returns the `AppCategory` it refers to, or the closest /// string that the user might have intended (if any). fn from_str(input: &str) -> Result { // Canonicalize input: let mut input = input.to_ascii_lowercase(); if input.starts_with(MACOS_APP_CATEGORY_PREFIX) { input = input .split_at(MACOS_APP_CATEGORY_PREFIX.len()) .1 .to_string(); } input = input.replace(' ', ""); input = input.replace('-', ""); // Find best match: let mut best_confidence = 0.0; let mut best_category: Option = None; for &(string, category) in CATEGORY_STRINGS.iter() { if input == string { return Ok(category); } let confidence = strsim::jaro_winkler(&input, string); if confidence >= CONFIDENCE_THRESHOLD && confidence > best_confidence { best_confidence = confidence; best_category = Some(category); } } Err(best_category.map(AppCategory::canonical)) } } impl AppCategory { /// Map an AppCategory to the string we recommend to use in Cargo.toml if /// the users misspells the category name. fn canonical(self) -> &'static str { match self { AppCategory::Business => "Business", AppCategory::DeveloperTool => "Developer Tool", AppCategory::Education => "Education", AppCategory::Entertainment => "Entertainment", AppCategory::Finance => "Finance", AppCategory::Game => "Game", AppCategory::ActionGame => "Action Game", AppCategory::AdventureGame => "Adventure Game", AppCategory::ArcadeGame => "Arcade Game", AppCategory::BoardGame => "Board Game", AppCategory::CardGame => "Card Game", AppCategory::CasinoGame => "Casino Game", AppCategory::DiceGame => "Dice Game", AppCategory::EducationalGame => "Educational Game", AppCategory::FamilyGame => "Family Game", AppCategory::KidsGame => "Kids Game", AppCategory::MusicGame => "Music Game", AppCategory::PuzzleGame => "Puzzle Game", AppCategory::RacingGame => "Racing Game", AppCategory::RolePlayingGame => "Role-Playing Game", AppCategory::SimulationGame => "Simulation Game", AppCategory::SportsGame => "Sports Game", AppCategory::StrategyGame => "Strategy Game", AppCategory::TriviaGame => "Trivia Game", AppCategory::WordGame => "Word Game", AppCategory::GraphicsAndDesign => "Graphics and Design", AppCategory::HealthcareAndFitness => "Healthcare and Fitness", AppCategory::Lifestyle => "Lifestyle", AppCategory::Medical => "Medical", AppCategory::Music => "Music", AppCategory::News => "News", AppCategory::Photography => "Photography", AppCategory::Productivity => "Productivity", AppCategory::Reference => "Reference", AppCategory::SocialNetworking => "Social Networking", AppCategory::Sports => "Sports", AppCategory::Travel => "Travel", AppCategory::Utility => "Utility", AppCategory::Video => "Video", AppCategory::Weather => "Weather", } } /// Map an AppCategory to the closest set of GNOME desktop registered /// categories that matches that category. pub fn gnome_desktop_categories(self) -> &'static str { match &self { AppCategory::Business => "Office;", AppCategory::DeveloperTool => "Development;", AppCategory::Education => "Education;", AppCategory::Entertainment => "Network;", AppCategory::Finance => "Office;Finance;", AppCategory::Game => "Game;", AppCategory::ActionGame => "Game;ActionGame;", AppCategory::AdventureGame => "Game;AdventureGame;", AppCategory::ArcadeGame => "Game;ArcadeGame;", AppCategory::BoardGame => "Game;BoardGame;", AppCategory::CardGame => "Game;CardGame;", AppCategory::CasinoGame => "Game;", AppCategory::DiceGame => "Game;", AppCategory::EducationalGame => "Game;Education;", AppCategory::FamilyGame => "Game;", AppCategory::KidsGame => "Game;KidsGame;", AppCategory::MusicGame => "Game;", AppCategory::PuzzleGame => "Game;LogicGame;", AppCategory::RacingGame => "Game;", AppCategory::RolePlayingGame => "Game;RolePlaying;", AppCategory::SimulationGame => "Game;Simulation;", AppCategory::SportsGame => "Game;SportsGame;", AppCategory::StrategyGame => "Game;StrategyGame;", AppCategory::TriviaGame => "Game;", AppCategory::WordGame => "Game;", AppCategory::GraphicsAndDesign => "Graphics;", AppCategory::HealthcareAndFitness => "Science;", AppCategory::Lifestyle => "Education;", AppCategory::Medical => "Science;MedicalSoftware;", AppCategory::Music => "AudioVideo;Audio;Music;", AppCategory::News => "Network;News;", AppCategory::Photography => "Graphics;Photography;", AppCategory::Productivity => "Office;", AppCategory::Reference => "Education;", AppCategory::SocialNetworking => "Network;", AppCategory::Sports => "Education;Sports;", AppCategory::Travel => "Education;", AppCategory::Utility => "Utility;", AppCategory::Video => "AudioVideo;Video;", AppCategory::Weather => "Science;", } } /// Map an AppCategory to the closest LSApplicationCategoryType value that /// matches that category. pub fn macos_application_category_type(self) -> &'static str { match &self { AppCategory::Business => "public.app-category.business", AppCategory::DeveloperTool => "public.app-category.developer-tools", AppCategory::Education => "public.app-category.education", AppCategory::Entertainment => "public.app-category.entertainment", AppCategory::Finance => "public.app-category.finance", AppCategory::Game => "public.app-category.games", AppCategory::ActionGame => "public.app-category.action-games", AppCategory::AdventureGame => "public.app-category.adventure-games", AppCategory::ArcadeGame => "public.app-category.arcade-games", AppCategory::BoardGame => "public.app-category.board-games", AppCategory::CardGame => "public.app-category.card-games", AppCategory::CasinoGame => "public.app-category.casino-games", AppCategory::DiceGame => "public.app-category.dice-games", AppCategory::EducationalGame => "public.app-category.educational-games", AppCategory::FamilyGame => "public.app-category.family-games", AppCategory::KidsGame => "public.app-category.kids-games", AppCategory::MusicGame => "public.app-category.music-games", AppCategory::PuzzleGame => "public.app-category.puzzle-games", AppCategory::RacingGame => "public.app-category.racing-games", AppCategory::RolePlayingGame => "public.app-category.role-playing-games", AppCategory::SimulationGame => "public.app-category.simulation-games", AppCategory::SportsGame => "public.app-category.sports-games", AppCategory::StrategyGame => "public.app-category.strategy-games", AppCategory::TriviaGame => "public.app-category.trivia-games", AppCategory::WordGame => "public.app-category.word-games", AppCategory::GraphicsAndDesign => "public.app-category.graphics-design", AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness", AppCategory::Lifestyle => "public.app-category.lifestyle", AppCategory::Medical => "public.app-category.medical", AppCategory::Music => "public.app-category.music", AppCategory::News => "public.app-category.news", AppCategory::Photography => "public.app-category.photography", AppCategory::Productivity => "public.app-category.productivity", AppCategory::Reference => "public.app-category.reference", AppCategory::SocialNetworking => "public.app-category.social-networking", AppCategory::Sports => "public.app-category.sports", AppCategory::Travel => "public.app-category.travel", AppCategory::Utility => "public.app-category.utilities", AppCategory::Video => "public.app-category.video", AppCategory::Weather => "public.app-category.weather", } } } impl<'d> serde::Deserialize<'d> for AppCategory { fn deserialize>(deserializer: D) -> Result { deserializer.deserialize_str(AppCategoryVisitor { did_you_mean: None }) } } struct AppCategoryVisitor { did_you_mean: Option<&'static str>, } impl serde::de::Visitor<'_> for AppCategoryVisitor { type Value = AppCategory; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self.did_you_mean { Some(string) => write!( formatter, "a valid app category string (did you mean \"{string}\"?)" ), None => write!(formatter, "a valid app category string"), } } fn visit_str(mut self, value: &str) -> Result { match AppCategory::from_str(value) { Ok(category) => Ok(category), Err(did_you_mean) => { self.did_you_mean = did_you_mean; let unexp = serde::de::Unexpected::Str(value); Err(serde::de::Error::invalid_value(unexp, &self)) } } } } const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[ ("actiongame", AppCategory::ActionGame), ("actiongames", AppCategory::ActionGame), ("adventuregame", AppCategory::AdventureGame), ("adventuregames", AppCategory::AdventureGame), ("arcadegame", AppCategory::ArcadeGame), ("arcadegames", AppCategory::ArcadeGame), ("boardgame", AppCategory::BoardGame), ("boardgames", AppCategory::BoardGame), ("business", AppCategory::Business), ("cardgame", AppCategory::CardGame), ("cardgames", AppCategory::CardGame), ("casinogame", AppCategory::CasinoGame), ("casinogames", AppCategory::CasinoGame), ("developer", AppCategory::DeveloperTool), ("developertool", AppCategory::DeveloperTool), ("developertools", AppCategory::DeveloperTool), ("development", AppCategory::DeveloperTool), ("dicegame", AppCategory::DiceGame), ("dicegames", AppCategory::DiceGame), ("education", AppCategory::Education), ("educationalgame", AppCategory::EducationalGame), ("educationalgames", AppCategory::EducationalGame), ("entertainment", AppCategory::Entertainment), ("familygame", AppCategory::FamilyGame), ("familygames", AppCategory::FamilyGame), ("finance", AppCategory::Finance), ("fitness", AppCategory::HealthcareAndFitness), ("game", AppCategory::Game), ("games", AppCategory::Game), ("graphicdesign", AppCategory::GraphicsAndDesign), ("graphicsanddesign", AppCategory::GraphicsAndDesign), ("graphicsdesign", AppCategory::GraphicsAndDesign), ("healthcareandfitness", AppCategory::HealthcareAndFitness), ("healthcarefitness", AppCategory::HealthcareAndFitness), ("kidsgame", AppCategory::KidsGame), ("kidsgames", AppCategory::KidsGame), ("lifestyle", AppCategory::Lifestyle), ("logicgame", AppCategory::PuzzleGame), ("medical", AppCategory::Medical), ("medicalsoftware", AppCategory::Medical), ("music", AppCategory::Music), ("musicgame", AppCategory::MusicGame), ("musicgames", AppCategory::MusicGame), ("news", AppCategory::News), ("photography", AppCategory::Photography), ("productivity", AppCategory::Productivity), ("puzzlegame", AppCategory::PuzzleGame), ("puzzlegames", AppCategory::PuzzleGame), ("racinggame", AppCategory::RacingGame), ("racinggames", AppCategory::RacingGame), ("reference", AppCategory::Reference), ("roleplaying", AppCategory::RolePlayingGame), ("roleplayinggame", AppCategory::RolePlayingGame), ("roleplayinggames", AppCategory::RolePlayingGame), ("rpg", AppCategory::RolePlayingGame), ("simulationgame", AppCategory::SimulationGame), ("simulationgames", AppCategory::SimulationGame), ("socialnetwork", AppCategory::SocialNetworking), ("socialnetworking", AppCategory::SocialNetworking), ("sports", AppCategory::Sports), ("sportsgame", AppCategory::SportsGame), ("sportsgames", AppCategory::SportsGame), ("strategygame", AppCategory::StrategyGame), ("strategygames", AppCategory::StrategyGame), ("travel", AppCategory::Travel), ("triviagame", AppCategory::TriviaGame), ("triviagames", AppCategory::TriviaGame), ("utilities", AppCategory::Utility), ("utility", AppCategory::Utility), ("video", AppCategory::Video), ("weather", AppCategory::Weather), ("wordgame", AppCategory::WordGame), ("wordgames", AppCategory::WordGame), ]; #[cfg(test)] mod tests { use super::AppCategory; use std::str::FromStr; #[test] fn category_from_string_ok() { // Canonical name of category works: assert_eq!( AppCategory::from_str("Education"), Ok(AppCategory::Education) ); assert_eq!( AppCategory::from_str("Developer Tool"), Ok(AppCategory::DeveloperTool) ); // Lowercase, spaces, and hyphens are fine: assert_eq!( AppCategory::from_str(" puzzle game "), Ok(AppCategory::PuzzleGame) ); assert_eq!( AppCategory::from_str("Role-playing game"), Ok(AppCategory::RolePlayingGame) ); // Using macOS LSApplicationCategoryType value is fine: assert_eq!( AppCategory::from_str("public.app-category.developer-tools"), Ok(AppCategory::DeveloperTool) ); assert_eq!( AppCategory::from_str("public.app-category.role-playing-games"), Ok(AppCategory::RolePlayingGame) ); // Using GNOME category name is fine: assert_eq!( AppCategory::from_str("Development"), Ok(AppCategory::DeveloperTool) ); assert_eq!( AppCategory::from_str("LogicGame"), Ok(AppCategory::PuzzleGame) ); // Using common abbreviations is fine: assert_eq!( AppCategory::from_str("RPG"), Ok(AppCategory::RolePlayingGame) ); } #[test] fn category_from_string_did_you_mean() { assert_eq!(AppCategory::from_str("gaming"), Err(Some("Game"))); assert_eq!(AppCategory::from_str("photos"), Err(Some("Photography"))); assert_eq!( AppCategory::from_str("strategery"), Err(Some("Strategy Game")) ); } #[test] fn category_from_string_totally_wrong() { assert_eq!(AppCategory::from_str("fhqwhgads"), Err(None)); assert_eq!(AppCategory::from_str("WHARRGARBL"), Err(None)); } #[test] fn ls_application_category_type_round_trip() { let values = &[ "public.app-category.business", "public.app-category.developer-tools", "public.app-category.education", "public.app-category.entertainment", "public.app-category.finance", "public.app-category.games", "public.app-category.action-games", "public.app-category.adventure-games", "public.app-category.arcade-games", "public.app-category.board-games", "public.app-category.card-games", "public.app-category.casino-games", "public.app-category.dice-games", "public.app-category.educational-games", "public.app-category.family-games", "public.app-category.kids-games", "public.app-category.music-games", "public.app-category.puzzle-games", "public.app-category.racing-games", "public.app-category.role-playing-games", "public.app-category.simulation-games", "public.app-category.sports-games", "public.app-category.strategy-games", "public.app-category.trivia-games", "public.app-category.word-games", "public.app-category.graphics-design", "public.app-category.healthcare-fitness", "public.app-category.lifestyle", "public.app-category.medical", "public.app-category.music", "public.app-category.news", "public.app-category.photography", "public.app-category.productivity", "public.app-category.reference", "public.app-category.social-networking", "public.app-category.sports", "public.app-category.travel", "public.app-category.utilities", "public.app-category.video", "public.app-category.weather", ]; // Test that if the user uses an LSApplicationCategoryType string as // the category string, they will get back that same string for the // macOS app bundle LSApplicationCategoryType. for &value in values.iter() { let category = AppCategory::from_str(value).expect(value); assert_eq!(category.macos_application_category_type(), value); } } } ================================================ FILE: crates/packager/src/config/mod.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! Configuration type and associated utilities. use std::{ collections::HashMap, ffi::OsString, fmt::{self, Display}, fs, path::{Path, PathBuf}, }; use relative_path::PathExt; use serde::{Deserialize, Serialize}; use crate::{util, Error}; mod builder; mod category; pub use builder::*; pub use category::AppCategory; pub use cargo_packager_utils::PackageFormat; /// **macOS-only**. Corresponds to CFBundleTypeRole #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[derive(Default)] pub enum BundleTypeRole { /// CFBundleTypeRole.Editor. Files can be read and edited. #[default] Editor, /// CFBundleTypeRole.Viewer. Files can be read. Viewer, /// CFBundleTypeRole.Shell Shell, /// CFBundleTypeRole.QLGenerator QLGenerator, /// CFBundleTypeRole.None None, } impl Display for BundleTypeRole { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Editor => write!(f, "Editor"), Self::Viewer => write!(f, "Viewer"), Self::Shell => write!(f, "Shell"), Self::QLGenerator => write!(f, "QLGenerator"), Self::None => write!(f, "None"), } } } /// A file association configuration. #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct FileAssociation { /// File extensions to associate with this app. e.g. 'png' pub extensions: Vec, /// The mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**. #[serde(alias = "mime-type", alias = "mime_type")] pub mime_type: Option, /// The association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer. pub description: Option, /// The name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext` pub name: Option, /// The app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. /// Defaults to [`BundleTypeRole::Editor`] #[serde(default)] pub role: BundleTypeRole, } impl FileAssociation { /// Creates a new [`FileAssociation`] using provided extensions. pub fn new(extensions: I) -> Self where I: IntoIterator, S: Into, { Self { extensions: extensions.into_iter().map(Into::into).collect(), mime_type: None, description: None, name: None, role: BundleTypeRole::default(), } } /// Set the extenstions to associate with this app. e.g. 'png'. pub fn extensions(mut self, extensions: I) -> Self where I: IntoIterator, S: Into, { self.extensions = extensions.into_iter().map(Into::into).collect(); self } /// Set the mime-type e.g. 'image/png' or 'text/plain'. **Linux-only**. pub fn mime_type>(mut self, mime_type: S) -> Self { self.mime_type.replace(mime_type.into()); self } /// Se the association description. **Windows-only**. It is displayed on the `Type` column on Windows Explorer. pub fn description>(mut self, description: S) -> Self { self.description.replace(description.into()); self } /// Set he name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext` pub fn name>(mut self, name: S) -> Self { self.name.replace(name.into()); self } /// Set he app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. /// Defaults to [`BundleTypeRole::Editor`] pub fn role(mut self, role: BundleTypeRole) -> Self { self.role = role; self } } /// Deep link protocol #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct DeepLinkProtocol { /// URL schemes to associate with this app without `://`. For example `my-app` pub schemes: Vec, /// The protocol name. **macOS-only** and maps to `CFBundleTypeName`. Defaults to `.` pub name: Option, /// The app's role for these schemes. **macOS-only** and maps to `CFBundleTypeRole`. #[serde(default)] pub role: BundleTypeRole, } impl DeepLinkProtocol { /// Creates a new [`DeepLinkProtocol``] using provided schemes. pub fn new(schemes: I) -> Self where I: IntoIterator, S: Into, { Self { schemes: schemes.into_iter().map(Into::into).collect(), name: None, role: BundleTypeRole::default(), } } /// Set the name. Maps to `CFBundleTypeName` on macOS. Defaults to the first item in `ext` pub fn name>(mut self, name: S) -> Self { self.name.replace(name.into()); self } /// Set he app's role with respect to the type. Maps to `CFBundleTypeRole` on macOS. /// Defaults to [`BundleTypeRole::Editor`] pub fn role(mut self, role: BundleTypeRole) -> Self { self.role = role; self } } /// The Linux Debian configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct DebianConfig { /// The list of Debian dependencies. pub depends: Option, /// Path to a custom desktop file Handlebars template. /// /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`. /// /// Default file contents: /// ```text /// [Desktop Entry] /// Categories={{categories}} /// {{#if comment}} /// Comment={{comment}} /// {{/if}} /// Exec={{exec}} {{exec_arg}} /// Icon={{icon}} /// Name={{name}} /// Terminal=false /// Type=Application /// {{#if mime_type}} /// MimeType={{mime_type}} /// {{/if}} /// ``` /// /// The `{{exec_arg}}` will be set to: /// * "%F", if at least one [Config::file_associations] was specified but no deep link protocols were given. /// * The "%F" arg means that your application can be invoked with multiple file paths. /// * "%U", if at least one [Config::deep_link_protocols] was specified. /// * The "%U" arg means that your application can be invoked with multiple URLs. /// * If both [Config::file_associations] and [Config::deep_link_protocols] were specified, /// the "%U" arg will be used, causing the file paths to be passed to your app as `file://` URLs. /// * An empty string "" (nothing) if neither are given. /// * This means that your application will never be invoked with any URLs or file paths. /// /// To specify a custom `exec_arg`, just use plaintext directly instead of `{{exec_arg}}`: /// ```text /// Exec={{exec}} %u /// ``` /// /// See more here: . #[serde(alias = "desktop-template", alias = "desktop_template")] pub desktop_template: Option, /// Define the section in Debian Control file. See : pub section: Option, /// Change the priority of the Debian Package. By default, it is set to `optional`. /// Recognized Priorities as of now are : `required`, `important`, `standard`, `optional`, `extra` pub priority: Option, /// List of custom files to add to the deb package. /// Maps a dir/file to a dir/file inside the debian package. pub files: Option>, /// Name to use for the `Package` field in the Debian Control file. /// Defaults to [`Config::product_name`] converted to kebab-case. #[serde(alias = "package-name", alias = "package_name")] pub package_name: Option, } impl DebianConfig { /// Creates a new [`DebianConfig`]. pub fn new() -> Self { Self::default() } /// Set the list of Debian dependencies directly using an iterator of strings. pub fn depends(mut self, depends: I) -> Self where I: IntoIterator, S: Into, { self.depends.replace(Dependencies::List( depends.into_iter().map(Into::into).collect(), )); self } /// Set the list of Debian dependencies indirectly via a path to a file, /// which must contain one dependency (a package name) per line. pub fn depends_path

(mut self, path: P) -> Self where P: Into, { self.depends.replace(Dependencies::Path(path.into())); self } /// Set the path to a custom desktop file Handlebars template. /// /// Available variables: `categories`, `comment` (optional), `exec`, `icon` and `name`. /// /// Default file contents: /// ```text /// [Desktop Entry] /// Categories={{categories}} /// {{#if comment}} /// Comment={{comment}} /// {{/if}} /// Exec={{exec}} {{exec_arg}} /// Icon={{icon}} /// Name={{name}} /// Terminal=false /// Type=Application /// {{#if mime_type}} /// MimeType={{mime_type}} /// {{/if}} /// ``` pub fn desktop_template>(mut self, desktop_template: P) -> Self { self.desktop_template.replace(desktop_template.into()); self } /// Define the section in Debian Control file. See : pub fn section>(mut self, section: S) -> Self { self.section.replace(section.into()); self } /// Change the priority of the Debian Package. By default, it is set to `optional`. /// Recognized Priorities as of now are : `required`, `important`, `standard`, `optional`, `extra` pub fn priority>(mut self, priority: S) -> Self { self.priority.replace(priority.into()); self } /// Set the list of custom files to add to the deb package. /// Maps a dir/file to a dir/file inside the debian package. pub fn files(mut self, files: I) -> Self where I: IntoIterator, S: Into, T: Into, { self.files.replace( files .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(), ); self } } /// A list of dependencies specified as either a list of Strings /// or as a path to a file that lists the dependencies, one per line. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum Dependencies { /// The list of dependencies provided directly as a vector of Strings. List(Vec), /// A path to the file containing the list of dependences, formatted as one per line: /// ```text /// libc6 /// libxcursor1 /// libdbus-1-3 /// libasyncns0 /// ... /// ``` Path(PathBuf), } impl Dependencies { /// Returns the dependencies as a list of Strings. pub fn to_list(&self) -> crate::Result> { match self { Self::List(v) => Ok(v.clone()), Self::Path(path) => { let trimmed_lines = fs::read_to_string(path) .map_err(|e| Error::IoWithPath(path.clone(), e))? .lines() .filter_map(|line| { let trimmed = line.trim(); if !trimmed.is_empty() { Some(trimmed.to_owned()) } else { None } }) .collect(); Ok(trimmed_lines) } } } } /// The Linux AppImage configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct AppImageConfig { /// List of libs that exist in `/usr/lib*` to be include in the final AppImage. /// The libs will be searched for, using the command /// `find -L /usr/lib* -name ` pub libs: Option>, /// List of binary paths to include in the final AppImage. /// For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open` pub bins: Option>, /// List of custom files to add to the appimage package. /// Maps a dir/file to a dir/file inside the appimage package. pub files: Option>, /// A map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) /// plugin name and its URL to be downloaded and executed while packaing the appimage. /// For example, if you want to use the /// [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, /// you'd specify `gtk` as the key and its url as the value. #[serde(alias = "linuxdeploy-plugins", alias = "linuxdeploy_plugins")] pub linuxdeploy_plugins: Option>, /// List of globs of libraries to exclude from the final AppImage. /// For example, to exclude libnss3.so, you'd specify `libnss3*` #[serde(alias = "excluded-libraries", alias = "excluded_libraries")] pub excluded_libs: Option>, } impl AppImageConfig { /// Creates a new [`DebianConfig`]. pub fn new() -> Self { Self::default() } /// Set the list of libs that exist in `/usr/lib*` to be include in the final AppImage. /// The libs will be searched for using, the command /// `find -L /usr/lib* -name ` pub fn libs(mut self, libs: I) -> Self where I: IntoIterator, S: Into, { self.libs .replace(libs.into_iter().map(Into::into).collect()); self } /// Set the list of binary paths to include in the final AppImage. /// For example, if you want `xdg-open`, you'd specify `/usr/bin/xdg-open` pub fn bins(mut self, bins: I) -> Self where I: IntoIterator, S: Into, { self.bins .replace(bins.into_iter().map(Into::into).collect()); self } /// Set the list of custom files to add to the appimage package. /// Maps a dir/file to a dir/file inside the appimage package. pub fn files(mut self, files: I) -> Self where I: IntoIterator, S: Into, T: Into, { self.files.replace( files .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(), ); self } /// Set the map of [`linuxdeploy`](https://github.com/linuxdeploy/linuxdeploy) /// plugin name and its URL to be downloaded and executed while packaing the appimage. /// For example, if you want to use the /// [`gtk`](https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh) plugin, /// you'd specify `gtk` as the key and its url as the value. pub fn linuxdeploy_plugins(mut self, linuxdeploy_plugins: I) -> Self where I: IntoIterator, S: Into, T: Into, { self.linuxdeploy_plugins.replace( linuxdeploy_plugins .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(), ); self } } /// The Linux pacman configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct PacmanConfig { /// List of custom files to add to the pacman package. /// Maps a dir/file to a dir/file inside the pacman package. pub files: Option>, /// List of softwares that must be installed for the app to build and run. /// /// See : pub depends: Option, /// Additional packages that are provided by this app. /// /// See : pub provides: Option>, /// Packages that conflict or cause problems with the app. /// All these packages and packages providing this item will need to be removed /// /// See : pub conflicts: Option>, /// Only use if this app replaces some obsolete packages. /// For example, if you rename any package. /// /// See : pub replaces: Option>, /// Source of the package to be stored at PKGBUILD. /// PKGBUILD is a bash script, so version can be referred as ${pkgver} pub source: Option>, } impl PacmanConfig { /// Creates a new [`PacmanConfig`]. pub fn new() -> Self { Self::default() } /// Set the list of custom files to add to the pacman package. /// Maps a dir/file to a dir/file inside the pacman package. pub fn files(mut self, files: I) -> Self where I: IntoIterator, S: Into, T: Into, { self.files.replace( files .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(), ); self } /// Set the list of pacman dependencies directly using an iterator of strings. pub fn depends(mut self, depends: I) -> Self where I: IntoIterator, S: Into, { self.depends.replace(Dependencies::List( depends.into_iter().map(Into::into).collect(), )); self } /// Set the list of pacman dependencies indirectly via a path to a file, /// which must contain one dependency (a package name) per line. pub fn depends_path

(mut self, path: P) -> Self where P: Into, { self.depends.replace(Dependencies::Path(path.into())); self } /// Set the list of additional packages that are provided by this app. pub fn provides(mut self, provides: I) -> Self where I: IntoIterator, S: Into, { self.provides .replace(provides.into_iter().map(Into::into).collect()); self } /// Set the list of packages that conflict with the app. pub fn conflicts(mut self, conflicts: I) -> Self where I: IntoIterator, S: Into, { self.conflicts .replace(conflicts.into_iter().map(Into::into).collect()); self } /// Set the list of obsolete packages that are replaced by this package. pub fn replaces(mut self, replaces: I) -> Self where I: IntoIterator, S: Into, { self.replaces .replace(replaces.into_iter().map(Into::into).collect()); self } /// Set the list of sources where the package will be stored. pub fn source(mut self, source: I) -> Self where I: IntoIterator, S: Into, { self.source .replace(source.into_iter().map(Into::into).collect()); self } } /// Position coordinates struct. #[derive(Default, Copy, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Position { /// X coordinate. pub x: u32, /// Y coordinate. pub y: u32, } /// Size struct. #[derive(Default, Copy, Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Size { /// Width. pub width: u32, /// Height. pub height: u32, } /// The Apple Disk Image (.dmg) configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct DmgConfig { /// Image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`. pub background: Option, /// Position of volume window on screen. pub window_position: Option, /// Size of volume window. #[serde(alias = "window-size", alias = "window_size")] pub window_size: Option, /// Position of application file on window. #[serde(alias = "app-position", alias = "app_position")] pub app_position: Option, /// Position of application folder on window. #[serde( alias = "application-folder-position", alias = "application_folder_position" )] pub app_folder_position: Option, } impl DmgConfig { /// Creates a new [`DmgConfig`]. pub fn new() -> Self { Self::default() } /// Set an image to use as the background in dmg file. Accepted formats: `png`/`jpg`/`gif`. pub fn background>(mut self, path: P) -> Self { self.background.replace(path.into()); self } /// Set the poosition of volume window on screen. pub fn window_position(mut self, position: Position) -> Self { self.window_position.replace(position); self } /// Set the size of volume window. pub fn window_size(mut self, size: Size) -> Self { self.window_size.replace(size); self } /// Set the poosition of app file on window. pub fn app_position(mut self, position: Position) -> Self { self.app_position.replace(position); self } /// Set the position of application folder on window. pub fn app_folder_position(mut self, position: Position) -> Self { self.app_folder_position.replace(position); self } } /// Notarization authentication credentials. #[derive(Clone, Debug)] pub enum MacOsNotarizationCredentials { /// Apple ID authentication. AppleId { /// Apple ID. apple_id: OsString, /// Password. password: OsString, /// Team ID. team_id: OsString, }, /// App Store Connect API key. ApiKey { /// API key issuer. issuer: OsString, /// API key ID. key_id: OsString, /// Path to the API key file. key_path: PathBuf, }, /// Keychain profile with a stored app-specific password for notarytool to use /// Passwords can be generated at https://account.apple.com when signed in with your developer account. /// The password must then be stored in your keychain for notarytool to access, /// using the following, with the appopriate Apple and Team IDs: /// `xcrun notarytool store-credentials --apple-id "name@example.com" --team-id "ABCD123456"` /// This will prompt for a keychain profile name, and the password itself. /// This setting can only be provided as an environment variable "APPLE_KEYCHAIN_PROFILE" KeychainProfile { /// The keychain profile name (as provided when the password was stored using notarytool) keychain_profile: OsString, }, } /// The macOS configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct MacOsConfig { /// MacOS frameworks that need to be packaged with the app. /// /// Each string can either be the name of a framework (without the `.framework` extension, e.g. `"SDL2"`), /// in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), /// or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle /// (under `Foobar.app/Contents/Frameworks/`); you are still responsible for: /// /// - arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script) /// /// - embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath "@executable_path/../Frameworks" path/to/binary` after compiling) pub frameworks: Option>, /// A version string indicating the minimum MacOS version that the packaged app supports (e.g. `"10.11"`). /// If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`. #[serde(alias = "minimum-system-version", alias = "minimum_system_version")] pub minimum_system_version: Option, /// The exception domain to use on the macOS .app package. /// /// This allows communication to the outside world e.g. a web server you're shipping. #[serde(alias = "exception-domain", alias = "exception_domain")] pub exception_domain: Option, /// Code signing identity. /// /// This is typically of the form: `"Developer ID Application: TEAM_NAME (TEAM_ID)"`. #[serde(alias = "signing-identity", alias = "signing_identity")] pub signing_identity: Option, /// Codesign certificate (base64 encoded of the p12 file). /// /// Note: this field cannot be specified via a config file or Cargo package metadata. #[serde(skip)] pub signing_certificate: Option, /// Password of the codesign certificate. /// /// Note: this field cannot be specified via a config file or Cargo package metadata. #[serde(skip)] pub signing_certificate_password: Option, /// Notarization authentication credentials. /// /// Note: this field cannot be specified via a config file or Cargo package metadata. #[serde(skip)] pub notarization_credentials: Option, /// Provider short name for notarization. #[serde(alias = "provider-short-name", alias = "provider_short_name")] pub provider_short_name: Option, /// Path to the entitlements.plist file. pub entitlements: Option, /// Path to the Info.plist file for the package. #[serde(alias = "info-plist-path", alias = "info_plist_path")] pub info_plist_path: Option, /// Path to the embedded.provisionprofile file for the package. #[serde( alias = "embedded-provisionprofile-path", alias = "embedded_provisionprofile_path" )] pub embedded_provisionprofile_path: Option, /// Apps that need to be packaged within the app. #[serde(alias = "embedded-apps", alias = "embedded_apps")] pub embedded_apps: Option>, /// Whether this is a background application. If true, the app will not appear in the Dock. /// /// Sets the `LSUIElement` flag in the macOS plist file. #[serde(default, alias = "background_app", alias = "background-app")] pub background_app: bool, } impl MacOsConfig { /// Creates a new [`MacOsConfig`]. pub fn new() -> Self { Self::default() } /// MacOS frameworks that need to be packaged with the app. /// /// Each string can either be the name of a framework (without the `.framework` extension, e.g. `"SDL2"`), /// in which case we will search for that framework in the standard install locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and `/Network/Library/Frameworks/`), /// or a path to a specific framework bundle (e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes cargo-packager copy the specified frameworks into the OS X app bundle /// (under `Foobar.app/Contents/Frameworks/`); you are still responsible for: /// /// - arranging for the compiled binary to link against those frameworks (e.g. by emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your `build.rs` script) /// /// - embedding the correct rpath in your binary (e.g. by running `install_name_tool -add_rpath "@executable_path/../Frameworks" path/to/binary` after compiling) pub fn frameworks(mut self, frameworks: I) -> Self where I: IntoIterator, S: Into, { self.frameworks .replace(frameworks.into_iter().map(Into::into).collect()); self } /// A version string indicating the minimum MacOS version that the packaged app supports (e.g. `"10.11"`). /// If you are using this config field, you may also want have your `build.rs` script emit `cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11`. pub fn minimum_system_version>(mut self, minimum_system_version: S) -> Self { self.minimum_system_version .replace(minimum_system_version.into()); self } /// The exception domain to use on the macOS .app package. /// /// This allows communication to the outside world e.g. a web server you're shipping. pub fn exception_domain>(mut self, exception_domain: S) -> Self { self.exception_domain.replace(exception_domain.into()); self } /// Code signing identity. pub fn signing_identity>(mut self, signing_identity: S) -> Self { self.signing_identity.replace(signing_identity.into()); self } /// Provider short name for notarization. pub fn provider_short_name>(mut self, provider_short_name: S) -> Self { self.provider_short_name.replace(provider_short_name.into()); self } /// Path to the entitlements.plist file. pub fn entitlements>(mut self, entitlements: S) -> Self { self.entitlements.replace(entitlements.into()); self } /// Path to the Info.plist file for the package. pub fn info_plist_path>(mut self, info_plist_path: S) -> Self { self.info_plist_path.replace(info_plist_path.into()); self } /// Path to the embedded.provisionprofile file for the package. pub fn embedded_provisionprofile_path>( mut self, embedded_provisionprofile_path: S, ) -> Self { self.embedded_provisionprofile_path .replace(embedded_provisionprofile_path.into()); self } /// Apps that need to be packaged within the app. pub fn embedded_apps(mut self, embedded_apps: I) -> Self where I: IntoIterator, S: Into, { self.embedded_apps .replace(embedded_apps.into_iter().map(Into::into).collect()); self } } /// Linux configuration #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct LinuxConfig { /// Flag to indicate if desktop entry should be generated. #[serde( default = "default_true", alias = "generate-desktop-entry", alias = "generate_desktop_entry" )] pub generate_desktop_entry: bool, } /// A wix language. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum WixLanguage { /// Built-in wix language identifier. Identifier(String), /// Custom wix language. Custom { /// Idenitifier of this language, for example `en-US` identifier: String, /// The path to a locale (`.wxl`) file. See . path: Option, }, } impl Default for WixLanguage { fn default() -> Self { Self::Identifier("en-US".into()) } } /// The wix format configuration #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct WixConfig { /// The app languages to build. See . pub languages: Option>, /// By default, the packager uses an internal template. /// This option allows you to define your own wix file. pub template: Option, /// List of merge modules to include in your installer. /// For example, if you want to include [C++ Redis merge modules] /// /// [C++ Redis merge modules]: https://wixtoolset.org/docs/v3/howtos/redistributables_and_install_checks/install_vcredist/ #[serde(alias = "merge-modules", alias = "merge_modules")] pub merge_modules: Option>, /// A list of paths to .wxs files with WiX fragments to use. #[serde(alias = "fragment-paths", alias = "fragment_paths")] pub fragment_paths: Option>, /// List of WiX fragments as strings. This is similar to `config.wix.fragments_paths` but /// is a string so you can define it inline in your config. /// /// ```text /// /// /// /// /// /// /// /// /// /// ``` pub fragments: Option>, /// The ComponentGroup element ids you want to reference from the fragments. #[serde(alias = "component-group-refs", alias = "component_group_refs")] pub component_group_refs: Option>, /// The Component element ids you want to reference from the fragments. #[serde(alias = "component-refs", alias = "component_refs")] pub component_refs: Option>, /// The CustomAction element ids you want to reference from the fragments. #[serde(alias = "custom-action-refs", alias = "custom_action_refs")] pub custom_action_refs: Option>, /// The FeatureGroup element ids you want to reference from the fragments. #[serde(alias = "feature-group-refs", alias = "feature_group_refs")] pub feature_group_refs: Option>, /// The Feature element ids you want to reference from the fragments. #[serde(alias = "feature-refs", alias = "feature_refs")] pub feature_refs: Option>, /// The Merge element ids you want to reference from the fragments. #[serde(alias = "merge-refs", alias = "merge_refs")] pub merge_refs: Option>, /// Path to a bitmap file to use as the installation user interface banner. /// This bitmap will appear at the top of all but the first page of the installer. /// /// The required dimensions are 493px × 58px. #[serde(alias = "banner-path", alias = "banner_path")] pub banner_path: Option, /// Path to a bitmap file to use on the installation user interface dialogs. /// It is used on the welcome and completion dialogs. /// The required dimensions are 493px × 312px. #[serde(alias = "dialog-image-path", alias = "dialog_image_path")] pub dialog_image_path: Option, /// Enables FIPS compliant algorithms. #[serde(default, alias = "fips-compliant", alias = "fips_compliant")] pub fips_compliant: bool, } impl WixConfig { /// Creates a new [`WixConfig`]. pub fn new() -> Self { Self::default() } /// Set the app languages to build. See . pub fn languages>(mut self, languages: I) -> Self { self.languages.replace(languages.into_iter().collect()); self } /// By default, the packager uses an internal template. /// This option allows you to define your own wix file. pub fn template>(mut self, template: P) -> Self { self.template.replace(template.into()); self } /// Set a list of merge modules to include in your installer. /// For example, if you want to include [C++ Redis merge modules] /// /// [C++ Redis merge modules]: https://wixtoolset.org/docs/v3/howtos/redistributables_and_install_checks/install_vcredist/ pub fn merge_modules(mut self, merge_modules: I) -> Self where I: IntoIterator, P: Into, { self.merge_modules .replace(merge_modules.into_iter().map(Into::into).collect()); self } /// Set a list of paths to .wxs files with WiX fragments to use. pub fn fragment_paths(mut self, fragment_paths: I) -> Self where I: IntoIterator, S: Into, { self.fragment_paths .replace(fragment_paths.into_iter().map(Into::into).collect()); self } /// Set a list of WiX fragments as strings. This is similar to [`WixConfig::fragment_paths`] but /// is a string so you can define it inline in your config. /// /// ```text /// /// /// /// /// /// /// /// /// /// ``` pub fn fragments(mut self, fragments: I) -> Self where I: IntoIterator, S: Into, { self.fragments .replace(fragments.into_iter().map(Into::into).collect()); self } /// Set the ComponentGroup element ids you want to reference from the fragments. pub fn component_group_refs(mut self, component_group_refs: I) -> Self where I: IntoIterator, S: Into, { self.component_group_refs .replace(component_group_refs.into_iter().map(Into::into).collect()); self } /// Set the Component element ids you want to reference from the fragments. pub fn component_refs(mut self, component_refs: I) -> Self where I: IntoIterator, S: Into, { self.component_refs .replace(component_refs.into_iter().map(Into::into).collect()); self } /// Set the CustomAction element ids you want to reference from the fragments. pub fn custom_action_refs(mut self, custom_action_refs: I) -> Self where I: IntoIterator, S: Into, { self.custom_action_refs .replace(custom_action_refs.into_iter().map(Into::into).collect()); self } /// Set he FeatureGroup element ids you want to reference from the fragments. pub fn feature_group_refs(mut self, feature_group_refs: I) -> Self where I: IntoIterator, S: Into, { self.feature_group_refs .replace(feature_group_refs.into_iter().map(Into::into).collect()); self } /// Set the Feature element ids you want to reference from the fragments. pub fn feature_refs(mut self, feature_refs: I) -> Self where I: IntoIterator, S: Into, { self.feature_refs .replace(feature_refs.into_iter().map(Into::into).collect()); self } /// Set he Merge element ids you want to reference from the fragments. pub fn merge_refs(mut self, merge_refs: I) -> Self where I: IntoIterator, S: Into, { self.merge_refs .replace(merge_refs.into_iter().map(Into::into).collect()); self } /// Set the path to a bitmap file to use as the installation user interface banner. /// This bitmap will appear at the top of all but the first page of the installer. /// /// The required dimensions are 493px × 58px. pub fn banner_path>(mut self, path: P) -> Self { self.banner_path.replace(path.into()); self } /// Set the path to a bitmap file to use on the installation user interface dialogs. /// It is used on the welcome and completion dialogs. /// The required dimensions are 493px × 312px. pub fn dialog_image_path>(mut self, path: P) -> Self { self.dialog_image_path.replace(path.into()); self } /// Set whether to enable or disable FIPS compliant algorithms. pub fn fips_compliant(mut self, fips_compliant: bool) -> Self { self.fips_compliant = fips_compliant; self } } /// Install Modes for the NSIS installer. #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] #[derive(Default)] pub enum NSISInstallerMode { /// Default mode for the installer. /// /// Install the app by default in a directory that doesn't require Administrator access. /// /// Installer metadata will be saved under the `HKCU` registry path. #[default] CurrentUser, /// Install the app by default in the `Program Files` folder directory requires Administrator /// access for the installation. /// /// Installer metadata will be saved under the `HKLM` registry path. PerMachine, /// Combines both modes and allows the user to choose at install time /// whether to install for the current user or per machine. Note that this mode /// will require Administrator access even if the user wants to install it for the current user only. /// /// Installer metadata will be saved under the `HKLM` or `HKCU` registry path based on the user's choice. Both, } /// Compression algorithms used in the NSIS installer. /// /// See #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub enum NsisCompression { /// ZLIB uses the deflate algorithm, it is a quick and simple method. With the default compression level it uses about 300 KB of memory. Zlib, /// BZIP2 usually gives better compression ratios than ZLIB, but it is a bit slower and uses more memory. With the default compression level it uses about 4 MB of memory. Bzip2, /// LZMA (default) is a new compression method that gives very good compression ratios. The decompression speed is high (10-20 MB/s on a 2 GHz CPU), the compression speed is lower. The memory size that will be used for decompression is the dictionary size plus a few KBs, the default is 8 MB. Lzma, /// Disable compression. Off, } /// The NSIS format configuration. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct NsisConfig { /// Set the compression algorithm used to compress files in the installer. /// /// See pub compression: Option, /// A custom `.nsi` template to use. /// /// See the default template here /// pub template: Option, /// Logic of an NSIS section that will be ran before the install section. /// /// See the available libraries, dlls and global variables here /// /// /// ### Example /// ```toml /// [package.metadata.packager.nsis] /// preinstall-section = """ /// ; Setup custom messages /// LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." /// LangString webview2DownloadError ${LANG_ARABIC} "خطأ: فشل تنزيل WebView2 - $0" /// /// Section PreInstall /// ;

/// SectionEnd /// /// Section AnotherPreInstall /// ;
/// SectionEnd /// """ /// ``` #[serde(alias = "preinstall-section", alias = "preinstall_section")] pub preinstall_section: Option, /// The path to a bitmap file to display on the header of installers pages. /// /// The recommended dimensions are 150px x 57px. #[serde(alias = "header-image", alias = "header_image")] pub header_image: Option, /// The path to a bitmap file for the Welcome page and the Finish page. /// /// The recommended dimensions are 164px x 314px. #[serde(alias = "sidebar-image", alias = "sidebar_image")] pub sidebar_image: Option, /// The path to an icon file used as the installer icon. #[serde(alias = "installer-icon", alias = "installer_icon")] pub installer_icon: Option, /// Whether the installation will be for all users or just the current user. #[serde(default, alias = "installer-mode", alias = "installer_mode")] pub install_mode: NSISInstallerMode, /// A list of installer languages. /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. /// To allow the user to select the language, set `display_language_selector` to `true`. /// /// See for the complete list of languages. pub languages: Option>, /// An key-value pair where the key is the language and the /// value is the path to a custom `.nsi` file that holds the translated text for cargo-packager's custom messages. /// /// See for an example `.nsi` file. /// /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array, #[serde(alias = "custom-language-file", alias = "custom_language_file")] pub custom_language_files: Option>, /// Whether to display a language selector dialog before the installer and uninstaller windows are rendered or not. /// By default the OS language is selected, with a fallback to the first language in the `languages` array. #[serde( default, alias = "display-language-selector", alias = "display_language_selector" )] pub display_language_selector: bool, /// List of paths where your app stores data. /// This options tells the uninstaller to provide the user with an option /// (disabled by default) whether they want to rmeove your app data or keep it. /// /// The path should use a constant from /// in addition to `$IDENTIFIER`, `$PUBLISHER` and `$PRODUCTNAME`, for example, if you store your /// app data in `C:\\Users\\\\AppData\\Local\\\\` /// you'd need to specify /// ```toml /// [package.metadata.packager.nsis] /// appdata-paths = ["$LOCALAPPDATA/$PUBLISHER/$PRODUCTNAME"] /// ``` #[serde(default, alias = "appdata-paths", alias = "appdata_paths")] pub appdata_paths: Option>, } impl NsisConfig { /// Creates a new [`NsisConfig`]. pub fn new() -> Self { Self::default() } /// Set the compression algorithm used to compress files in the installer. /// /// See pub fn compression(mut self, compression: NsisCompression) -> Self { self.compression.replace(compression); self } /// Set a custom `.nsi` template to use. /// /// See the default template here /// pub fn template>(mut self, template: P) -> Self { self.template.replace(template.into()); self } /// Set the logic of an NSIS section that will be ran before the install section. /// /// See the available libraries, dlls and global variables here /// /// /// ### Example /// ```toml /// [package.metadata.packager.nsis] /// preinstall-section = """ /// ; Setup custom messages /// LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." /// LangString webview2DownloadError ${LANG_ARABIC} "خطأ: فشل تنزيل WebView2 - $0" /// /// Section PreInstall /// ;
/// SectionEnd /// /// Section AnotherPreInstall /// ;
/// SectionEnd /// """ /// ``` pub fn preinstall_section>(mut self, preinstall_section: S) -> Self { self.preinstall_section.replace(preinstall_section.into()); self } /// Set the path to a bitmap file to display on the header of installers pages. /// /// The recommended dimensions are 150px x 57px. pub fn header_image>(mut self, header_image: P) -> Self { self.header_image.replace(header_image.into()); self } /// Set the path to a bitmap file for the Welcome page and the Finish page. /// /// The recommended dimensions are 164px x 314px. pub fn sidebar_image>(mut self, sidebar_image: P) -> Self { self.sidebar_image.replace(sidebar_image.into()); self } /// Set the path to an icon file used as the installer icon. pub fn installer_icon>(mut self, installer_icon: P) -> Self { self.installer_icon.replace(installer_icon.into()); self } /// Set whether the installation will be for all users or just the current user. pub fn install_mode(mut self, install_mode: NSISInstallerMode) -> Self { self.install_mode = install_mode; self } /// Set a list of installer languages. /// By default the OS language is used. If the OS language is not in the list of languages, the first language will be used. /// To allow the user to select the language, set `display_language_selector` to `true`. /// /// See for the complete list of languages. pub fn languages(mut self, languages: I) -> Self where I: IntoIterator, S: Into, { self.languages .replace(languages.into_iter().map(Into::into).collect()); self } /// Set a map of key-value pair where the key is the language and the /// value is the path to a custom `.nsi` file that holds the translated text for cargo-packager's custom messages. /// /// See for an example `.nsi` file. /// /// **Note**: the key must be a valid NSIS language and it must be added to [`NsisConfig`]languages array, pub fn custom_language_files(mut self, custom_language_files: I) -> Self where I: IntoIterator, S: Into, P: Into, { self.custom_language_files.replace( custom_language_files .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(), ); self } /// Set wether to display a language selector dialog before the installer and uninstaller windows are rendered or not. /// By default the OS language is selected, with a fallback to the first language in the `languages` array. pub fn display_language_selector(mut self, display: bool) -> Self { self.display_language_selector = display; self } /// Set a list of paths where your app stores data. /// This options tells the uninstaller to provide the user with an option /// (disabled by default) whether they want to rmeove your app data or keep it. /// /// The path should use a constant from /// in addition to `$IDENTIFIER`, `$PUBLISHER` and `$PRODUCTNAME`, for example, if you store your /// app data in `C:\\Users\\\\AppData\\Local\\\\` /// you'd need to specify /// ```toml /// [package.metadata.packager.nsis] /// appdata-paths = ["$LOCALAPPDATA/$PUBLISHER/$PRODUCTNAME"] /// ``` pub fn appdata_paths(mut self, appdata_paths: I) -> Self where I: IntoIterator, S: Into, { self.appdata_paths .replace(appdata_paths.into_iter().map(Into::into).collect()); self } } /// The Windows configuration. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct WindowsConfig { /// The file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended. #[serde(alias = "digest-algorithm", alias = "digest_algorithm")] pub digest_algorithm: Option, /// The SHA1 hash of the signing certificate. #[serde(alias = "certificate-thumbprint", alias = "certificate_thumbprint")] pub certificate_thumbprint: Option, /// Whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may /// use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true. #[serde(default)] pub tsp: bool, /// Server to use during timestamping. #[serde(alias = "timestamp-url", alias = "timestamp_url")] pub timestamp_url: Option, /// Whether to validate a second app installation, blocking the user from installing an older version if set to `false`. /// /// For instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`. /// /// The default value of this flag is `true`. #[serde( default = "default_true", alias = "allow-downgrades", alias = "allow_downgrades" )] pub allow_downgrades: bool, /// Specify a custom command to sign the binaries. /// This command needs to have a `%1` in it which is just a placeholder for the binary path, /// which we will detect and replace before calling the command. /// /// By Default we use `signtool.exe` which can be found only on Windows so /// if you are on another platform and want to cross-compile and sign you will /// need to use another tool like `osslsigncode`. #[serde(alias = "sign-command", alias = "sign_command")] pub sign_command: Option, } impl Default for WindowsConfig { fn default() -> Self { Self { digest_algorithm: None, certificate_thumbprint: None, timestamp_url: None, tsp: false, allow_downgrades: true, sign_command: None, } } } impl WindowsConfig { /// Creates a new [`WindowsConfig`]. pub fn new() -> Self { Self::default() } /// Set the file digest algorithm to use for creating file signatures. Required for code signing. SHA-256 is recommended. pub fn digest_algorithm>(mut self, digest_algorithm: S) -> Self { self.digest_algorithm.replace(digest_algorithm.into()); self } /// Set the SHA1 hash of the signing certificate. pub fn certificate_thumbprint>(mut self, certificate_thumbprint: S) -> Self { self.certificate_thumbprint .replace(certificate_thumbprint.into()); self } /// Set whether to use Time-Stamp Protocol (TSP, a.k.a. RFC 3161) for the timestamp server. Your code signing provider may /// use a TSP timestamp server, like e.g. SSL.com does. If so, enable TSP by setting to true. pub fn tsp(mut self, tsp: bool) -> Self { self.tsp = tsp; self } /// Set server url to use during timestamping. pub fn timestamp_url>(mut self, timestamp_url: S) -> Self { self.timestamp_url.replace(timestamp_url.into()); self } /// Set whether to validate a second app installation, blocking the user from installing an older version if set to `false`. /// /// For instance, if `1.2.1` is installed, the user won't be able to install app version `1.2.0` or `1.1.5`. /// /// The default value of this flag is `true`. pub fn allow_downgrades(mut self, allow: bool) -> Self { self.allow_downgrades = allow; self } } /// An enum representing the available verbosity levels of the logger. #[derive(Deserialize, Serialize)] #[repr(usize)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[derive(Default)] pub enum LogLevel { /// The "error" level. /// /// Designates very serious errors. #[default] Error = 1, /// The "warn" level. /// /// Designates hazardous situations. Warn, /// The "info" level. /// /// Designates useful information. Info, /// The "debug" level. /// /// Designates lower priority information. Debug, /// The "trace" level. /// /// Designates very low priority, often extremely verbose, information. Trace, } /// A binary to package within the final package. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct Binary { /// Path to the binary (without `.exe` on Windows). /// If it's relative, it will be resolved from [`Config::out_dir`]. pub path: PathBuf, /// Whether this is the main binary or not #[serde(default)] pub main: bool, } impl Binary { /// Creates a new [`Binary`] from a path to the binary (without `.exe` on Windows). /// If it's relative, it will be resolved from [`Config::out_dir`]. pub fn new>(path: P) -> Self { Self { path: path.into(), main: false, } } /// Set the path of the binary. pub fn path>(mut self, path: P) -> Self { self.path = path.into(); self } /// Set the binary as main binary. pub fn main(mut self, main: bool) -> Self { self.main = main; self } } /// A path to a resource (with optional glob pattern) /// or an object of `src` and `target` paths. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum Resource { /// Supports glob patterns Single(String), /// An object descriping the src file or directory /// and its target location in the final package. Mapped { /// The src file or directory, supports glob patterns. src: String, /// A relative path from the root of the final package. /// /// If `src` is a glob, this will always be treated as a directory /// where all globbed files will be placed under. target: PathBuf, }, } /// Describes a shell command to be executed when a CLI hook is triggered. #[derive(Debug, Clone, Deserialize, Serialize)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(untagged)] #[non_exhaustive] pub enum HookCommand { /// Run the given script with the default options. Script(String), /// Run the given script with custom options. ScriptWithOptions { /// The script to execute. script: String, /// The working directory. dir: Option, }, } /// The packaging config. #[derive(Deserialize, Serialize, Default, Debug, Clone)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Config { /// The JSON schema for the config. /// /// Setting this field has no effect, this just exists so /// we can parse the JSON correctly when it has `$schema` field set. #[serde(rename = "$schema")] schema: Option, /// The app name, this is just an identifier that could be used /// to filter which app to package using `--packages` cli arg when there is multiple apps in the /// workspace or in the same config. /// /// This field resembles, the `name` field in `Cargo.toml` or `package.json` /// /// If `unset`, the CLI will try to auto-detect it from `Cargo.toml` or /// `package.json` otherwise, it will keep it unset. pub(crate) name: Option, /// Whether this config is enabled or not. Defaults to `true`. #[serde(default = "default_true")] pub(crate) enabled: bool, /// The package's product name, for example "My Awesome App". #[serde(default, alias = "product-name", alias = "product_name")] pub product_name: String, /// The package's version. #[serde(default)] pub version: String, /// The binaries to package. #[serde(default)] pub binaries: Vec, /// The application identifier in reverse domain name notation (e.g. `com.packager.example`). /// This string must be unique across applications since it is used in some system configurations. /// This string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), /// and periods (.). #[cfg_attr(feature = "schema", schemars(regex(pattern = r"^[a-zA-Z0-9-\.]*$")))] pub identifier: Option, /// The command to run before starting to package an application. /// /// This runs only once. #[serde(alias = "before-packaging-command", alias = "before_packaging_command")] pub before_packaging_command: Option, /// The command to run before packaging each format for an application. /// /// This will run multiple times depending on the formats specifed. #[serde( alias = "before-each-package-command", alias = "before_each_package_command" )] pub before_each_package_command: Option, /// The logging level. #[serde(alias = "log-level", alias = "log_level")] pub log_level: Option, /// The packaging formats to create, if not present, [`PackageFormat::platform_default`] is used. pub formats: Option>, /// The directory where the generated packages will be placed. /// /// If [`Config::binaries_dir`] is not set, this is also where the [`Config::binaries`] exist. #[serde(default, alias = "out-dir", alias = "out_dir")] pub out_dir: PathBuf, /// The directory where the [`Config::binaries`] exist. /// /// Defaults to [`Config::out_dir`]. #[serde(default, alias = "binaries-dir", alias = "binaries_dir")] pub binaries_dir: Option, /// The target triple we are packaging for. /// /// Defaults to the current OS target triple. #[serde(alias = "target-triple", alias = "target_triple")] pub target_triple: Option, /// The package's description. pub description: Option, /// The app's long description. #[serde(alias = "long-description", alias = "long_description")] pub long_description: Option, /// The package's homepage. pub homepage: Option, /// The package's authors. #[serde(default)] pub authors: Option>, /// The app's publisher. Defaults to the second element in [`Config::identifier`](Config::identifier) string. /// Currently maps to the Manufacturer property of the Windows Installer. pub publisher: Option, /// A path to the license file. #[serde(alias = "license-file", alias = "license_file")] pub license_file: Option, /// The app's copyright. pub copyright: Option, /// The app's category. pub category: Option, /// The app's icon list. Supports glob patterns. pub icons: Option>, /// The file associations #[serde(alias = "file-associations", alias = "file_associations")] pub file_associations: Option>, /// Deep-link protocols. #[serde(alias = "deep-link-protocols", alias = "deep_link_protocols")] pub deep_link_protocols: Option>, /// The app's resources to package. This a list of either a glob pattern, path to a file, path to a directory /// or an object of `src` and `target` paths. In the case of using an object, /// the `src` could be either a glob pattern, path to a file, path to a directory, /// and the `target` is a path inside the final resources folder in the installed package. /// /// ## Format-specific: /// /// - **[PackageFormat::Nsis] / [PackageFormat::Wix]**: The resources are placed next to the executable in the root of the packager. /// - **[PackageFormat::Deb]**: The resources are placed in `usr/lib` of the package. pub resources: Option>, /// Paths to external binaries to add to the package. /// /// The path specified should not include `-<.exe>` suffix, /// it will be auto-added when by the packager when reading these paths, /// so the actual binary name should have the target platform's target triple appended, /// as well as `.exe` for Windows. /// /// For example, if you're packaging an external binary called `sqlite3`, the packager expects /// a binary named `sqlite3-x86_64-unknown-linux-gnu` on linux, /// and `sqlite3-x86_64-pc-windows-gnu.exe` on windows. /// /// If you are building a universal binary for MacOS, the packager expects /// your external binary to also be universal, and named after the target triple, /// e.g. `sqlite3-universal-apple-darwin`. See /// #[serde(alias = "external-binaries", alias = "external_binaries")] pub external_binaries: Option>, /// Windows-specific configuration. pub windows: Option, /// MacOS-specific configuration. pub macos: Option, /// Linux-specific configuration pub linux: Option, /// Debian-specific configuration. pub deb: Option, /// AppImage configuration. pub appimage: Option, /// Pacman configuration. pub pacman: Option, /// WiX configuration. pub wix: Option, /// Nsis configuration. pub nsis: Option, /// Dmg configuration. pub dmg: Option, } impl Config { /// Creates a new [`ConfigBuilder`]. pub fn builder() -> ConfigBuilder { ConfigBuilder::default() } /// Returns the [windows](Config::windows) specific configuration. pub fn windows(&self) -> Option<&WindowsConfig> { self.windows.as_ref() } /// Returns the [macos](Config::macos) specific configuration. pub fn macos(&self) -> Option<&MacOsConfig> { self.macos.as_ref() } /// Returns the [linux](Config::linux) specific configuration. pub fn linux(&self) -> Option<&LinuxConfig> { self.linux.as_ref() } /// Returns the [nsis](Config::nsis) specific configuration. pub fn nsis(&self) -> Option<&NsisConfig> { self.nsis.as_ref() } /// Returns the [wix](Config::wix) specific configuration. pub fn wix(&self) -> Option<&WixConfig> { self.wix.as_ref() } /// Returns the [debian](Config::deb) specific configuration. pub fn deb(&self) -> Option<&DebianConfig> { self.deb.as_ref() } /// Returns the [appimage](Config::appimage) specific configuration. pub fn appimage(&self) -> Option<&AppImageConfig> { self.appimage.as_ref() } /// Returns the [pacman](Config::pacman) specific configuration. pub fn pacman(&self) -> Option<&PacmanConfig> { self.pacman.as_ref() } /// Returns the [dmg](Config::dmg) specific configuration. pub fn dmg(&self) -> Option<&DmgConfig> { self.dmg.as_ref() } /// Returns the target triple of this config, if not set, fallsback to the current OS target triple. pub fn target_triple(&self) -> String { self.target_triple.clone().unwrap_or_else(|| { util::target_triple().expect("Failed to detect current target triple") }) } /// Returns the architecture for the package to be built (e.g. "arm", "x86" or "x86_64"). pub fn target_arch(&self) -> crate::Result<&str> { let target = self.target_triple(); Ok(if target.starts_with("x86_64") { "x86_64" } else if target.starts_with('i') { "x86" } else if target.starts_with("arm") { "arm" } else if target.starts_with("aarch64") { "aarch64" } else if target.starts_with("universal") { "universal" } else { return Err(crate::Error::UnexpectedTargetTriple(target)); }) } /// Returns the path to the specified binary. pub fn binary_path(&self, binary: &Binary) -> PathBuf { if binary.path.is_absolute() { binary.path.clone() } else { self.binaries_dir().join(&binary.path) } } /// Returns the package identifier. Defaults an empty string. pub fn identifier(&self) -> &str { self.identifier.as_deref().unwrap_or("") } /// Returns the package publisher. /// Defaults to the second element in [`Config::identifier`](Config::identifier()). pub fn publisher(&self) -> String { self.publisher.clone().unwrap_or_else(|| { self.identifier() .split('.') .nth(1) .unwrap_or(self.identifier()) .into() }) } /// Returns the out dir. Defaults to the current directory. pub fn out_dir(&self) -> PathBuf { if self.out_dir.as_os_str().is_empty() { return std::env::current_dir().expect("failed to resolve cwd"); } if !self.out_dir.exists() { fs::create_dir_all(&self.out_dir).expect("failed to create output directory"); } dunce::canonicalize(&self.out_dir).unwrap_or_else(|_| self.out_dir.clone()) } /// Returns the binaries dir. Defaults to [`Self::out_dir`] if [`Self::binaries_dir`] is not set. pub fn binaries_dir(&self) -> PathBuf { if let Some(path) = &self.binaries_dir { dunce::canonicalize(path).unwrap_or_else(|_| path.clone()) } else { self.out_dir() } } /// Returns the main binary. pub fn main_binary(&self) -> crate::Result<&Binary> { self.binaries .iter() .find(|bin| bin.main) .ok_or_else(|| crate::Error::MainBinaryNotFound) } /// Returns a mutable reference to the main binary. pub fn main_binary_mut(&mut self) -> crate::Result<&mut Binary> { self.binaries .iter_mut() .find(|bin| bin.main) .ok_or_else(|| crate::Error::MainBinaryNotFound) } /// Returns the main binary name. pub fn main_binary_name(&self) -> crate::Result { self.binaries .iter() .find(|bin| bin.main) .map(|b| b.path.file_stem().unwrap().to_string_lossy().into_owned()) .ok_or_else(|| crate::Error::MainBinaryNotFound) } /// Returns all icons path. pub fn icons(&self) -> crate::Result>> { let Some(patterns) = &self.icons else { return Ok(None); }; let mut paths = Vec::new(); for pattern in patterns { for icon_path in glob::glob(pattern)? { paths.push(icon_path?); } } Ok(Some(paths)) } } #[derive(Debug, Clone)] pub(crate) struct ResolvedResource { pub src: PathBuf, pub target: PathBuf, } impl Config { #[inline] pub(crate) fn resources_from_dir( src_dir: &Path, target_dir: &Path, ) -> crate::Result> { let mut out = Vec::new(); for entry in walkdir::WalkDir::new(src_dir) { let entry = entry?; let path = entry.path(); if path.is_file() { let relative = path.relative_to(src_dir)?.to_path(""); let src = dunce::canonicalize(path) .map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; let resource = ResolvedResource { src, target: target_dir.join(relative), }; out.push(resource); } } Ok(out) } #[inline] pub(crate) fn resources_from_glob(glob: &str) -> crate::Result> { let mut out = Vec::new(); for src in glob::glob(glob)? { let src = src?; let src = dunce::canonicalize(&src).map_err(|e| Error::IoWithPath(src, e))?; let target = PathBuf::from(src.file_name().unwrap_or_default()); out.push(ResolvedResource { src, target }) } Ok(out) } pub(crate) fn resources(&self) -> crate::Result> { if let Some(resources) = &self.resources { let mut out = Vec::new(); for r in resources { match r { Resource::Single(src) => { let src_dir = PathBuf::from(src); if src_dir.is_dir() { let target_dir = Path::new(src_dir.file_name().unwrap_or_default()); out.extend(Self::resources_from_dir(&src_dir, target_dir)?); } else { out.extend(Self::resources_from_glob(src)?); } } Resource::Mapped { src, target } => { let src_path = PathBuf::from(src); let target_dir = sanitize_path(target); if src_path.is_dir() { out.extend(Self::resources_from_dir(&src_path, &target_dir)?); } else if src_path.is_file() { let src = dunce::canonicalize(&src_path) .map_err(|e| Error::IoWithPath(src_path, e))?; out.push(ResolvedResource { src, target: sanitize_path(target), }); } else { let globbed_res = Self::resources_from_glob(src)?; let retargetd_res = globbed_res.into_iter().map(|mut r| { r.target = target_dir.join(r.target); r }); out.extend(retargetd_res); } } } } Ok(out) } else { Ok(vec![]) } } #[allow(unused)] pub(crate) fn find_ico(&self) -> crate::Result> { let icon = self .icons()? .as_ref() .and_then(|icons| { icons .iter() .find(|i| PathBuf::from(i).extension().and_then(|s| s.to_str()) == Some("ico")) .or_else(|| { icons.iter().find(|i| { PathBuf::from(i).extension().and_then(|s| s.to_str()) == Some("png") }) }) }) .map(PathBuf::from); Ok(icon) } #[allow(unused)] pub(crate) fn copy_resources(&self, path: &Path) -> crate::Result<()> { for resource in self.resources()? { let dest = path.join(resource.target); fs::create_dir_all( dest.parent() .ok_or_else(|| crate::Error::ParentDirNotFound(dest.to_path_buf()))?, )?; fs::copy(&resource.src, &dest) .map_err(|e| Error::CopyFile(resource.src.clone(), dest.clone(), e))?; } Ok(()) } #[allow(unused)] pub(crate) fn copy_external_binaries(&self, path: &Path) -> crate::Result> { let mut paths = Vec::new(); if let Some(external_binaries) = &self.external_binaries { let cwd = std::env::current_dir()?; let target_triple = self.target_triple(); for src in external_binaries { let file_name = src .file_name() .ok_or_else(|| crate::Error::FailedToExtractFilename(src.clone()))? .to_string_lossy(); #[cfg(windows)] let src = src.with_file_name(format!("{file_name}-{target_triple}.exe")); #[cfg(not(windows))] let src = src.with_file_name(format!("{file_name}-{target_triple}")); #[cfg(windows)] let dest = path.join(format!("{file_name}.exe")); #[cfg(not(windows))] let dest = path.join(&*file_name); fs::copy(&src, &dest).map_err(|e| Error::CopyFile(src.clone(), dest.clone(), e))?; paths.push(dest); } } Ok(paths) } } fn sanitize_path>(path: P) -> PathBuf { let mut dest = PathBuf::new(); for c in path.as_ref().components() { if let std::path::Component::Normal(s) = c { dest.push(s) } } dest } fn default_true() -> bool { true } ================================================ FILE: crates/packager/src/error.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::path::PathBuf; use thiserror::Error; #[non_exhaustive] #[derive(Error, Debug)] /// Errors returned by cargo-packager. pub enum Error { /// JSON parsing error. #[error(transparent)] Json(#[from] serde_json::Error), /// Target triple architecture error #[error("Unable to determine target-architecture")] Architecture, /// Target triple OS error #[error("Unable to determine target-os")] Os, /// Target triple environment error #[error("Unable to determine target-environment")] Environment, /// I/O errors with path. #[error("I/O Error ({0}): {1}")] IoWithPath(PathBuf, std::io::Error), /// I/O copy file errors. #[error("Failed to copy file from {0} to {1}: {2}")] CopyFile(PathBuf, PathBuf, std::io::Error), /// I/O rename file errors. #[error("Failed to rename file from {0} to {1}: {2}")] RenameFile(PathBuf, PathBuf, std::io::Error), /// I/O symlink file errors. #[error("Failed to symlink file from {0} to {1}: {2}")] Symlink(PathBuf, PathBuf, std::io::Error), /// I/O errors. #[error(transparent)] Io(#[from] std::io::Error), /// Hex de/encoding errors. #[error(transparent)] Hex(#[from] hex::FromHexError), /// Failed to validate downloaded file hash. #[error("Hash mismatch of downloaded file")] HashError, /// Zip error. #[error(transparent)] ZipError(#[from] zip::result::ZipError), /// Zip error. #[error(transparent)] DownloadError(#[from] Box), /// Unsupported OS bitness. #[error("Unsupported OS bitness")] UnsupportedBitness, /// Windows SignTool not found. #[error("SignTool not found")] SignToolNotFound, /// Unexpected target triple. #[error("Unexpected target triple: {0}")] UnexpectedTargetTriple(String), /// Unsupported architecture. #[error("Unsupported architecture for \"{0}\" target triple: {0}")] UnsupportedArch(String, String), /// Could not find the main binary in list of provided binaries. #[error("Could not find the main binary in list of provided binaries")] MainBinaryNotFound, /// Semver parsing error #[error(transparent)] Semver(#[from] semver::Error), /// Non-numeric build metadata in app version. #[error("Optional build metadata in app version must be numeric-only {}", .0.clone().unwrap_or_default())] NonNumericBuildMetadata(Option), /// Invalid app version when building [crate::PackageFormat::Wix] #[error("Invalid app version: {0}")] InvalidAppVersion(String), /// Handlebars render error. #[error(transparent)] HandleBarsRenderError(#[from] handlebars::RenderError), /// Handlebars template error. #[error(transparent)] HandleBarsTemplateError(#[from] Box), /// Nsis error #[error("Error running makensis.exe: {0}")] NsisFailed(std::io::Error), /// Nsis error #[error("Error running {0}: {0}")] WixFailed(String, std::io::Error), /// create-dmg script error #[error("Error running create-dmg script: {0}")] CreateDmgFailed(std::io::Error), /// signtool.exe error #[error("Error running signtool.exe: {0}")] SignToolFailed(std::io::Error), /// Custom signing command error #[error("Error running custom signing command: {0}")] CustomSignCommandFailed(std::io::Error), /// bundle_appimage script error #[error("Error running bundle_appimage.sh script: {0}")] AppImageScriptFailed(std::io::Error), /// Failed to get parent directory of a path #[error("Failed to get parent directory of {0}")] ParentDirNotFound(PathBuf), /// A hook, for example `beforePackagaingCommand`, has failed. #[error("{0} `{1}` failed: {2}")] HookCommandFailure(String, String, std::io::Error), /// A hook, for example `beforePackagaingCommand`, has failed with an exit code. #[error("{0} `{1}` failed with exit code {2}")] HookCommandFailureWithExitCode(String, String, i32), /// Regex error. #[cfg(windows)] #[error(transparent)] RegexError(#[from] regex::Error), /// Glob pattern error. #[error(transparent)] GlobPatternError(#[from] glob::PatternError), /// Glob error. #[error(transparent)] Glob(#[from] glob::GlobError), /// Unsupported WiX language #[cfg(windows)] #[error("Wix language {0} not found. It must be one of {1}")] UnsupportedWixLanguage(String, String), /// Image crate errors. #[error(transparent)] ImageError(#[from] image::ImageError), /// walkdir crate errors. #[error(transparent)] WalkDirError(#[from] walkdir::Error), /// Path prefix strip error. #[error(transparent)] StripPrefixError(#[from] std::path::StripPrefixError), /// Relative paths errors #[error(transparent)] RelativeToError(#[from] relative_path::RelativeToError), /// Time error. #[error("`{0}`")] TimeError(#[from] time::error::Error), /// Plist error. #[error(transparent)] Plist(#[from] plist::Error), /// Framework not found. #[error("Framework {0} not found")] FrameworkNotFound(String), /// Invalid framework. #[error("Invalid framework {framework}: {reason}")] InvalidFramework { /// Framework name framework: String, /// Reason why this framework is invalid reason: &'static str, }, /// Invalid icons. #[error("Could not find a valid icon")] InvalidIconList, /// Failed to notarize. #[error("Failed to notarize app")] FailedToNotarize, /// Rejected on notarize. #[error("Failed to notarize app: {0}")] NotarizeRejected(String), /// Failed to parse notarytool output. #[error("Failed to parse notarytool output as JSON: `{0}`")] FailedToParseNotarytoolOutput(String), /// Failed to find API key file. #[error("Could not find API key file. Please set the APPLE_API_KEY_PATH environment variables to the path to the {filename} file")] ApiKeyMissing { /// Filename of the API key. filename: String, }, /// Missing notarize environment variables. #[error("Could not find APPLE_ID & APPLE_PASSWORD & APPLE_TEAM_ID or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found")] MissingNotarizeAuthVars, /// Failed to list keychains #[error("Failed to list keychains: {0}")] FailedToListKeyChain(std::io::Error), /// Failed to decode certficate as base64 #[error("Failed to decode certficate as base64: {0}")] FailedToDecodeCert(std::io::Error), /// Failed to create keychain. #[error("Failed to create keychain: {0}")] FailedToCreateKeyChain(std::io::Error), /// Failed to create keychain. #[error("Failed to unlock keychain: {0}")] FailedToUnlockKeyChain(std::io::Error), /// Failed to import certificate. #[error("Failed to import certificate: {0}")] FailedToImportCert(std::io::Error), /// Failed to set keychain settings. #[error("Failed to set keychain settings: {0}")] FailedToSetKeychainSettings(std::io::Error), /// Failed to set key partition list. #[error("Failed to set key partition list: {0}")] FailedToSetKeyPartitionList(std::io::Error), /// Failed to run codesign utility. #[error("Failed to run codesign utility: {0}")] FailedToRunCodesign(std::io::Error), /// Failed to run ditto utility. #[error("Failed to run ditto utility: {0}")] FailedToRunDitto(std::io::Error), /// Failed to run xcrun utility. #[error("Failed to run xcrun utility: {0}")] FailedToRunXcrun(std::io::Error), /// Path already exists. #[error("{0} already exists")] AlreadyExists(PathBuf), /// Path does not exist. #[error("{0} does not exist")] DoesNotExist(PathBuf), /// Path is not a directory. #[error("{0} is not a directory")] IsNotDirectory(PathBuf), /// Could not find a square icon to use as AppImage icon #[error("Could not find a square icon to use as AppImage icon")] AppImageSquareIcon, /// Base64 decoding error. #[error(transparent)] Base64DecodeError(#[from] base64::DecodeError), /// Utf8 parsing error. #[error(transparent)] Utf8Error(#[from] std::str::Utf8Error), /// minisign errors. #[error(transparent)] Minisign(#[from] minisign::PError), /// System time errors. #[error(transparent)] SystemTimeError(#[from] std::time::SystemTimeError), /// Signing keys generation error. #[error("aborted key generation, {0} already exists and force overrwite wasnot desired.")] SigningKeyExists(PathBuf), /// Failed to extract external binary filename #[error("Failed to extract filename from {0}")] FailedToExtractFilename(PathBuf), /// Failed to remove extended attributes from app bundle #[error("Failed to remove extended attributes from app bundle: {0}")] #[cfg(target_os = "macos")] FailedToRemoveExtendedAttributes(std::io::Error), /// Could not find the embedded.provisionprofile file. #[error("Embedded provision profile file {0} not found")] EmbeddedProvisionprofileFileNotFound(PathBuf), /// Could not copy the embedded.provisionprofile file to the Contents directory. #[error("Could not copy embedded provision profile file {0}: {1}")] FailedToCopyEmbeddedProvisionprofile(PathBuf, std::io::Error), /// Failed to open Windows registry. #[error("failed to open registry {0}")] OpenRegistry(String), /// Failed to get registry value. #[error("failed to get {0} value on registry")] GetRegistryValue(String), /// Failed to enumerate registry keys. #[error("failed to enumerate registry keys")] FailedToEnumerateRegKeys, } /// Convenient type alias of Result type for cargo-packager. pub type Result = std::result::Result; ================================================ FILE: crates/packager/src/lib.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! [![cargo-packager splash](https://github.com/crabnebula-dev/cargo-packager/raw/main/.github/splash.png)](https://github.com/crabnebula-dev/cargo-packager) //! //! Executable packager, bundler and updater. A cli tool and library to generate installers or app bundles for your executables. //! It also comes with useful addons: //! - an [updater](https://docs.rs/cargo-packager-updater) //! - a [resource resolver](https://docs.rs/cargo-packager-resource-resolver) //! //! ### Supported packages //! //! - macOS //! - DMG (.dmg) //! - Bundle (.app) //! - Linux //! - Debian package (.deb) //! - AppImage (.AppImage) //! - Pacman (.tar.gz and PKGBUILD) //! - Windows //! - NSIS (.exe) //! - MSI using WiX Toolset (.msi) //! //! ## CLI //! //! This crate is a cargo subcommand so you can install using: //! //! ```sh //! cargo install cargo-packager --locked //! ``` //! You then need to configure your app so the cli can recognize it. Configuration can be done in `Packager.toml` or `packager.json` in your project or modify Cargo.toml and include this snippet: //! //! ```toml //! [package.metadata.packager] //! before-packaging-command = "cargo build --release" //! ``` //! //! Once, you are done configuring your app, run: //! //! ```sh //! cargo packager --release //! ``` //! //! ### Configuration //! //! By default, the packager reads its configuration from `Packager.toml` or `packager.json` if it exists, and from `package.metadata.packager` table in `Cargo.toml`. //! You can also specify a custom configuration using the `-c/--config` cli argument. //! //! For a full list of configuration options, see [Config]. //! //! You could also use the [schema](./schema.json) file from GitHub to validate your configuration or have auto completions in your IDE. //! //! ### Building your application before packaging //! //! By default, the packager doesn't build your application, so if your app requires a compilation step, the packager has an option to specify a shell command to be executed before packaing your app, `beforePackagingCommand`. //! //! ### Cargo profiles //! //! By default, the packager looks for binaries built using the `debug` profile, if your `beforePackagingCommand` builds your app using `cargo build --release`, you will also need to //! run the packager in release mode `cargo packager --release`, otherwise, if you have a custom cargo profile, you will need to specify it using `--profile` cli arg `cargo packager --profile custom-release-profile`. //! //! ### Library //! //! This crate is also published to crates.io as a library that you can integrate into your tooling, just make sure to disable the default-feature flags. //! //! ```sh //! cargo add cargo-packager --no-default-features //! ``` //! //! #### Feature flags //! //! - **`cli`**: Enables the cli specifc features and dependencies. Enabled by default. //! - **`tracing`**: Enables `tracing` crate integration. #![cfg_attr(doc_cfg, feature(doc_cfg))] #![deny(missing_docs)] use std::{io::Write, path::PathBuf}; mod codesign; mod error; mod package; mod shell; mod util; #[cfg(feature = "cli")] #[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))] pub mod cli; pub mod config; pub mod sign; pub use config::{Config, PackageFormat}; pub use error::{Error, Result}; use flate2::{write::GzEncoder, Compression}; pub use sign::SigningConfig; pub use package::{package, PackageOutput}; use util::PathExt; #[cfg(feature = "cli")] fn parse_log_level(verbose: u8) -> tracing::Level { match verbose { 0 => tracing_subscriber::EnvFilter::builder() .from_env_lossy() .max_level_hint() .and_then(|l| l.into_level()) .unwrap_or(tracing::Level::INFO), 1 => tracing::Level::DEBUG, 2.. => tracing::Level::TRACE, } } /// Inits the tracing subscriber. #[cfg(feature = "cli")] #[cfg_attr(doc_cfg, doc(cfg(feature = "cli")))] pub fn init_tracing_subscriber(verbosity: u8) { let level = parse_log_level(verbosity); let debug = level == tracing::Level::DEBUG; let tracing = level == tracing::Level::TRACE; let subscriber = tracing_subscriber::fmt() .with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr())) .with_target(debug) .with_line_number(tracing) .with_file(tracing) .with_max_level(level); let formatter = tracing_subscriber::fmt::format() .compact() .with_target(debug) .with_line_number(tracing) .with_file(tracing); if tracing { subscriber .event_format(TracingFormatter::WithTime(formatter)) .init(); } else { subscriber .without_time() .event_format(TracingFormatter::WithoutTime(formatter.without_time())) .init(); } } #[cfg(feature = "cli")] enum TracingFormatter { WithoutTime( tracing_subscriber::fmt::format::Format, ), WithTime(tracing_subscriber::fmt::format::Format), } #[cfg(feature = "cli")] struct ShellFieldVisitor { message: String, } #[cfg(feature = "cli")] impl tracing::field::Visit for ShellFieldVisitor { fn record_str(&mut self, field: &tracing::field::Field, value: &str) { if field.name() == "message" { self.message = value.to_string(); } } fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { if field.name() == "message" { self.message = format!("{value:?}"); } } } #[cfg(feature = "cli")] impl tracing_subscriber::fmt::FormatEvent for TracingFormatter where S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, N: for<'a> tracing_subscriber::fmt::FormatFields<'a> + 'static, { fn format_event( &self, ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, mut writer: tracing_subscriber::fmt::format::Writer<'_>, event: &tracing::Event<'_>, ) -> std::fmt::Result { if event.fields().any(|f| f.name() == "shell") { let mut visitor = ShellFieldVisitor { message: "".into() }; event.record(&mut visitor); writeln!(writer, "{}", visitor.message) } else { match self { TracingFormatter::WithoutTime(formatter) => { formatter.format_event(ctx, writer, event) } TracingFormatter::WithTime(formatter) => formatter.format_event(ctx, writer, event), } } } } /// Sign the specified packages and return the signatures paths. /// /// If `packages` contain a directory in the case of [`PackageFormat::App`] /// it will zip the directory before signing and appends it to `packages`. #[tracing::instrument(level = "trace")] pub fn sign_outputs( config: &SigningConfig, packages: &mut Vec, ) -> crate::Result> { let mut signatures = Vec::new(); for package in packages { for path in &package.paths.clone() { let path = if path.is_dir() { let zip = path.with_additional_extension("tar.gz"); let dest_file = util::create_file(&zip)?; let gzip_encoder = GzEncoder::new(dest_file, Compression::default()); let writer = util::create_tar_from_dir(path, gzip_encoder)?; let mut dest_file = writer.finish()?; dest_file.flush()?; package.paths.push(zip); package.paths.last().unwrap() } else { path }; signatures.push(sign::sign_file(config, path)?); } } Ok(signatures) } /// Package an app using the specified config. /// Then signs the generated packages. /// /// This is similar to calling `sign_outputs(signing_config, package(config)?)` /// /// Returns a tuple of list of packages and list of signatures. #[tracing::instrument(level = "trace")] pub fn package_and_sign( config: &Config, signing_config: &SigningConfig, ) -> crate::Result<(Vec, Vec)> { let mut packages = package(config)?; let signatures = sign_outputs(signing_config, &mut packages)?; Ok((packages, signatures)) } ================================================ FILE: crates/packager/src/package/app/mod.rs ================================================ // Copyright 2016-2019 Cargo-Bundle developers // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::fs; use std::path::{Path, PathBuf}; use super::Context; use crate::Error; use crate::{config::Config, util}; #[cfg(target_os = "macos")] use crate::{ codesign::macos::{self as codesign, SignTarget}, shell::CommandExt, }; #[tracing::instrument(level = "trace", skip(ctx))] pub(crate) fn package(ctx: &Context) -> crate::Result> { let Context { config, .. } = ctx; // we should use the bundle name (App name) as a macOS standard. // version or platform shouldn't be included in the App name. let app_product_name = format!("{}.app", config.product_name); let app_bundle_path = config.out_dir().join(&app_product_name); if app_bundle_path.exists() { fs::remove_dir_all(&app_bundle_path) .map_err(|e| Error::IoWithPath(app_bundle_path.clone(), e))?; } tracing::info!( "Packaging {} ({})", app_product_name, app_bundle_path.display() ); let contents_directory = app_bundle_path.join("Contents"); fs::create_dir_all(&contents_directory) .map_err(|e| Error::IoWithPath(contents_directory.clone(), e))?; let resources_dir = contents_directory.join("Resources"); let bin_dir = contents_directory.join("MacOS"); fs::create_dir_all(&bin_dir).map_err(|e| Error::IoWithPath(bin_dir.clone(), e))?; #[cfg(target_os = "macos")] let mut sign_paths = std::collections::BinaryHeap::new(); let bundle_icon_file = util::create_icns_file(&resources_dir, config)?; tracing::debug!("Creating Info.plist"); create_info_plist(&contents_directory, bundle_icon_file, config)?; tracing::debug!("Copying frameworks"); let _framework_paths = copy_frameworks_to_bundle(&contents_directory, config)?; #[cfg(target_os = "macos")] sign_paths.extend( _framework_paths .into_iter() .filter(|p| { let ext = p.extension(); ext == Some(std::ffi::OsStr::new("framework")) }) .map(|path| SignTarget { path, is_native_binary: false, }), ); tracing::debug!("Copying resources"); config.copy_resources(&resources_dir)?; tracing::debug!("Copying embedded.provisionprofile"); copy_embedded_provisionprofile_file(&contents_directory, config)?; tracing::debug!("Copying embedded apps"); let embedded_apps = copy_embedded_apps(&contents_directory, config)?; tracing::debug!("Copying external binaries"); config.copy_external_binaries(&bin_dir)?; tracing::debug!("Copying binaries"); for bin in &config.binaries { let bin_path = config.binary_path(bin); let dest_path = bin_dir.join(bin.path.file_name().unwrap()); fs::copy(&bin_path, &dest_path) .map_err(|e| Error::CopyFile(bin_path.clone(), dest_path.clone(), e))?; } // All dylib files and native executables should be signed manually // It is highly discouraged by Apple to use the --deep codesign parameter in larger projects. // https://developer.apple.com/forums/thread/129980 // Find all files in the app bundle let files = walkdir::WalkDir::new(&app_bundle_path) .into_iter() .flatten() .map(|dir| dir.into_path()) .filter(|path| !embedded_apps.iter().any(|x| path.starts_with(x))); // Filter all files for Mach-O headers. This will target all .dylib and native executable files for file in files { let metadata = match fs::metadata(&file) { Ok(f) => f, Err(err) => { tracing::warn!("Failed to get metadata for {}: {err}, this file will not be scanned for Mach-O header!", file.display()); continue; } }; // ignore folders and files that do not include at least the header size if !metadata.is_file() || metadata.len() < 4 { continue; } let mut open_file = match fs::File::open(&file) { Ok(f) => f, Err(err) => { tracing::warn!("Failed to open {} for reading: {err}, this file will not be scanned for Mach-O header!", file.display()); continue; } }; let mut buffer = [0; 4]; std::io::Read::read_exact(&mut open_file, &mut buffer)?; const MACH_O_MAGIC_NUMBERS: [u32; 5] = [0xfeedface, 0xfeedfacf, 0xcafebabe, 0xcefaedfe, 0xcffaedfe]; let magic = u32::from_be_bytes(buffer); let is_mach = MACH_O_MAGIC_NUMBERS.contains(&magic); if !is_mach { continue; } #[cfg(target_os = "macos")] sign_paths.push(SignTarget { path: file, is_native_binary: true, }); } #[cfg(target_os = "macos")] if let Some(identity) = config .macos() .and_then(|macos| macos.signing_identity.as_ref()) { tracing::debug!("Codesigning {}", app_bundle_path.display()); // Sign frameworks and sidecar binaries first, per apple, signing must be done inside out // https://developer.apple.com/forums/thread/701514 sign_paths.push(SignTarget { path: app_bundle_path.clone(), is_native_binary: true, }); // Remove extra attributes, which could cause codesign to fail // https://developer.apple.com/library/archive/qa/qa1940/_index.html remove_extra_attr(&app_bundle_path)?; // sign application let sign_paths = sign_paths.into_sorted_vec(); codesign::try_sign(sign_paths, identity, config)?; // notarization is required for distribution match config .macos() .and_then(|m| m.notarization_credentials.clone()) .ok_or(crate::Error::MissingNotarizeAuthVars) .or_else(|_| codesign::notarize_auth()) { Ok(auth) => { tracing::debug!("Notarizing {}", app_bundle_path.display()); codesign::notarize(app_bundle_path.clone(), auth, config)?; } Err(e) => { tracing::warn!("Skipping app notarization, {}", e.to_string()); } } } Ok(vec![app_bundle_path]) } // Creates the Info.plist file. #[tracing::instrument(level = "trace", skip(config))] fn create_info_plist( contents_directory: &Path, bundle_icon_file: Option, config: &Config, ) -> crate::Result<()> { let format = time::format_description::parse("[year][month][day].[hour][minute][second]") .map_err(time::error::Error::from)?; let build_number = time::OffsetDateTime::now_utc() .format(&format) .map_err(time::error::Error::from)?; let mut plist = plist::Dictionary::new(); plist.insert("CFBundleDevelopmentRegion".into(), "English".into()); plist.insert( "CFBundleDisplayName".into(), config.product_name.clone().into(), ); plist.insert( "CFBundleExecutable".into(), config.main_binary_name()?.clone().into(), ); if let Some(path) = bundle_icon_file { plist.insert( "CFBundleIconFile".into(), path.file_name() .ok_or_else(|| Error::FailedToExtractFilename(path.clone()))? .to_string_lossy() .into_owned() .into(), ); } plist.insert("CFBundleIdentifier".into(), config.identifier().into()); plist.insert("CFBundleInfoDictionaryVersion".into(), "6.0".into()); plist.insert("CFBundleName".into(), config.product_name.clone().into()); plist.insert("CFBundlePackageType".into(), "APPL".into()); plist.insert( "CFBundleShortVersionString".into(), config.version.clone().into(), ); plist.insert("CFBundleVersion".into(), build_number.into()); plist.insert("CSResourcesFileMapped".into(), true.into()); if let Some(category) = &config.category { plist.insert( "LSApplicationCategoryType".into(), category.macos_application_category_type().into(), ); } if let Some(version) = config .macos() .and_then(|macos| macos.minimum_system_version.as_deref()) { plist.insert("LSMinimumSystemVersion".into(), version.into()); } if let Some(associations) = &config.file_associations { plist.insert( "CFBundleDocumentTypes".into(), plist::Value::Array( associations .iter() .map(|association| { let mut dict = plist::Dictionary::new(); dict.insert( "CFBundleTypeExtensions".into(), plist::Value::Array( association .extensions .iter() .map(|ext| ext.to_string().into()) .collect(), ), ); dict.insert( "CFBundleTypeName".into(), association .name .as_ref() .unwrap_or(&association.extensions[0]) .to_string() .into(), ); dict.insert( "CFBundleTypeRole".into(), association.role.to_string().into(), ); plist::Value::Dictionary(dict) }) .collect(), ), ); } if let Some(protocols) = &config.deep_link_protocols { plist.insert( "CFBundleURLTypes".into(), plist::Value::Array( protocols .iter() .map(|protocol| { let mut dict = plist::Dictionary::new(); dict.insert( "CFBundleURLSchemes".into(), plist::Value::Array( protocol .schemes .iter() .map(|s| s.to_string().into()) .collect(), ), ); dict.insert( "CFBundleURLName".into(), protocol .name .clone() .unwrap_or(format!( "{} {}", config.identifier(), protocol.schemes[0] )) .into(), ); dict.insert("CFBundleTypeRole".into(), protocol.role.to_string().into()); plist::Value::Dictionary(dict) }) .collect(), ), ); } plist.insert("LSRequiresCarbon".into(), true.into()); plist.insert("NSHighResolutionCapable".into(), true.into()); if let Some(macos_config) = config.macos() { if macos_config.background_app { plist.insert("LSUIElement".into(), true.into()); } } if let Some(copyright) = &config.copyright { plist.insert("NSHumanReadableCopyright".into(), copyright.clone().into()); } if let Some(exception_domain) = config .macos() .and_then(|macos| macos.exception_domain.clone()) { let mut security = plist::Dictionary::new(); let mut domain = plist::Dictionary::new(); domain.insert("NSExceptionAllowsInsecureHTTPLoads".into(), true.into()); domain.insert("NSIncludesSubdomains".into(), true.into()); let mut exception_domains = plist::Dictionary::new(); exception_domains.insert(exception_domain, domain.into()); security.insert("NSExceptionDomains".into(), exception_domains.into()); plist.insert("NSAppTransportSecurity".into(), security.into()); } if let Some(user_plist_path) = config .macos() .and_then(|macos| macos.info_plist_path.as_ref()) { let user_plist = plist::Value::from_file(user_plist_path)?; if let Some(dict) = user_plist.into_dictionary() { for (key, value) in dict { plist.insert(key, value); } } } plist::Value::Dictionary(plist).to_file_xml(contents_directory.join("Info.plist"))?; Ok(()) } #[tracing::instrument(level = "trace")] fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> { if !from.exists() { return Err(Error::DoesNotExist(from.to_path_buf())); } if !from.is_dir() { return Err(Error::IsNotDirectory(from.to_path_buf())); } if to.exists() { return Err(Error::AlreadyExists(to.to_path_buf())); } let parent = to .parent() .ok_or_else(|| Error::ParentDirNotFound(to.to_path_buf()))?; fs::create_dir_all(parent).map_err(|e| Error::IoWithPath(parent.to_path_buf(), e))?; for entry in walkdir::WalkDir::new(from) { let entry = entry?; let path = entry.path(); debug_assert!(path.starts_with(from)); let rel_path = path.strip_prefix(from)?; let dest = to.join(rel_path); if entry.file_type().is_symlink() { let target = fs::read_link(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; #[cfg(unix)] std::os::unix::fs::symlink(&target, &dest) .map_err(|e| Error::Symlink(target, dest, e))?; #[cfg(windows)] { if entry.file_type().is_file() { std::os::windows::fs::symlink_file(&target, &dest) .map_err(|e| Error::Symlink(target, dest, e))?; } else { std::os::windows::fs::symlink_dir(&target, &dest) .map_err(|e| Error::Symlink(target, dest, e))?; } } } else if entry.file_type().is_dir() { fs::create_dir(&dest).map_err(|e| Error::IoWithPath(dest, e))? } else { fs::copy(path, &dest).map_err(|e| Error::CopyFile(path.to_path_buf(), dest, e))?; } } Ok(()) } // Copies the framework under `{src_dir}/{framework}.framework` to `{dest_dir}/{framework}.framework`. #[tracing::instrument(level = "trace")] fn copy_framework_from(dest_dir: &Path, framework: &str, src_dir: &Path) -> crate::Result { let src_name = format!("{framework}.framework"); let src_path = src_dir.join(&src_name); if src_path.exists() { copy_dir(&src_path, &dest_dir.join(&src_name))?; Ok(true) } else { Ok(false) } } // Copies the macOS application bundle frameworks to the .app #[tracing::instrument(level = "trace", skip(config))] fn copy_frameworks_to_bundle( contents_directory: &Path, config: &Config, ) -> crate::Result> { let mut paths = Vec::new(); if let Some(frameworks) = config.macos().and_then(|m| m.frameworks.as_ref()) { let dest_dir = contents_directory.join("Frameworks"); fs::create_dir_all(contents_directory)?; for framework in frameworks { if framework.ends_with(".framework") || framework.ends_with(".app") { let src_path = PathBuf::from(framework); let src_name = src_path .file_name() .ok_or_else(|| Error::FailedToExtractFilename(src_path.clone()))?; let dest_path = dest_dir.join(src_name); copy_dir(&src_path, &dest_path)?; paths.push(dest_path); continue; } else if framework.ends_with(".dylib") { let src_path = PathBuf::from(&framework); if !src_path.exists() { return Err(Error::FrameworkNotFound(framework.to_string())); } let src_name = src_path .file_name() .ok_or_else(|| Error::FailedToExtractFilename(src_path.clone()))?; fs::create_dir_all(&dest_dir)?; let dest_path = dest_dir.join(src_name); fs::copy(&src_path, &dest_path) .map_err(|e| Error::CopyFile(src_path.clone(), dest_path.clone(), e))?; paths.push(dest_path); continue; } else if framework.contains('/') { return Err(Error::InvalidFramework { framework: framework.to_string(), reason: "framework extension should be either .framework, .dylib or .app", }); } if let Some(home_dir) = dirs::home_dir() { if copy_framework_from(&dest_dir, framework, &home_dir.join("Library/Frameworks/"))? { continue; } } if copy_framework_from(&dest_dir, framework, &PathBuf::from("/Library/Frameworks/"))? || copy_framework_from( &dest_dir, framework, &PathBuf::from("/Network/Library/Frameworks/"), )? { continue; } return Err(Error::FrameworkNotFound(framework.to_string())); } } Ok(paths) } #[cfg(target_os = "macos")] fn remove_extra_attr(app_bundle_path: &Path) -> crate::Result<()> { std::process::Command::new("xattr") .arg("-cr") .arg(app_bundle_path) .output_ok() .map(|_| ()) .map_err(crate::Error::FailedToRemoveExtendedAttributes) } // Copies the embedded.provisionprofile file to the Contents directory, if needed. fn copy_embedded_provisionprofile_file( contents_directory: &Path, config: &Config, ) -> crate::Result<()> { if let Some(embedded_provisionprofile_file) = config .macos() .and_then(|m| m.embedded_provisionprofile_path.as_ref()) { if !embedded_provisionprofile_file.exists() { return Err(crate::Error::EmbeddedProvisionprofileFileNotFound( embedded_provisionprofile_file.to_path_buf(), )); } fs::copy( embedded_provisionprofile_file, contents_directory.join("embedded.provisionprofile"), ) .map_err(|e| { crate::Error::FailedToCopyEmbeddedProvisionprofile( embedded_provisionprofile_file.to_path_buf(), e, ) })?; } Ok(()) } // Copies app structures that may need to be embedded inside this app. #[tracing::instrument(level = "trace", skip(config))] fn copy_embedded_apps(contents_directory: &Path, config: &Config) -> crate::Result> { let mut paths = Vec::new(); if let Some(embedded_apps) = config.macos().and_then(|m| m.embedded_apps.as_ref()) { let dest_dir = contents_directory.join("MacOS"); for embedded_app in embedded_apps { let src_path = PathBuf::from(embedded_app); let src_name = src_path .file_name() .ok_or_else(|| Error::FailedToExtractFilename(src_path.clone()))?; let dest_path = dest_dir.join(src_name); copy_dir(&src_path, &dest_path)?; tracing::debug!("Copied embedded app: {:?}", dest_path); paths.push(dest_path); } } Ok(paths) } ================================================ FILE: crates/packager/src/package/appimage/appimage ================================================ #!/usr/bin/env bash # Copyright 2019-2023 Tauri Programme within The Commons Conservancy # Copyright 2023-2023 CrabNebula Ltd. # SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: MIT set -euxo pipefail export ARCH={{arch}} mkdir -p "{{app_name}}.AppDir" cp -r ../appimage_deb/data/* "{{app_name}}.AppDir" cd "{{app_name}}.AppDir" mkdir -p "usr/bin" mkdir -p "usr/lib" mkdir -p "usr/lib64" # Copy libs. Follow symlinks in case `/usr/lib64` is a symlink to `/usr/lib` {{#each libs}} find -L /usr/lib* -name {{this}} -exec mkdir -p "$(dirname '{}')" \; -exec cp --parents '{}' "." \; || true {{/each}} # Copy bins. {{#each bins}} cp {{this}} usr/bin {{/each}} # We need AppRun to be installed as {{app_name}}.AppDir/AppRun. # Otherwise the linuxdeploy scripts will default to symlinking our main bin instead and will crash on trying to launch. cp "{{packager_tools_path}}/AppRun-${ARCH}" AppRun cp "{{icon_path}}" .DirIcon ln -sf "{{icon_path}}" "{{app_name}}.png" ln -sf "usr/share/applications/{{app_name}}.desktop" "{{app_name}}.desktop" cd .. # modify the linux deploy appimage ELF header so that binfmt no longer identifies it as an appimage # and so appimagelauncher doesn't inject itself and the binary runs directly dd if=/dev/zero bs=1 count=3 seek=8 conv=notrunc of="{{packager_tools_path}}/linuxdeploy-{{linuxdeploy_arch}}.AppImage" OUTPUT="{{appimage_path}}" "{{packager_tools_path}}/linuxdeploy-{{linuxdeploy_arch}}.AppImage" --appimage-extract-and-run --appdir "{{app_name}}.AppDir" {{linuxdeploy_plugins}} {{excluded_libs}} --output appimage ================================================ FILE: crates/packager/src/package/appimage/mod.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ collections::BTreeMap, fs, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, process::Command, }; use handlebars::{to_json, Handlebars}; use super::{deb, Context}; use crate::{shell::CommandExt, util, Error}; #[tracing::instrument(level = "trace", skip(ctx))] fn donwload_dependencies( ctx: &Context, appimage_tools_path: &Path, arch: &str, linuxdeploy_arch: &str, ) -> crate::Result<()> { let internal_deps = vec![ ( format!("AppRun-{arch}"), format!("https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-{arch}") ), ( format!("linuxdeploy-{linuxdeploy_arch}.AppImage"), format!("https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-{linuxdeploy_arch}.AppImage") ), // This path is incompatible with cross-platform compilation but linuxdeploy doens't support that anyway. ( "linuxdeploy-plugin-appimage.AppImage".to_string(), format!("https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-{arch}.AppImage") ), ]; let user_deps = ctx .config .appimage() .and_then(|a| a.linuxdeploy_plugins.clone()) .unwrap_or_default() .into_iter() .map(|mut p| { p.0 = format!("linuxdeploy-plugin-{}.sh", p.0); p }) .collect(); for (path, url) in [internal_deps, user_deps].concat() { let path = appimage_tools_path.join(path); if !path.exists() { let data = util::download(&url)?; tracing::debug!( "Writing {} and setting its permissions to 764", path.display() ); fs::write(&path, data).map_err(|e| Error::IoWithPath(path.clone(), e))?; fs::set_permissions(&path, fs::Permissions::from_mode(0o764)) .map_err(|e| Error::IoWithPath(path, e))?; } } Ok(()) } #[tracing::instrument(level = "trace", skip(ctx))] pub(crate) fn package(ctx: &Context) -> crate::Result> { let Context { config, intermediates_path, tools_path, .. } = ctx; let mut config = config.clone(); let main_binary_name = config.main_binary_name()?; // if binary file name contains spaces, we must change it to kebab-case if main_binary_name.contains(' ') { let main_binary = config.main_binary_mut()?; let main_binary_name_kebab = heck::AsKebabCase(main_binary_name).to_string(); let new_path = intermediates_path.join(&main_binary_name_kebab); fs::copy(&main_binary.path, &new_path)?; main_binary.path = new_path; } // generate the deb binary name let (arch, linuxdeploy_arch) = match config.target_arch()? { "x86" => ("i686", "i386"), "arm" => ("armhf", "arm"), other => (other, other), }; let appimage_tools_path = tools_path.join("AppImage"); fs::create_dir_all(&appimage_tools_path) .map_err(|e| Error::IoWithPath(appimage_tools_path.clone(), e))?; donwload_dependencies(ctx, &appimage_tools_path, arch, linuxdeploy_arch)?; let appimage_deb_data_dir = intermediates_path.join("appimage_deb").join("data"); let intermediates_path = intermediates_path.join("appimage"); // generate deb_folder structure tracing::debug!("Generating data"); let icons = deb::generate_data(&config, &appimage_deb_data_dir)?; tracing::debug!("Copying files specified in `appimage.files`"); if let Some(files) = config.appimage().and_then(|d| d.files.as_ref()) { deb::copy_custom_files(files, &appimage_deb_data_dir)?; } let icons: Vec = icons.into_iter().collect(); let main_binary_name = config.main_binary_name()?; let upcase_app_name = main_binary_name.to_uppercase(); let app_dir_path = intermediates_path.join(format!("{}.AppDir", &main_binary_name)); let appimage_filename = format!("{}_{}_{}.AppImage", main_binary_name, config.version, arch); let appimage_path = config.out_dir().join(&appimage_filename); fs::create_dir_all(&app_dir_path).map_err(|e| Error::IoWithPath(app_dir_path.clone(), e))?; // setup data to insert into shell script let mut sh_map = BTreeMap::new(); sh_map.insert("arch", to_json(arch)); sh_map.insert("linuxdeploy_arch", to_json(linuxdeploy_arch)); sh_map.insert("app_name", to_json(main_binary_name)); sh_map.insert("app_name_uppercase", to_json(upcase_app_name)); sh_map.insert("appimage_path", to_json(&appimage_path)); sh_map.insert( "packager_tools_path", to_json(appimage_tools_path.display().to_string()), ); let libs = config .appimage() .and_then(|c| c.libs.clone()) .unwrap_or_default(); sh_map.insert("libs", to_json(libs)); let bins = config .appimage() .and_then(|c| c.bins.clone()) .unwrap_or_default(); sh_map.insert("bins", to_json(bins)); let linuxdeploy_plugins = config .appimage() .and_then(|a| a.linuxdeploy_plugins.clone()) .unwrap_or_default() .into_keys() .map(|name| format!("--plugin {name}")) .collect::>() .join(" "); sh_map.insert("linuxdeploy_plugins", to_json(linuxdeploy_plugins)); let excluded_libraries = config .appimage() .and_then(|a| a.excluded_libs.clone()) .unwrap_or_default() .into_iter() .map(|library| format!("--exclude-library {library}")) .collect::>() .join(" "); sh_map.insert("excluded_libs", to_json(excluded_libraries)); let larger_icon = icons .iter() .filter(|i| i.width == i.height) .max_by_key(|i| i.width) .ok_or(crate::Error::AppImageSquareIcon)?; let larger_icon_path = larger_icon .path .strip_prefix(appimage_deb_data_dir) .unwrap() .to_string_lossy() .to_string(); sh_map.insert("icon_path", to_json(larger_icon_path)); // initialize shell script template. let mut handlebars = Handlebars::new(); handlebars.register_escape_fn(handlebars::no_escape); handlebars .register_template_string("appimage", include_str!("appimage")) .map_err(Box::new)?; let template = handlebars.render("appimage", &sh_map)?; let sh_file = intermediates_path.join("build_appimage.sh"); tracing::debug!( "Writing {} and setting its permissions to 764", sh_file.display() ); fs::write(&sh_file, template).map_err(|e| Error::IoWithPath(sh_file.clone(), e))?; fs::set_permissions(&sh_file, fs::Permissions::from_mode(0o764)) .map_err(|e| Error::IoWithPath(sh_file.clone(), e))?; tracing::info!( "Packaging {} ({})", appimage_filename, appimage_path.display() ); // execute the shell script to build the appimage. Command::new(&sh_file) .current_dir(intermediates_path) .output_ok() .map_err(crate::Error::AppImageScriptFailed)?; Ok(vec![appimage_path]) } ================================================ FILE: crates/packager/src/package/context.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{fs, path::PathBuf}; use crate::{util, Config}; /// The packaging context info #[derive(Debug)] pub struct Context { /// The config for the app we are packaging pub config: Config, /// The intermediates path, which is `/.cargo-packager` pub intermediates_path: PathBuf, /// The global path which we store tools used by cargo-packager and usually is /// `/.cargo-packager` pub tools_path: PathBuf, } impl Context { pub fn new(config: &Config) -> crate::Result { let tools_path = dirs::cache_dir() .unwrap_or_else(|| config.out_dir()) .join(".cargo-packager"); if !tools_path.exists() { fs::create_dir_all(&tools_path)?; } let intermediates_path = config.out_dir().join(".cargo-packager"); util::create_clean_dir(&intermediates_path)?; Ok(Self { config: config.clone(), tools_path, intermediates_path, }) } } ================================================ FILE: crates/packager/src/package/deb/main.desktop ================================================ [Desktop Entry] Categories={{categories}} {{#if comment}} Comment={{comment}} {{/if}} Exec={{exec}} {{exec_arg}} Icon={{icon}} Name={{name}} Terminal=false Type=Application {{#if mime_type}} MimeType={{mime_type}} {{/if}} ================================================ FILE: crates/packager/src/package/deb/mod.rs ================================================ // Copyright 2016-2019 Cargo-Bundle developers // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ collections::{BTreeSet, HashMap}, ffi::OsStr, fs::{self, File}, io::{BufReader, Write}, path::{Path, PathBuf}, }; use flate2::{write::GzEncoder, Compression}; use handlebars::Handlebars; use heck::AsKebabCase; use image::{codecs::png::PngDecoder, ImageDecoder}; use relative_path::PathExt; use serde::Serialize; use tar::HeaderMode; use walkdir::WalkDir; use super::Context; use crate::{ config::Config, util::{self, PathExt as UtilPathExt}, Error, }; #[derive(PartialEq, Eq, PartialOrd, Ord)] pub struct DebIcon { pub width: u32, pub height: u32, pub is_high_density: bool, pub path: PathBuf, } /// Generate the icon files and store them under the `data_dir`. #[tracing::instrument(level = "trace", skip(config))] fn generate_icon_files(config: &Config, data_dir: &Path) -> crate::Result> { let hicolor_dir = data_dir.join("usr/share/icons/hicolor"); let main_binary_name = config.main_binary_name()?; let get_dest_path = |width: u32, height: u32, is_high_density: bool| { hicolor_dir.join(format!( "{}x{}{}/apps/{}.png", width, height, if is_high_density { "@2" } else { "" }, main_binary_name )) }; let mut icons_set = BTreeSet::new(); if let Some(icons) = config.icons()? { for icon_path in icons { if icon_path.extension() != Some(OsStr::new("png")) { continue; } // Put file in scope so that it's closed when copying it let deb_icon = { let file = File::open(&icon_path).map_err(|e| Error::IoWithPath(icon_path.clone(), e))?; let file = BufReader::new(file); let decoder = PngDecoder::new(file)?; let width = decoder.dimensions().0; let height = decoder.dimensions().1; let is_high_density = util::is_retina(&icon_path); let dest_path = get_dest_path(width, height, is_high_density); DebIcon { width, height, is_high_density, path: dest_path, } }; if !icons_set.contains(&deb_icon) { let parent = deb_icon .path .parent() .ok_or_else(|| crate::Error::ParentDirNotFound(deb_icon.path.clone()))?; fs::create_dir_all(parent) .map_err(|e| Error::IoWithPath(parent.to_path_buf(), e))?; fs::copy(&icon_path, &deb_icon.path) .map_err(|e| Error::CopyFile(icon_path.clone(), deb_icon.path.clone(), e))?; icons_set.insert(deb_icon); } } } Ok(icons_set) } /// Generate the application desktop file and store it under the `data_dir`. #[tracing::instrument(level = "trace", skip(config))] fn generate_desktop_file(config: &Config, data_dir: &Path) -> crate::Result<()> { let bin_name = config.main_binary_name()?; let desktop_file_name = format!("{bin_name}.desktop"); let desktop_file_path = data_dir .join("usr/share/applications") .join(desktop_file_name); // For more information about the format of this file, see: // let file = &mut util::create_file(&desktop_file_path)?; let mut handlebars = Handlebars::new(); handlebars.register_escape_fn(handlebars::no_escape); if let Some(template) = config.deb().and_then(|d| d.desktop_template.as_ref()) { handlebars .register_template_string("main.desktop", fs::read_to_string(template)?) .map_err(Box::new)?; } else { handlebars .register_template_string("main.desktop", include_str!("./main.desktop")) .map_err(Box::new)?; } #[derive(Serialize)] struct DesktopTemplateParams<'a> { categories: &'a str, comment: Option<&'a str>, exec: &'a str, exec_arg: Option<&'a str>, icon: &'a str, name: &'a str, mime_type: Option, } // Set the argument code at the end of the `Exec` key. // See the docs for `DebianConfig::desktop_template` for more details. let mut exec_arg = None; let mut mime_type: Vec = Vec::new(); if let Some(associations) = &config.file_associations { if !associations.is_empty() { exec_arg = Some("%F"); } mime_type.extend( associations .iter() .filter_map(|association| association.mime_type.clone()), ); } if let Some(protocols) = &config.deep_link_protocols { if !protocols.is_empty() { // Use "%U" even if file associations were already provided, // as it can also accommodate file names in addition to URLs. exec_arg = Some("%U"); } mime_type.extend( protocols .iter() .flat_map(|protocol| &protocol.schemes) .map(|s| format!("x-scheme-handler/{s}")), ); } let mime_type = (!mime_type.is_empty()).then(|| mime_type.join(";")); let bin_name_exec = if bin_name.contains(' ') { format!("\"{bin_name}\"") } else { bin_name.to_string() }; handlebars.render_to_write( "main.desktop", &DesktopTemplateParams { categories: config .category .map(|category| category.gnome_desktop_categories()) .unwrap_or(""), comment: config.description.as_deref(), exec: &bin_name_exec, exec_arg, icon: &bin_name, name: config.product_name.as_str(), mime_type, }, file, )?; Ok(()) } #[tracing::instrument(level = "trace", skip(config))] pub fn generate_data(config: &Config, data_dir: &Path) -> crate::Result> { let bin_dir = data_dir.join("usr/bin"); tracing::debug!("Copying binaries"); fs::create_dir_all(&bin_dir).map_err(|e| Error::IoWithPath(bin_dir.clone(), e))?; for bin in config.binaries.iter() { let bin_path = config.binary_path(bin); let bin_out_path = bin_dir.join(bin.path.file_name().unwrap()); fs::copy(&bin_path, &bin_out_path) .map_err(|e| Error::CopyFile(bin_path.clone(), bin_out_path.clone(), e))?; } tracing::debug!("Copying resources"); let resource_dir = data_dir.join("usr/lib").join(config.main_binary_name()?); config.copy_resources(&resource_dir)?; tracing::debug!("Copying external binaries"); config.copy_external_binaries(&bin_dir)?; tracing::debug!("Generating icons"); let icons = generate_icon_files(config, data_dir)?; let generate_desktop_entry = config .linux() .is_none_or(|linux| linux.generate_desktop_entry); if generate_desktop_entry { tracing::debug!("Generating desktop file"); generate_desktop_file(config, data_dir)?; } Ok(icons) } pub fn get_size>(path: P) -> crate::Result { let mut result = 0; let path = path.as_ref(); if path.is_dir() { for entry in fs::read_dir(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))? { let path = entry?.path(); if path.is_file() { let metadata = path.metadata().map_err(|e| Error::IoWithPath(path, e))?; result += metadata.len(); } else { result += get_size(path)?; } } } else { let metadata = path .metadata() .map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; result += metadata.len(); } Ok(result) } /// Copies user-defined files to the deb package. #[tracing::instrument(level = "trace")] pub fn copy_custom_files(files: &HashMap, data_dir: &Path) -> crate::Result<()> { for (src, target) in files.iter() { let src = Path::new(src); let src = src .canonicalize() .map_err(|e| Error::IoWithPath(src.to_path_buf(), e))?; let target = Path::new(target); let target = if target.is_absolute() { target.strip_prefix("/").unwrap() } else { target }; if src.is_file() { let dest = data_dir.join(target); let parent = dest .parent() .ok_or_else(|| crate::Error::ParentDirNotFound(dest.clone()))?; fs::create_dir_all(parent).map_err(|e| Error::IoWithPath(parent.to_path_buf(), e))?; fs::copy(&src, &dest).map_err(|e| Error::CopyFile(src, dest, e))?; } else if src.is_dir() { let dest_dir = data_dir.join(target); for entry in walkdir::WalkDir::new(&src) { let entry = entry?; let path = entry.path(); if path.is_file() { let relative = path.relative_to(&src)?.to_path(""); let dest = dest_dir.join(relative); let parent = dest .parent() .ok_or_else(|| crate::Error::ParentDirNotFound(dest.clone()))?; fs::create_dir_all(parent) .map_err(|e| Error::IoWithPath(parent.to_path_buf(), e))?; fs::copy(path, &dest) .map_err(|e| Error::CopyFile(src.clone(), dest.clone(), e))?; } } } } Ok(()) } /// Generates the debian control file and stores it under the `control_dir`. #[tracing::instrument(level = "trace", skip(config))] fn generate_control_file( config: &Config, arch: &str, control_dir: &Path, data_dir: &Path, ) -> crate::Result<()> { // For more information about the format of this file, see // https://www.debian.org/doc/debian-policy/ch-controlfields.html let dest_path = control_dir.join("control"); let mut file = util::create_file(&dest_path)?; let pkg_name = config .deb() .and_then(|deb| deb.package_name.clone()) .unwrap_or_else(|| AsKebabCase(&config.product_name).to_string()); writeln!(file, "Package: {pkg_name}")?; writeln!(file, "Version: {}", &config.version)?; writeln!(file, "Architecture: {arch}")?; // Installed-Size must be divided by 1024, see https://www.debian.org/doc/debian-policy/ch-controlfields.html#installed-size writeln!(file, "Installed-Size: {}", get_size(data_dir)? / 1024)?; if let Some(authors) = &config.authors { writeln!(file, "Maintainer: {}", authors.join(", "))?; } if let Some(section) = config.deb().and_then(|d| d.section.as_ref()) { writeln!(file, "Section: {section}")?; } if let Some(priority) = config.deb().and_then(|d| d.priority.as_ref()) { writeln!(file, "Priority: {priority}")?; } else { writeln!(file, "Priority: optional")?; } if let Some(homepage) = &config.homepage { writeln!(file, "Homepage: {homepage}")?; } if let Some(depends) = config.deb().and_then(|d| d.depends.as_ref()) { let dependencies = depends.to_list()?; if !dependencies.is_empty() { writeln!(file, "Depends: {}", dependencies.join(", "))?; } } writeln!( file, "Description: {}", config.description.as_deref().unwrap_or("(none)") )?; for line in config .long_description .as_deref() .unwrap_or("(none)") .lines() { let line = line.trim(); if line.is_empty() { writeln!(file, " .")?; } else { writeln!(file, " {line}")?; } } file.flush()?; Ok(()) } /// Creates an `md5sums` file in the `control_dir` containing the MD5 checksums /// for each file within the `data_dir`. #[tracing::instrument(level = "trace")] fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> { let md5sums_path = control_dir.join("md5sums"); let mut md5sums_file = util::create_file(&md5sums_path)?; for entry in WalkDir::new(data_dir) { let entry = entry?; let path = entry.path(); if path.is_dir() { continue; } let mut file = File::open(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; let mut hash = md5::Context::new(); std::io::copy(&mut file, &mut hash)?; for byte in hash.finalize().iter() { write!(md5sums_file, "{byte:02x}")?; } let rel_path = path.strip_prefix(data_dir)?; let path_str = rel_path.to_str().ok_or_else(|| { let msg = format!("Non-UTF-8 path: {rel_path:?}"); std::io::Error::new(std::io::ErrorKind::InvalidData, msg) })?; writeln!(md5sums_file, " {path_str}")?; } Ok(()) } fn create_tar_from_dir, W: Write>(src_dir: P, dest_file: W) -> crate::Result { use std::os::unix::fs::MetadataExt; let src_dir = src_dir.as_ref(); let mut tar_builder = tar::Builder::new(dest_file); for entry in walkdir::WalkDir::new(src_dir) { let entry = entry?; let src_path = entry.path(); if src_path == src_dir { continue; } let dest_path = src_path.strip_prefix(src_dir)?; let stat = fs::metadata(src_path).map_err(|e| Error::IoWithPath(src_path.to_path_buf(), e))?; let mut header = tar::Header::new_gnu(); header.set_metadata_in_mode(&stat, HeaderMode::Deterministic); header.set_mtime(stat.mtime() as u64); if entry.file_type().is_dir() { tar_builder.append_data(&mut header, dest_path, &mut std::io::empty())?; } else { let mut src_file = File::open(src_path).map_err(|e| Error::IoWithPath(src_path.to_path_buf(), e))?; tar_builder.append_data(&mut header, dest_path, &mut src_file)?; } } tar_builder.into_inner().map_err(Into::into) } /// Creates a `.tar.gz` file from the given directory (placing the new file /// within the given directory's parent directory), then deletes the original /// directory and returns the path to the new file. pub fn tar_and_gzip_dir>(src_dir: P) -> crate::Result { let src_dir = src_dir.as_ref(); let dest_path = src_dir.with_additional_extension("tar.gz"); let dest_file = util::create_file(&dest_path)?; let gzip_encoder = GzEncoder::new(dest_file, Compression::default()); let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?; let mut dest_file = gzip_encoder.finish()?; dest_file.flush()?; Ok(dest_path) } /// Creates an `ar` archive from the given source files and writes it to the /// given destination path. fn create_archive(srcs: Vec, dest: &Path) -> crate::Result<()> { let mut builder = ar::Builder::new(util::create_file(dest)?); for path in &srcs { builder.append_path(path)?; } builder.into_inner()?.flush()?; Ok(()) } #[tracing::instrument(level = "trace", skip(ctx))] pub(crate) fn package(ctx: &Context) -> crate::Result> { let Context { config, intermediates_path, .. } = ctx; let arch = match config.target_arch()? { "x86" => "i386", "x86_64" => "amd64", "arm" => "armhf", "aarch64" => "arm64", other => other, }; let intermediates_path = intermediates_path.join("deb"); util::create_clean_dir(&intermediates_path)?; let deb_base_name = format!("{}_{}_{}", config.main_binary_name()?, config.version, arch); let deb_name = format!("{deb_base_name}.deb"); let deb_dir = intermediates_path.join(&deb_base_name); let deb_path = config.out_dir().join(&deb_name); tracing::info!("Packaging {} ({})", deb_name, deb_path.display()); tracing::debug!("Generating data"); let data_dir = deb_dir.join("data"); let _ = generate_data(config, &data_dir)?; tracing::debug!("Copying files specified in `deb.files`"); if let Some(files) = config.deb().and_then(|d| d.files.as_ref()) { copy_custom_files(files, &data_dir)?; } let control_dir = deb_dir.join("control"); tracing::debug!("Generating control file"); generate_control_file(config, arch, &control_dir, &data_dir)?; tracing::debug!("Generating md5sums"); generate_md5sums(&control_dir, &data_dir)?; // Generate `debian-binary` file; see // http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66 tracing::debug!("Creating debian-binary file"); let debian_binary_path = deb_dir.join("debian-binary"); let mut file = util::create_file(&debian_binary_path)?; file.write_all(b"2.0\n")?; file.flush()?; // Apply tar/gzip/ar to create the final package file. tracing::debug!("Zipping control dir using tar and gzip"); let control_tar_gz_path = tar_and_gzip_dir(control_dir)?; tracing::debug!("Zipping data dir using tar and gzip"); let data_tar_gz_path = tar_and_gzip_dir(data_dir)?; tracing::debug!("Creating final archive: {}", deb_path.display()); create_archive( vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path], &deb_path, )?; Ok(vec![deb_path]) } ================================================ FILE: crates/packager/src/package/dmg/eula-resources-template.xml ================================================ LPic Attributes 0x0000 Data AAAAAgAAAAAAAAAAAAQAAA== ID 5000 Name STR# Attributes 0x0000 Data AAYNRW5nbGlzaCB0ZXN0MQVBZ3JlZQhEaXNhZ3JlZQVQcmludAdT YXZlLi4ueklmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0 aGlzIGxpY2Vuc2UsIGNsaWNrICJBZ3JlZSIgdG8gYWNjZXNzIHRo ZSBzb2Z0d2FyZS4gIElmIHlvdSBkbyBub3QgYWdyZWUsIHByZXNz ICJEaXNhZ3JlZS4i ID 5000 Name English buttons Attributes 0x0000 Data AAYHRW5nbGlzaAVBZ3JlZQhEaXNhZ3JlZQVQcmludAdTYXZlLi4u e0lmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0aGlzIGxp Y2Vuc2UsIHByZXNzICJBZ3JlZSIgdG8gaW5zdGFsbCB0aGUgc29m dHdhcmUuICBJZiB5b3UgZG8gbm90IGFncmVlLCBwcmVzcyAiRGlz YWdyZWUiLg== ID 5002 Name English ${EULA_FORMAT} Attributes 0x0000 Data ${EULA_DATA} ID 5000 Name English TMPL Attributes 0x0000 Data E0RlZmF1bHQgTGFuZ3VhZ2UgSUREV1JEBUNvdW50T0NOVAQqKioq TFNUQwtzeXMgbGFuZyBJRERXUkQebG9jYWwgcmVzIElEIChvZmZz ZXQgZnJvbSA1MDAwRFdSRBAyLWJ5dGUgbGFuZ3VhZ2U/RFdSRAQq KioqTFNURQ== ID 128 Name LPic styl Attributes 0x0000 Data AAMAAAAAAAwACQAUAAAAAAAAAAAAAAAAACcADAAJABQBAAAAAAAA AAAAAAAAKgAMAAkAFAAAAAAAAAAAAAA= ID 5000 Name English ================================================ FILE: crates/packager/src/package/dmg/mod.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{fs, os::unix::fs::PermissionsExt, path::PathBuf, process::Command}; use super::Context; use crate::{ codesign::macos as codesign, shell::CommandExt, util::{self, download}, Error, }; const CREATE_DMG_URL: &str = "https://raw.githubusercontent.com/create-dmg/create-dmg/28867ba3563ddef62f55dcf130677103b4296c42/create-dmg"; #[tracing::instrument(level = "trace", skip(ctx))] pub(crate) fn package(ctx: &Context) -> crate::Result> { let Context { config, tools_path, intermediates_path, .. } = ctx; let out_dir = config.out_dir(); let intermediates_path = intermediates_path.join("dmg"); util::create_clean_dir(&intermediates_path)?; let package_base_name = format!( "{}_{}_{}", config.product_name, config.version, match config.target_arch()? { "x86_64" => "x64", other => other, } ); let app_bundle_file_name = format!("{}.app", config.product_name); let dmg_name = format!("{}.dmg", &package_base_name); let dmg_path = out_dir.join(&dmg_name); tracing::info!("Packaging {} ({})", dmg_name, dmg_path.display()); if dmg_path.exists() { fs::remove_file(&dmg_path).map_err(|e| Error::IoWithPath(dmg_path.clone(), e))?; } let dmg_tools_path = tools_path.join("DMG"); let script_dir = dmg_tools_path.join("script"); fs::create_dir_all(&script_dir).map_err(|e| Error::IoWithPath(script_dir.clone(), e))?; let create_dmg_script_path = script_dir.join("create-dmg"); let support_directory_path = dmg_tools_path.join("share/create-dmg/support"); fs::create_dir_all(&support_directory_path) .map_err(|e| Error::IoWithPath(support_directory_path.clone(), e))?; if !dmg_tools_path.exists() { fs::create_dir_all(&dmg_tools_path) .map_err(|e| Error::IoWithPath(dmg_tools_path.clone(), e))?; } if !create_dmg_script_path.exists() { tracing::debug!("Downloading create-dmg script"); let data = download(CREATE_DMG_URL)?; tracing::debug!( "Writing {} and setting its permissions to 764", create_dmg_script_path.display() ); fs::write(&create_dmg_script_path, data) .map_err(|e| Error::IoWithPath(create_dmg_script_path.clone(), e))?; fs::set_permissions(&create_dmg_script_path, fs::Permissions::from_mode(0o764)) .map_err(|e| Error::IoWithPath(create_dmg_script_path.clone(), e))?; } tracing::debug!("Writing template.applescript"); let template_applescript = support_directory_path.join("template.applescript"); fs::write(&template_applescript, include_str!("template.applescript")) .map_err(|e| Error::IoWithPath(template_applescript, e))?; tracing::debug!("Writing eula-resources-template.xml"); let eula_template = support_directory_path.join("eula-resources-template.xml"); fs::write(&eula_template, include_str!("eula-resources-template.xml")) .map_err(|e| Error::IoWithPath(eula_template, e))?; let dmg = config.dmg(); let mut bundle_dmg_cmd = Command::new(&create_dmg_script_path); let app_x = dmg .and_then(|d| d.app_position) .map(|p| p.x) .unwrap_or(180) .to_string(); let app_y = dmg .and_then(|d| d.app_position) .map(|p| p.y) .unwrap_or(170) .to_string(); let app_folder_x = dmg .and_then(|d| d.app_folder_position) .map(|p| p.x) .unwrap_or(480) .to_string(); let app_folder_y = dmg .and_then(|d| d.app_folder_position) .map(|p| p.y) .unwrap_or(170) .to_string(); let window_width = dmg .and_then(|d| d.window_size) .map(|s| s.width) .unwrap_or(600) .to_string(); let window_height = dmg .and_then(|d| d.window_size) .map(|s| s.height) .unwrap_or(400) .to_string(); bundle_dmg_cmd.args([ "--volname", &config.product_name, "--icon", &app_bundle_file_name, &app_x, &app_y, "--app-drop-link", &app_folder_x, &app_folder_y, "--window-size", &window_width, &window_height, "--hide-extension", &app_bundle_file_name, ]); let window_position = dmg .and_then(|d| d.window_position) .map(|p| (p.x.to_string(), p.y.to_string())); if let Some((x, y)) = window_position { bundle_dmg_cmd.arg("--window-pos"); bundle_dmg_cmd.arg(&x); bundle_dmg_cmd.arg(&y); } let background_path = match &dmg.and_then(|d| d.background.as_ref()) { Some(p) => Some(std::env::current_dir()?.join(p)), None => None, }; if let Some(background_path) = &background_path { bundle_dmg_cmd.arg("--background"); bundle_dmg_cmd.arg(background_path); } tracing::debug!("Creating icns file"); let icns_icon_path = util::create_icns_file(&intermediates_path, config)?; if let Some(icon) = &icns_icon_path { bundle_dmg_cmd.arg("--volicon"); bundle_dmg_cmd.arg(icon); } let license_file = match config.license_file.as_ref() { Some(l) => Some(std::env::current_dir()?.join(l)), None => None, }; if let Some(license_path) = &license_file { bundle_dmg_cmd.arg("--eula"); bundle_dmg_cmd.arg(license_path); } // Issue #592 - Building MacOS dmg files on CI // https://github.com/tauri-apps/tauri/issues/592 if let Some(value) = std::env::var_os("CI") { if value == "true" { bundle_dmg_cmd.arg("--skip-jenkins"); } } tracing::debug!("Running create-dmg"); // execute the bundle script bundle_dmg_cmd .current_dir(&out_dir) .args(vec![dmg_name.as_str(), app_bundle_file_name.as_str()]) .output_ok() .map_err(crate::Error::CreateDmgFailed)?; // Sign DMG if needed if let Some(identity) = &config .macos() .and_then(|macos| macos.signing_identity.as_ref()) { tracing::debug!("Codesigning {}", dmg_path.display()); codesign::try_sign( vec![codesign::SignTarget { path: dmg_path.clone(), is_native_binary: false, }], identity, config, )?; } Ok(vec![dmg_path]) } ================================================ FILE: crates/packager/src/package/dmg/template.applescript ================================================ on run (volumeName) tell application "Finder" tell disk (volumeName as string) open set theXOrigin to WINX set theYOrigin to WINY set theWidth to WINW set theHeight to WINH set theBottomRightX to (theXOrigin + theWidth) set theBottomRightY to (theYOrigin + theHeight) set dsStore to "\"" & "/Volumes/" & volumeName & "/" & ".DS_STORE\"" tell container window set current view to icon view set toolbar visible to false set statusbar visible to false set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY} set statusbar visible to false REPOSITION_HIDDEN_FILES_CLAUSE end tell set opts to the icon view options of container window tell opts set icon size to ICON_SIZE set text size to TEXT_SIZE set arrangement to not arranged end tell BACKGROUND_CLAUSE -- Positioning POSITION_CLAUSE -- Hiding HIDING_CLAUSE -- Application and QL Link Clauses APPLICATION_CLAUSE QL_CLAUSE close open -- Force saving of the size delay 1 tell container window set statusbar visible to false set the bounds to {theXOrigin, theYOrigin, theBottomRightX - 10, theBottomRightY - 10} end tell end tell delay 1 tell disk (volumeName as string) tell container window set statusbar visible to false set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY} end tell end tell --give the finder some time to write the .DS_Store file delay 3 set waitTime to 0 set ejectMe to false repeat while ejectMe is false delay 1 set waitTime to waitTime + 1 if (do shell script "[ -f " & dsStore & " ]; echo $?") = "0" then set ejectMe to true end repeat log "waited " & waitTime & " seconds for .DS_STORE to be created." end tell end run ================================================ FILE: crates/packager/src/package/mod.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::path::PathBuf; use crate::{config, shell::CommandExt, util, Config, PackageFormat}; use self::context::Context; mod app; #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] mod appimage; #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] mod deb; #[cfg(target_os = "macos")] mod dmg; mod nsis; #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] mod pacman; #[cfg(windows)] mod wix; mod context; /// Generated Package metadata. #[derive(Debug, Clone)] #[non_exhaustive] pub struct PackageOutput { /// The package type. pub format: PackageFormat, /// All paths for this package. pub paths: Vec, } impl PackageOutput { /// Creates a new package output. /// /// This is only useful if you need to sign the packages in a different process, /// after packaging the app and storing its paths. pub fn new(format: PackageFormat, paths: Vec) -> Self { Self { format, paths } } } /// Package an app using the specified config. #[tracing::instrument(level = "trace", skip(config))] pub fn package(config: &Config) -> crate::Result> { let mut formats = config .formats .clone() .unwrap_or_else(|| PackageFormat::platform_default().to_vec()); if formats.is_empty() { return Ok(Vec::new()); } if formats.contains(&PackageFormat::Default) { formats = PackageFormat::platform_default().to_vec(); } if formats.contains(&PackageFormat::All) { formats = PackageFormat::platform_all().to_vec(); } formats.sort_by_key(|f| f.priority()); let formats_comma_separated = formats .iter() .map(|f| f.short_name()) .collect::>() .join(","); run_before_packaging_command_hook(config, &formats_comma_separated)?; let ctx = Context::new(config)?; tracing::trace!(ctx = ?ctx); let mut packages = Vec::new(); for format in &formats { run_before_each_packaging_command_hook( config, &formats_comma_separated, format.short_name(), )?; let paths = match format { PackageFormat::App => app::package(&ctx), #[cfg(target_os = "macos")] PackageFormat::Dmg => { // PackageFormat::App is required for the DMG bundle if !packages .iter() .any(|b: &PackageOutput| b.format == PackageFormat::App) { let paths = app::package(&ctx)?; packages.push(PackageOutput { format: PackageFormat::App, paths, }); } dmg::package(&ctx) } #[cfg(target_os = "windows")] PackageFormat::Wix => wix::package(&ctx), PackageFormat::Nsis => nsis::package(&ctx), #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::Deb => deb::package(&ctx), #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::AppImage => appimage::package(&ctx), #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::Pacman => pacman::package(&ctx), _ => { tracing::warn!("ignoring {}", format.short_name()); continue; } }?; packages.push(PackageOutput { format: *format, paths, }); } #[cfg(target_os = "macos")] { // Clean up .app if only building dmg if !formats.contains(&PackageFormat::App) { if let Some(app_bundle_paths) = packages .iter() .position(|b| b.format == PackageFormat::App) .map(|i| packages.remove(i)) .map(|b| b.paths) { for p in &app_bundle_paths { use crate::Error; use std::fs; tracing::debug!("Cleaning {}", p.display()); match p.is_dir() { true => { fs::remove_dir_all(p).map_err(|e| Error::IoWithPath(p.clone(), e))? } false => fs::remove_file(p).map_err(|e| Error::IoWithPath(p.clone(), e))?, }; } } } } Ok(packages) } fn run_before_each_packaging_command_hook( config: &Config, formats_comma_separated: &str, format: &str, ) -> crate::Result<()> { if let Some(hook) = &config.before_each_package_command { let (mut cmd, script) = match hook { config::HookCommand::Script(script) => { let cmd = util::cross_command(script); (cmd, script) } config::HookCommand::ScriptWithOptions { script, dir } => { let mut cmd = util::cross_command(script); if let Some(dir) = dir { cmd.current_dir(dir); } (cmd, script) } }; tracing::info!("Running beforeEachPackageCommand [{format}] `{script}`"); let output = cmd .env("CARGO_PACKAGER_FORMATS", formats_comma_separated) .env("CARGO_PACKAGER_FORMAT", format) .output_ok_info() .map_err(|e| { crate::Error::HookCommandFailure( "beforeEachPackageCommand".into(), script.into(), e, ) })?; if !output.status.success() { return Err(crate::Error::HookCommandFailureWithExitCode( "beforeEachPackageCommand".into(), script.into(), output.status.code().unwrap_or_default(), )); } } Ok(()) } fn run_before_packaging_command_hook( config: &Config, formats_comma_separated: &str, ) -> crate::Result<()> { if let Some(hook) = &config.before_packaging_command { let (mut cmd, script) = match hook { config::HookCommand::Script(script) => { let cmd = util::cross_command(script); (cmd, script) } config::HookCommand::ScriptWithOptions { script, dir } => { let mut cmd = util::cross_command(script); if let Some(dir) = dir { cmd.current_dir(dir); } (cmd, script) } }; tracing::info!("Running beforePackageCommand `{script}`"); let output = cmd .env("CARGO_PACKAGER_FORMATS", formats_comma_separated) .output_ok_info() .map_err(|e| { crate::Error::HookCommandFailure("beforePackagingCommand".into(), script.into(), e) })?; if !output.status.success() { return Err(crate::Error::HookCommandFailureWithExitCode( "beforePackagingCommand".into(), script.into(), output.status.code().unwrap_or_default(), )); } } Ok(()) } ================================================ FILE: crates/packager/src/package/nsis/FileAssociation.nsh ================================================ ; from https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b ; fileassoc.nsh ; File association helper macros ; Written by Saivert ; ; Improved by Nikku. ; ; Features automatic backup system and UPDATEFILEASSOC macro for ; shell change notification. ; ; |> How to use <| ; To associate a file with an application so you can double-click it in explorer, use ; the APP_ASSOCIATE macro like this: ; ; Example: ; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \ ; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\"" ; ; Never insert the APP_ASSOCIATE macro multiple times, it is only ment ; to associate an application with a single file and using the ; the "open" verb as default. To add more verbs (actions) to a file ; use the APP_ASSOCIATE_ADDVERB macro. ; ; Example: ; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \ ; "$INSTDIR\myapp.exe /edit $\"%1$\"" ; ; To have access to more options when registering the file association use the ; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the ; standard action (default verb). ; ; Note, that this script takes into account user versus global installs. ; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext. ; ; And finally: To remove the association from the registry use the APP_UNASSOCIATE ; macro. Here is another example just to wrap it up: ; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile" ; ; |> Note <| ; When defining your file class string always use the short form of your application title ; then a period (dot) and the type of file. This keeps the file class sort of unique. ; Examples: ; Winamp.Playlist ; NSIS.Script ; Photoshop.JPEGFile ; ; |> Tech info <| ; The registry key layout for a global file association is: ; ; HKEY_LOCAL_MACHINE\Software\Classes ; <".ext"> = ; = <"description"> ; shell ; = <"menu-item text"> ; command = <"command string"> ; ; ; The registry key layout for a per-user file association is: ; ; HKEY_CURRENT_USER\Software\Classes ; <".ext"> = ; = <"description"> ; shell ; = <"menu-item text"> ; command = <"command string"> ; !macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND ; Backup the previously associated file class ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` !macroend !macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND ; Backup the previously associated file class ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" StrCmp "${SHELLNEW}" "0" +2 WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" "" WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` !macroend !macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}` WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}` !macroend !macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}` !macroend !macro APP_UNASSOCIATE EXT FILECLASS ; Backup the previously associated file class ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` !macroend !macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" "" !macroend ; !defines for use with SHChangeNotify !ifdef SHCNE_ASSOCCHANGED !undef SHCNE_ASSOCCHANGED !endif !define SHCNE_ASSOCCHANGED 0x08000000 !ifdef SHCNF_FLUSH !undef SHCNF_FLUSH !endif !define SHCNF_FLUSH 0x1000 !macro UPDATEFILEASSOC ; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we ; can update the shell. System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)" !macroend ================================================ FILE: crates/packager/src/package/nsis/installer.nsi ================================================ ; Set the compression algorithm. !if "{{compression}}" == "" SetCompressor /SOLID lzma !else SetCompressor /SOLID "{{compression}}" !endif Unicode true !include MUI2.nsh !include FileFunc.nsh !include x64.nsh !include WordFunc.nsh !include "FileAssociation.nsh" !include "StrFunc.nsh" !include "StrFunc.nsh" ${StrCase} ${StrLoc} !define MANUFACTURER "{{manufacturer}}" !define PRODUCTNAME "{{product_name}}" !define VERSION "{{version}}" !define VERSIONWITHBUILD "{{version_with_build}}" !define SHORTDESCRIPTION "{{short_description}}" !define INSTALLMODE "{{install_mode}}" !define LICENSE "{{license}}" !define INSTALLERICON "{{installer_icon}}" !define SIDEBARIMAGE "{{sidebar_image}}" !define HEADERIMAGE "{{header_image}}" !define MAINBINARYNAME "{{main_binary_name}}" !define MAINBINARYSRCPATH "{{main_binary_path}}" !define IDENTIFIER "{{identifier}}" !define COPYRIGHT "{{copyright}}" !define OUTFILE "{{out_file}}" !define ARCH "{{arch}}" !define PLUGINSPATH "{{additional_plugins_path}}" !define ALLOWDOWNGRADES "{{allow_downgrades}}" !define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}" !define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}" !define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}" !define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}" !define ESTIMATEDSIZE "{{estimated_size}}" Name "${PRODUCTNAME}" BrandingText "${COPYRIGHT}" OutFile "${OUTFILE}" VIProductVersion "${VERSIONWITHBUILD}" VIAddVersionKey "ProductName" "${PRODUCTNAME}" VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}" VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" VIAddVersionKey "FileVersion" "${VERSION}" VIAddVersionKey "ProductVersion" "${VERSION}" ; Plugins path, currently exists for linux only !if "${PLUGINSPATH}" != "" !addplugindir "${PLUGINSPATH}" !endif !if "${UNINSTALLERSIGNCOMMAND}" != "" !uninstfinalize '${UNINSTALLERSIGNCOMMAND}' !endif ; Handle install mode, `perUser`, `perMachine` or `both` !if "${INSTALLMODE}" == "perMachine" RequestExecutionLevel highest !endif !if "${INSTALLMODE}" == "currentUser" RequestExecutionLevel user !endif !if "${INSTALLMODE}" == "both" !define MULTIUSER_MUI !define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}" !define MULTIUSER_INSTALLMODE_COMMANDLINE !if "${ARCH}" == "x64" !define MULTIUSER_USE_PROGRAMFILES64 !else if "${ARCH}" == "arm64" !define MULTIUSER_USE_PROGRAMFILES64 !endif !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}" !define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser" !define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME !define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation !define MULTIUSER_EXECUTIONLEVEL Highest !include MultiUser.nsh !endif ; installer icon !if "${INSTALLERICON}" != "" !define MUI_ICON "${INSTALLERICON}" !endif ; installer sidebar image !if "${SIDEBARIMAGE}" != "" !define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}" !endif ; installer header image !if "${HEADERIMAGE}" != "" !define MUI_HEADERIMAGE !define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}" !endif ; Define registry key to store installer language !define MUI_LANGDLL_REGISTRY_ROOT "HKCU" !define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}" !define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language" ; Installer pages, must be ordered as they appear ; 1. Welcome Page !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_WELCOME ; 2. License Page (if defined) !if "${LICENSE}" != "" !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_LICENSE "${LICENSE}" !endif ; 3. Install mode (if it is set to `both`) !if "${INSTALLMODE}" == "both" !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MULTIUSER_PAGE_INSTALLMODE !endif ; 4. Custom page to ask user if he wants to reinstall/uninstall ; only if a previous installtion was detected Var ReinstallPageCheck Page custom PageReinstall PageLeaveReinstall Function PageReinstall ; Uninstall previous WiX installation if exists. ; ; A WiX installer stores the isntallation info in registry ; using a UUID and so we have to loop through all keys under ; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall` ; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER} ; ; This has a potentional issue that there maybe another installation that matches ; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer, ; however, this should be fine since the user will have to confirm the uninstallation ; and they can chose to abort it if doesn't make sense. StrCpy $0 0 wix_loop: EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0 StrCmp $1 "" wix_done ; Exit loop if there is no more keys to loop on IntOp $0 $0 + 1 ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName" ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher" StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString" ${StrCase} $R1 $R0 "L" ${StrLoc} $R0 $R1 "msiexec" ">" StrCmp $R0 0 0 wix_done StrCpy $R7 "wix" StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" Goto compare_version wix_done: ; Check if there is an existing installation, if not, abort the reinstall page ReadRegStr $R0 SHCTX "${UNINSTKEY}" "" ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" ${IfThen} "$R0$R1" == "" ${|} Abort ${|} ; Compare this installar version with the existing installation ; and modify the messages presented to the user accordingly compare_version: StrCpy $R4 "$(older)" ${If} $R7 == "wix" ReadRegStr $R0 HKLM "$R6" "DisplayVersion" ${Else} ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion" ${EndIf} ${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|} nsis_tauri_utils::SemverCompare "${VERSION}" $R0 Pop $R0 ; Reinstalling the same version ${If} $R0 == 0 StrCpy $R1 "$(alreadyInstalledLong)" StrCpy $R2 "$(addOrReinstall)" StrCpy $R3 "$(uninstallApp)" !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)" StrCpy $R5 "2" ; Upgrading ${ElseIf} $R0 == 1 StrCpy $R1 "$(olderOrUnknownVersionInstalled)" StrCpy $R2 "$(uninstallBeforeInstalling)" StrCpy $R3 "$(dontUninstall)" !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" StrCpy $R5 "1" ; Downgrading ${ElseIf} $R0 == -1 StrCpy $R1 "$(newerVersionInstalled)" StrCpy $R2 "$(uninstallBeforeInstalling)" !if "${ALLOWDOWNGRADES}" == "true" StrCpy $R3 "$(dontUninstall)" !else StrCpy $R3 "$(dontUninstallDowngrade)" !endif !insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)" StrCpy $R5 "1" ${Else} Abort ${EndIf} Call SkipIfPassive nsDialogs::Create 1018 Pop $R4 ${IfThen} $(^RTL) == 1 ${|} nsDialogs::SetRTL $(^RTL) ${|} ${NSD_CreateLabel} 0 0 100% 24u $R1 Pop $R1 ${NSD_CreateRadioButton} 30u 50u -30u 8u $R2 Pop $R2 ${NSD_OnClick} $R2 PageReinstallUpdateSelection ${NSD_CreateRadioButton} 30u 70u -30u 8u $R3 Pop $R3 ; disable this radio button if downgrading and downgrades are disabled !if "${ALLOWDOWNGRADES}" == "false" ${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|} !endif ${NSD_OnClick} $R3 PageReinstallUpdateSelection ; Check the first radio button if this the first time ; we enter this page or if the second button wasn't ; selected the last time we were on this page ${If} $ReinstallPageCheck != 2 SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0 ${Else} SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0 ${EndIf} ${NSD_SetFocus} $R2 nsDialogs::Show FunctionEnd Function PageReinstallUpdateSelection ${NSD_GetState} $R2 $R1 ${If} $R1 == ${BST_CHECKED} StrCpy $ReinstallPageCheck 1 ${Else} StrCpy $ReinstallPageCheck 2 ${EndIf} FunctionEnd Function PageLeaveReinstall ${NSD_GetState} $R2 $R1 ; $R5 holds whether we are reinstalling the same version or not ; $R5 == "1" -> different versions ; $R5 == "2" -> same version ; ; $R1 holds the radio buttons state. its meaning is dependant on the context StrCmp $R5 "1" 0 +2 ; Existing install is not the same version? StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling reinst_uninstall: HideWindow ClearErrors ${If} $R7 == "wix" ReadRegStr $R1 HKLM "$R6" "UninstallString" ExecWait '$R1' $0 ${Else} ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString" ExecWait '$R1 /P _?=$4' $0 ${EndIf} BringToFront ${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code ${If} $0 <> 0 ${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe" ${If} $0 = 1 ; User aborted uninstaller? StrCmp $R5 "2" 0 +2 ; Is the existing install the same version? Quit ; ...yes, already installed, we are done Abort ${EndIf} MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)" Abort ${Else} StrCpy $0 $R1 1 ${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString Delete $R1 RMDir $INSTDIR ${EndIf} reinst_done: FunctionEnd ; 5. Choose install directoy page !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_DIRECTORY ; 6. Start menu shortcut page !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive Var AppStartMenuFolder !insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder ; 7. Installation page !insertmacro MUI_PAGE_INSTFILES ; 8. Finish page ; ; Don't auto jump to finish page after installation page, ; because the installation page has useful info that can be used debug any issues with the installer. !define MUI_FINISHPAGE_NOAUTOCLOSE ; Use show readme button in the finish page as a button create a desktop shortcut !define MUI_FINISHPAGE_SHOWREADME !define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)" !define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut ; Show run app after installation. !define MUI_FINISHPAGE_RUN "$INSTDIR\${MAINBINARYNAME}.exe" !define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive !insertmacro MUI_PAGE_FINISH ; Uninstaller Pages ; 1. Confirm uninstall page {{#if appdata_paths}} Var DeleteAppDataCheckbox Var DeleteAppDataCheckboxState !define /ifndef WS_EX_LAYOUTRTL 0x00400000 !define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow Function un.ConfirmShow FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog ${If} $(^RTL) == 1 System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s' ${Else} System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 0,i 100,i 400, i 25,i$1,i0,i0,i0)i.s' ${EndIf} Pop $DeleteAppDataCheckbox SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1 SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1 FunctionEnd !define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave Function un.ConfirmLeave SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState FunctionEnd {{/if}} !insertmacro MUI_UNPAGE_CONFIRM ; 2. Uninstalling Page !insertmacro MUI_UNPAGE_INSTFILES ;Languages {{#each languages}} !insertmacro MUI_LANGUAGE "{{this}}" {{/each}} !insertmacro MUI_RESERVEFILE_LANGDLL {{#each language_files}} !include "{{this}}" {{/each}} !macro SetContext !if "${INSTALLMODE}" == "currentUser" SetShellVarContext current !else if "${INSTALLMODE}" == "perMachine" SetShellVarContext all !endif ${If} ${RunningX64} !if "${ARCH}" == "x64" SetRegView 64 !else if "${ARCH}" == "arm64" SetRegView 64 !else SetRegView 32 !endif ${EndIf} !macroend Var PassiveMode Function .onInit ${GetOptions} $CMDLINE "/P" $PassiveMode IfErrors +2 0 StrCpy $PassiveMode 1 !if "${DISPLAYLANGUAGESELECTOR}" == "true" !insertmacro MUI_LANGDLL_DISPLAY !endif !insertmacro SetContext ${If} $INSTDIR == "" ; Set default install location !if "${INSTALLMODE}" == "perMachine" ${If} ${RunningX64} !if "${ARCH}" == "x64" StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" !else if "${ARCH}" == "arm64" StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}" !else StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" !endif ${Else} StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}" ${EndIf} !else if "${INSTALLMODE}" == "currentUser" StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}" !endif Call RestorePreviousInstallLocation ${EndIf} !if "${INSTALLMODE}" == "both" !insertmacro MULTIUSER_INIT !endif FunctionEnd Section EarlyChecks ; Abort silent installer if downgrades is disabled !if "${ALLOWDOWNGRADES}" == "false" IfSilent 0 silent_downgrades_done ; If downgrading ${If} $R0 == -1 System::Call 'kernel32::AttachConsole(i -1)i.r0' ${If} $0 != 0 System::Call 'kernel32::GetStdHandle(i -11)i.r0' System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color FileWrite $0 "$(silentDowngrades)" ${EndIf} Abort ${EndIf} silent_downgrades_done: !endif SectionEnd {{#if preinstall_section}} {{unescape_newlines preinstall_section}} {{/if}} !macro CheckIfAppIsRunning nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe" Pop $R0 ${If} $R0 = 0 IfSilent kill 0 ${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|} kill: nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe" Pop $R0 Sleep 500 ${If} $R0 = 0 Goto app_check_done ${Else} IfSilent silent ui silent: System::Call 'kernel32::AttachConsole(i -1)i.r0' ${If} $0 != 0 System::Call 'kernel32::GetStdHandle(i -11)i.r0' System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color FileWrite $0 "$(appRunning)$\n" ${EndIf} Abort ui: Abort "$(failedToKillApp)" ${EndIf} cancel: Abort "$(appRunning)" ${EndIf} app_check_done: !macroend Section Install SetOutPath $INSTDIR !insertmacro CheckIfAppIsRunning ; Copy main executable File "${MAINBINARYSRCPATH}" ; Create resources directory structure {{#each resources_dirs}} CreateDirectory "$INSTDIR\\{{this}}" {{/each}} ; Copy resources {{#each resources}} File /a "/oname={{this}}" "{{@key}}" {{/each}} ; Copy external binaries {{#each binaries}} File /a "/oname={{this}}" "{{@key}}" {{/each}} ; Create file associations {{#each file_associations as |association| ~}} {{#each association.extensions as |ext| ~}} !insertmacro APP_ASSOCIATE "{{ext}}" "{{or association.name ext}}" "{{association-description association.description ext}}" "$INSTDIR\${MAINBINARYNAME}.exe,0" "Open with ${PRODUCTNAME}" "$INSTDIR\${MAINBINARYNAME}.exe $\"%1$\"" {{/each}} {{/each}} ; Register deep links {{#each deep_link_protocols as |protocol| ~}} WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "URL Protocol" "" WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "" "URL:${BUNDLEID} protocol" WriteRegStr SHCTX "Software\Classes\\{{protocol}}\DefaultIcon" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0" WriteRegStr SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" {{/each}} ; Create uninstaller WriteUninstaller "$INSTDIR\uninstall.exe" ; Save $INSTDIR in registry for future installations WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR !if "${INSTALLMODE}" == "both" ; Save install mode to be selected by default for the next installation such as updating ; or when uninstalling WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1 !endif ; Registry information for add/remove programs WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}" WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\"" WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}" WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}" WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\"" WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1" WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1" WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "${ESTIMATEDSIZE}" ; Create start menu shortcut (GUI) !insertmacro MUI_STARTMENU_WRITE_BEGIN Application Call CreateStartMenuShortcut !insertmacro MUI_STARTMENU_WRITE_END ; Create shortcuts for silent and passive installers, which ; can be disabled by passing `/NS` flag ; GUI installer has buttons for users to control creating them IfSilent check_ns_flag 0 ${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|} Goto shortcuts_done check_ns_flag: ${GetOptions} $CMDLINE "/NS" $R0 IfErrors 0 shortcuts_done Call CreateDesktopShortcut Call CreateStartMenuShortcut shortcuts_done: ; Auto close this page for passive mode ${IfThen} $PassiveMode == 1 ${|} SetAutoClose true ${|} SectionEnd Function .onInstSuccess ; Check for `/R` flag only in silent and passive installers because ; GUI installer has a toggle for the user to (re)start the app IfSilent check_r_flag 0 ${IfThen} $PassiveMode == 1 ${|} Goto check_r_flag ${|} Goto run_done check_r_flag: ${GetOptions} $CMDLINE "/R" $R0 IfErrors run_done 0 Exec '"$INSTDIR\${MAINBINARYNAME}.exe"' run_done: FunctionEnd Function un.onInit !insertmacro SetContext !if "${INSTALLMODE}" == "both" !insertmacro MULTIUSER_UNINIT !endif !insertmacro MUI_UNGETLANGUAGE FunctionEnd Section Uninstall !insertmacro CheckIfAppIsRunning ; Delete the app directory and its content from disk ; Copy main executable Delete "$INSTDIR\${MAINBINARYNAME}.exe" ; Delete resources {{#each resources}} Delete "$INSTDIR\\{{this}}" {{/each}} ; Delete external binaries {{#each binaries}} Delete "$INSTDIR\\{{this}}" {{/each}} ; Delete app associations {{#each file_associations as |association| ~}} {{#each association.ext as |ext| ~}} !insertmacro APP_UNASSOCIATE "{{ext}}" "{{or association.name ext}}" {{/each}} {{/each}} ; Delete deep links {{#each deep_link_protocols as |protocol| ~}} ReadRegStr $R7 SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" !if $R7 == "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\"" DeleteRegKey SHCTX "Software\Classes\\{{protocol}}" !endif {{/each}} ; Delete uninstaller Delete "$INSTDIR\uninstall.exe" {{#each resources_dirs}} RMDir /REBOOTOK "$INSTDIR\\{{this}}" {{/each}} RMDir "$INSTDIR" ; Remove start menu shortcut !insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" RMDir "$SMPROGRAMS\$AppStartMenuFolder" ; Remove desktop shortcuts Delete "$DESKTOP\${PRODUCTNAME}.lnk" ; Remove registry information for add/remove programs !if "${INSTALLMODE}" == "both" DeleteRegKey SHCTX "${UNINSTKEY}" !else if "${INSTALLMODE}" == "perMachine" DeleteRegKey HKLM "${UNINSTKEY}" !else DeleteRegKey HKCU "${UNINSTKEY}" !endif DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language" ; Delete app data {{#if appdata_paths}} ${If} $DeleteAppDataCheckboxState == 1 SetShellVarContext current {{#each appdata_paths}} RmDir /r "{{unescape_dollar_sign this}}" {{/each}} ${EndIf} {{/if}} ${GetOptions} $CMDLINE "/P" $R0 IfErrors +2 0 SetAutoClose true SectionEnd Function RestorePreviousInstallLocation ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" "" StrCmp $4 "" +2 0 StrCpy $INSTDIR $4 FunctionEnd Function SkipIfPassive ${IfThen} $PassiveMode == 1 ${|} Abort ${|} FunctionEnd Function CreateDesktopShortcut CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" ApplicationID::Set "$DESKTOP\${PRODUCTNAME}.lnk" "${IDENTIFIER}" FunctionEnd Function CreateStartMenuShortcut CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder" CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe" ApplicationID::Set "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "${IDENTIFIER}" FunctionEnd ================================================ FILE: crates/packager/src/package/nsis/languages/Arabic.nsh ================================================ LangString addOrReinstall ${LANG_ARABIC} "إضافة أو إزالة المكونات" LangString alreadyInstalled ${LANG_ARABIC} "التطبيق مثبت بالفعل" LangString alreadyInstalledLong ${LANG_ARABIC} "${PRODUCTNAME} ${VERSION} مثبت بالفعل. قم باختيار العملية التى تريدها ثم اضغط على التالى." LangString appRunning ${LANG_ARABIC} "${PRODUCTNAME} مازال يعمل! من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى." LangString appRunningOkKill ${LANG_ARABIC} "${PRODUCTNAME} مازال يعمل!$\nاضغط OK لإغلاقه" LangString chooseMaintenanceOption ${LANG_ARABIC} "قم باختيار نوع الصيانة التى تريدها." LangString choowHowToInstall ${LANG_ARABIC} "قم باختيار طريقة تنصيب ${PRODUCTNAME}." LangString createDesktop ${LANG_ARABIC} "اضف اختصار على سطح المكتب" LangString dontUninstall ${LANG_ARABIC} "عدم إزالة" LangString dontUninstallDowngrade ${LANG_ARABIC} "عدم إزالة (التخفيض بدون إزالة غير مسموح لهذا المثبت)" LangString failedToKillApp ${LANG_ARABIC} "فشل فى غلف ${PRODUCTNAME}. من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى." LangString installingWebview2 ${LANG_ARABIC} "تنصيب WebView2..." LangString newerVersionInstalled ${LANG_ARABIC} "يوجد نسخة جديدة من ${PRODUCTNAME} مثبتة بالغعل! لا ينصح بتنصيب نسخة اقدم من النسخة الحالية. اذا مازلت ترغب فى تنصيب النسخة الأقدم، فينصح بإزالة النسخة الحالية أولاً. قم باختيار العملية التى تريدها ثم اضغط على التالى للاستمرار." LangString older ${LANG_ARABIC} "أقدم" LangString olderOrUnknownVersionInstalled ${LANG_ARABIC} "نسخة $R4 من ${PRODUCTNAME} مثبتة بالفعل على نظامك. ينصح بإزالة النسخة الحالية قبل التنصيب. قم باختيار العملية التى تريدها ثم اضغط على التالى للاستمرار." LangString silentDowngrades ${LANG_ARABIC} "التخفيض غير مسموح لهذا المثبت ولا يمكن الاستمرار فى الوضع الصامت. من فضلك، قم باستخدام الواجهة الرسومية." LangString unableToUninstall ${LANG_ARABIC} "غير قادر على الإزالة!" LangString uninstallApp ${LANG_ARABIC} "إزالة ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_ARABIC} "قم بإزالة التطبيق قبل التثبيت" LangString unknown ${LANG_ARABIC} "غير معروفة" LangString deleteAppData ${LANG_ARABIC} "مسح بيانات التطبيق" ================================================ FILE: crates/packager/src/package/nsis/languages/Bulgarian.nsh ================================================ LangString addOrReinstall ${LANG_BULGARIAN} "Добавяне/Преинсталиране на компоненти" LangString alreadyInstalled ${LANG_BULGARIAN} "Вече инсталиран" LangString alreadyInstalledLong ${LANG_BULGARIAN} "${PRODUCTNAME} ${VERSION} е вече е инсталиран. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите." LangString appRunning ${LANG_BULGARIAN} "${PRODUCTNAME} е отворен! Моля, затворете го първо и опитайте отново." LangString appRunningOkKill ${LANG_BULGARIAN} "${PRODUCTNAME} е отворен!$\nНатиснете ОК, за да го затворите." LangString chooseMaintenanceOption ${LANG_BULGARIAN} "Изберете опция за поддръжка." LangString choowHowToInstall ${LANG_BULGARIAN} "Изберете как искате да инсталирате ${PRODUCTNAME}." LangString createDesktop ${LANG_BULGARIAN} "Създайте пряк път на работния плот" LangString dontUninstall ${LANG_BULGARIAN} "Не деинсталирайте" LangString dontUninstallDowngrade ${LANG_BULGARIAN} "Не деинсталирайте (Понижаването без деинсталация е забранено за този инсталатор)" LangString failedToKillApp ${LANG_BULGARIAN} "Неуспешно прекратяване на ${PRODUCTNAME}. Моля, затворете го първо и опитайте отново." LangString installingWebview2 ${LANG_BULGARIAN} "Инсталиране на WebView2..." LangString newerVersionInstalled ${LANG_BULGARIAN} "Вече е инсталирана по-нова версия на ${PRODUCTNAME}! Не се препоръчва да инсталирате по-стара версия. Ако наистина желаете да инсталирате тази по-стара версия, по-добре е да деинсталирате текущата версия първо. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите." LangString older ${LANG_BULGARIAN} "по-стара" LangString olderOrUnknownVersionInstalled ${LANG_BULGARIAN} "На вашата система е инсталирана $R4 версия на ${PRODUCTNAME}. Препоръчително е да деинсталирате текущата версия преди да инсталирате нова. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите." LangString silentDowngrades ${LANG_BULGARIAN} "Понижаванията не са позволени за този инсталатор. Не е възможно да продължите с безшумен инсталатор. Моля, използвайте графичния интерфейсен инсталатор." LangString unableToUninstall ${LANG_BULGARIAN} "Неуспешна деинсталация!" LangString uninstallApp ${LANG_BULGARIAN} "Деинсталиране на ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_BULGARIAN} "Деинсталирай преди инсталиране" LangString unknown ${LANG_BULGARIAN} "неизвестно" LangString deleteAppData ${LANG_BULGARIAN} "Изтриване на данните на приложението" ================================================ FILE: crates/packager/src/package/nsis/languages/Dutch.nsh ================================================ LangString addOrReinstall ${LANG_DUTCH} "(Her)installeer componenten" LangString alreadyInstalled ${LANG_DUTCH} "Al geïnstalleerd" LangString alreadyInstalledLong ${LANG_DUTCH} "${PRODUCTNAME} ${VERSION} is al geïnstalleerd. Kies een van de volgende opties en klik op Volgende om door te gaan." LangString appRunning ${LANG_DUTCH} "${PRODUCTNAME} is geopend! Sluit het programma eerst en probeer het dan opnieuw." LangString appRunningOkKill ${LANG_DUTCH} "${PRODUCTNAME} is geopend!$\nKlik op OK om het te stoppen." LangString chooseMaintenanceOption ${LANG_DUTCH} "Kies de onderhoudsoptie die u wilt uitvoeren." LangString choowHowToInstall ${LANG_DUTCH} "Kies hoe u ${PRODUCTNAME} wilt installeren." LangString createDesktop ${LANG_DUTCH} "Maak een snelkoppeling aan op het bureaublad" LangString dontUninstall ${LANG_DUTCH} "Deïnstalleer niet" LangString dontUninstallDowngrade ${LANG_DUTCH} "Deïnstalleer niet (Downgraden zonder deïnstalleren is uitgeschakeld voor deze installer)" LangString failedToKillApp ${LANG_DUTCH} "Het is niet gelukt ${PRODUCTNAME} te stoppen. Sluit het eerst zelf en probeer het dan nog een keer" LangString installingWebview2 ${LANG_DUTCH} "WebView2 wordt geïnstalleerd..." LangString newerVersionInstalled ${LANG_DUTCH} "Een nieuwere versie van ${PRODUCTNAME} is al geïnstalleerd! Het word niet aangeraden om een oudere versie te installeren. Als u echt deze oudere versie wilt installeren, kunt u beter de huidige versie eerst deïnstalleren. Kies een van de volgende opties en klik op Volgende om door te gaan." LangString older ${LANG_DUTCH} "oudere" LangString olderOrUnknownVersionInstalled ${LANG_DUTCH} "Een $R4 versie van ${PRODUCTNAME} is al geïnstalleerd. Het word aangeraden om de huidige versie eerst te deïnstalleren. Kies een van de volgende opties en klik op Volgende om door te gaan." LangString silentDowngrades ${LANG_DUTCH} "Downgrades zijn uitgeschakeld voor deze installer, de stille installatie kan niet worden voltooid, gebruik a.u.b. de grafische installatie methode.$\n" LangString unableToUninstall ${LANG_DUTCH} "De installatie kan niet ongedaan worden gemaakt." LangString uninstallApp ${LANG_DUTCH} "Deïnstalleer ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_DUTCH} "Deïnstalleer voor installatie" LangString unknown ${LANG_DUTCH} "onbekende" LangString deleteAppData ${LANG_DUTCH} "Verwijder de data van de applicatie" ================================================ FILE: crates/packager/src/package/nsis/languages/English.nsh ================================================ LangString addOrReinstall ${LANG_ENGLISH} "Add/Reinstall components" LangString alreadyInstalled ${LANG_ENGLISH} "Already Installed" LangString alreadyInstalledLong ${LANG_ENGLISH} "${PRODUCTNAME} ${VERSION} is already installed. Select the operation you want to perform and click Next to continue." LangString appRunning ${LANG_ENGLISH} "${PRODUCTNAME} is running! Please close it first then try again." LangString appRunningOkKill ${LANG_ENGLISH} "${PRODUCTNAME} is running!$\nClick OK to kill it" LangString chooseMaintenanceOption ${LANG_ENGLISH} "Choose the maintenance option to perform." LangString choowHowToInstall ${LANG_ENGLISH} "Choose how you want to install ${PRODUCTNAME}." LangString createDesktop ${LANG_ENGLISH} "Create desktop shortcut" LangString dontUninstall ${LANG_ENGLISH} "Do not uninstall" LangString dontUninstallDowngrade ${LANG_ENGLISH} "Do not uninstall (Downgrading without uninstall is disabled for this installer)" LangString failedToKillApp ${LANG_ENGLISH} "Failed to kill ${PRODUCTNAME}. Please close it first then try again" LangString installingWebview2 ${LANG_ENGLISH} "Installing WebView2..." LangString newerVersionInstalled ${LANG_ENGLISH} "A newer version of ${PRODUCTNAME} is already installed! It is not recommended that you install an older version. If you really want to install this older version, it's better to uninstall the current version first. Select the operation you want to perform and click Next to continue." LangString older ${LANG_ENGLISH} "older" LangString olderOrUnknownVersionInstalled ${LANG_ENGLISH} "An $R4 version of ${PRODUCTNAME} is installed on your system. It's recommended that you uninstall the current version before installing. Select the operation you want to perform and click Next to continue." LangString silentDowngrades ${LANG_ENGLISH} "Downgrades are disabled for this installer, can't proceed with the silent installer, please use the graphical interface installer instead.$\n" LangString unableToUninstall ${LANG_ENGLISH} "Unable to uninstall!" LangString uninstallApp ${LANG_ENGLISH} "Uninstall ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_ENGLISH} "Uninstall before installing" LangString unknown ${LANG_ENGLISH} "unknown" LangString deleteAppData ${LANG_ENGLISH} "Delete the application data" ================================================ FILE: crates/packager/src/package/nsis/languages/French.nsh ================================================ LangString addOrReinstall ${LANG_FRENCH} "Ajouter/Réinstaller un composant." LangString alreadyInstalled ${LANG_FRENCH} "Déja installé." LangString alreadyInstalledLong ${LANG_FRENCH} "${PRODUCTNAME} ${VERSION} est déja installé. Sélectionnez l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continuer." LangString appRunning ${LANG_FRENCH} "${PRODUCTNAME} est en cours d'exécution. Veuillez fermer l'application avant de réessayer." LangString appRunningOkKill ${LANG_FRENCH} "${PRODUCTNAME} est en cours d'exécution.$\nCliquez sur OK pour fermer l'application." LangString chooseMaintenanceOption ${LANG_FRENCH} "Veuillez choisir l'option de maintenance à effectuer." LangString choowHowToInstall ${LANG_FRENCH} "Veuillez choisir l'emplacement d'installation de ${PRODUCTNAME}." LangString createDesktop ${LANG_FRENCH} "Créer un raccourci sur le bureau." LangString dontUninstall ${LANG_FRENCH} "Ne pas désinstaller" LangString dontUninstallDowngrade ${LANG_FRENCH} "Ne pas désinstaller (revenir à une ancienne version sans désinstallation est désactivé pour cet installateur)" LangString failedToKillApp ${LANG_FRENCH} "La fermeture de ${PRODUCTNAME} a échoué. Veuillez fermer l'application et réessayer." LangString installingWebview2 ${LANG_FRENCH} "Installation de WebView2..." LangString newerVersionInstalled ${LANG_FRENCH} "Une version plus récente de ${PRODUCTNAME} est déja installée. Il n'est pas recommandé d'installer une ancienne version. Si vous souhaitez installer cette ancienne version, il est conseillé de désinstaller la version courante en premier. Veuillez sélectionner l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continer." LangString older ${LANG_FRENCH} "ancien" LangString olderOrUnknownVersionInstalled ${LANG_FRENCH} "La version $R4 de ${PRODUCTNAME} est installée sur le système. Il est recommandé de désinstaller la version actuelle avant d'installer celle-ci. Sélectionnez l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continuer." LangString silentDowngrades ${LANG_FRENCH} "Revenir à une version antérieure est désactivé pour cet installateur. Impossible de continuer avec l'installation silencieuse, veuillez utiliser l'interface graphique à la place.$\n" LangString unableToUninstall ${LANG_FRENCH} "Impossible de désinstaller le programme !" LangString uninstallApp ${LANG_FRENCH} "Désinstaller ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_FRENCH} "Désinstaller avant d'installer" LangString unknown ${LANG_FRENCH} "inconnu" LangString deleteAppData ${LANG_FRENCH} "Supprimer les données de l'application" ================================================ FILE: crates/packager/src/package/nsis/languages/Japanese.nsh ================================================ LangString addOrReinstall ${LANG_JAPANESE} "コンポーネントの追加・再インストール" LangString alreadyInstalled ${LANG_JAPANESE} "既にインストールされています" LangString alreadyInstalledLong ${LANG_JAPANESE} "${PRODUCTNAME} ${VERSION} は既にインストールされています。実行したい操作を選択し、「次へ」をクリックして続行します。" LangString appRunning ${LANG_JAPANESE} "${PRODUCTNAME} は動作中です。動作中のプログラムを終了し、もう一度やり直してください。" LangString appRunningOkKill ${LANG_JAPANESE} "${PRODUCTNAME} は動作中です。$\n「OK」を押すと動作中のプログラムを終了します。" LangString chooseMaintenanceOption ${LANG_JAPANESE} "メンテナンスオプションを選択して実行します。" LangString choowHowToInstall ${LANG_JAPANESE} "${PRODUCTNAME} のインストール方法を選択してください。" LangString createDesktop ${LANG_JAPANESE} "デスクトップショートカットを作成する" LangString dontUninstall ${LANG_JAPANESE} "アンインストールしない" LangString dontUninstallDowngrade ${LANG_JAPANESE} "アンインストールしない (このインストーラーでは、アンインストールをせずにダウングレードすることはできません)" LangString failedToKillApp ${LANG_JAPANESE} "${PRODUCTNAME} の終了に失敗しました。動作中のプログラムを終了し、もう一度やり直してください。" LangString installingWebview2 ${LANG_JAPANESE} "WebView2 をインストール中です..." LangString newerVersionInstalled ${LANG_JAPANESE} "既に新しいバージョンの ${PRODUCTNAME} がインストールされています。古いバージョンをインストールすることは推奨されません。どうしてもこの旧バージョンをインストールしたい場合は、先に現行バージョンをアンインストールしておく方がよいでしょう。実行したい操作を選択し、「次へ」をクリックして続行します。" LangString older ${LANG_JAPANESE} "旧" LangString olderOrUnknownVersionInstalled ${LANG_JAPANESE} "お使いのシステムには、 ${PRODUCTNAME} のバージョン $R4 がインストールされています。インストールする前に、現在のバージョンをアンインストールすることをお勧めします。実行したい操作を選択し、「次へ」をクリックして続行します。" LangString silentDowngrades ${LANG_JAPANESE} "このインストーラーではダウングレードはできません。サイレントインストーラーを続行できないので、代わりにグラフィカルインターフェースインストーラーを使用してください。$\n" LangString unableToUninstall ${LANG_JAPANESE} "アンインストールできません。" LangString uninstallApp ${LANG_JAPANESE} "${PRODUCTNAME} をアンインストールする" LangString uninstallBeforeInstalling ${LANG_JAPANESE} "インストールする前にアンインストールする" LangString unknown ${LANG_JAPANESE} "不明" LangString deleteAppData ${LANG_JAPANESE} "アプリケーションデータを削除する" ================================================ FILE: crates/packager/src/package/nsis/languages/Korean.nsh ================================================ LangString addOrReinstall ${LANG_KOREAN} "컴포넌트 추가 및 재설치" LangString alreadyInstalled ${LANG_KOREAN} "이미 설치되어 있습니다" LangString alreadyInstalledLong ${LANG_KOREAN} "${PRODUCTNAME} ${VERSION}이(가) 이미 설치되어 있습니다. 수행하고자 하는 작업을 선택하고 '다음'을 클릭하여 계속합니다." LangString appRunning ${LANG_KOREAN} "${PRODUCTNAME}이(가) 실행 중입니다! 먼저 닫은 후 다시 시도하세요." LangString appRunningOkKill ${LANG_KOREAN} "${PRODUCTNAME}이(가) 실행 중입니다!$\n'OK'를 누르면 실행 중인 프로그램을 종료합니다." LangString chooseMaintenanceOption ${LANG_KOREAN} "수행하려는 관리 옵션을 선택합니다." LangString choowHowToInstall ${LANG_KOREAN} "${PRODUCTNAME}의 설치 방법을 선택하세요.." LangString createDesktop ${LANG_KOREAN} "바탕화면 바로가기 만들기" LangString dontUninstall ${LANG_KOREAN} "제거하지 않기" LangString dontUninstallDowngrade ${LANG_KOREAN} "제거하지 않기 (이 설치 프로그램에서는 제거하지 않고 다운그레이드할 수 없습니다.)" LangString failedToKillApp ${LANG_KOREAN} "${PRODUCTNAME}을(를) 종료하지 못했습니다. 먼저 닫은 후 다시 시도하세요." LangString installingWebview2 ${LANG_KOREAN} "WebView2를 설치하는 중입니다..." LangString newerVersionInstalled ${LANG_KOREAN} "${PRODUCTNAME}의 최신 버전이 이미 설치되어 있습니다! 이전 버전을 설치하지 않는 것이 좋습니다. 이 이전 버전을 꼭 설치하려면 먼저 현재 버전을 제거하는 것이 좋습니다. 수행하려는 작업을 선택하고 '다음'을 클릭하여 계속합니다." LangString older ${LANG_KOREAN} "구" LangString olderOrUnknownVersionInstalled ${LANG_KOREAN} "시스템에 ${PRODUCTNAME}의 $R4 버전이 설치되어 있습니다. 설치하기 전에 현재 버전을 제거하는 것이 좋습니다. 수행하려는 작업을 선택하고 다음을 클릭하여 계속합니다." LangString silentDowngrades ${LANG_KOREAN} "이 설치 프로그램에서는 다운그레이드가 비활성화되어 자동 설치 프로그램을 진행할 수 없습니다. 대신 그래픽 인터페이스 설치 프로그램을 사용하세요.$\n" LangString unableToUninstall ${LANG_KOREAN} "제거할 수 없습니다!" LangString uninstallApp ${LANG_KOREAN} "${PRODUCTNAME} 제거하기" LangString uninstallBeforeInstalling ${LANG_KOREAN} "설치하기 전에 제거하기" LangString unknown ${LANG_KOREAN} "알 수 없음" LangString deleteAppData ${LANG_KOREAN} "애플리케이션 데이터 삭제하기" ================================================ FILE: crates/packager/src/package/nsis/languages/Persian.nsh ================================================ LangString addOrReinstall ${LANG_PERSIAN} "اضافه کردن/نصب مجدد کامپونتت" LangString alreadyInstalled ${LANG_PERSIAN} "قبلا نصب شده است" LangString alreadyInstalledLong ${LANG_PERSIAN} "${PRODUCTNAME} ${VERSION} قبلا نصب شده است. عملیات مدنظر را انتخاب کنید و بروی بعدی کلیک کنید." LangString appRunning ${LANG_PERSIAN} "${PRODUCTNAME} در حال اجر می باشد ! لطفا اول الان را ببندید و دوباره تلاش کنید" LangString appRunningOkKill ${LANG_PERSIAN} "${PRODUCTNAME} در حال اجرا می باشد!$\nبرای از بین بردن اوکی را انتخاب کنید" LangString chooseMaintenanceOption ${LANG_PERSIAN} "عملیات نگهداری مدنظر را برای اجرا انتخاب کنید" LangString choowHowToInstall ${LANG_PERSIAN} "نحوه نصب ${PRODUCTNAME} را انتخاب کنید" LangString createDesktop ${LANG_PERSIAN} "ایجاد میانبر دسکتاپ" LangString dontUninstall ${LANG_PERSIAN} "حذف نکنید" LangString dontUninstallDowngrade ${LANG_PERSIAN} "حذف نکنید (تنزل ورژن بدون حذف برای نصب کننده غیرفعال است)" LangString failedToKillApp ${LANG_PERSIAN} "${PRODUCTNAME} قابل کشته شدن نیست. اول آن را ببندید و دوباره تلاش کنید" LangString installingWebview2 ${LANG_PERSIAN} "در حال نصب WebView2 ..." LangString newerVersionInstalled ${LANG_PERSIAN} "ورژن جدید ${PRODUCTNAME} قبلا نصب شده است! نصب ورژن قدیمی تر به هیچ عنوان پیشنهاد نمی شود. اگر از این بابت اطمینان دارید , بهتر است ورژن فعلی را حذف کنید. عملیات مدنظر را انتخاب کنید و بروی بعدی کلیک کنید." LangString older ${LANG_PERSIAN} "قدیمی تر" LangString olderOrUnknownVersionInstalled ${LANG_PERSIAN} "ورژن $R4 ${PRODUCTNAME} قبلا بروی سیستم شما نصب شده است. ر. عملیات مدنظر را انتخاب کنید و بروی بعدی کلیک کنید." LangString silentDowngrades ${LANG_PERSIAN} "تنزل ورژن بدون حذف غیرفعال می باشد, عملیات نصب خاموش غیرقابل انجام است , از رابط گرافیکی برای نصب استفاده کنید.$\n" LangString unableToUninstall ${LANG_PERSIAN} "قابل نصب نیست!" LangString uninstallApp ${LANG_PERSIAN} "حذف ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_PERSIAN} "قبل از نصب , حذف کنید" LangString unknown ${LANG_PERSIAN} "ناشناس" LangString deleteAppData ${LANG_PERSIAN} "حذف دیتا های اپلیکیشن" ================================================ FILE: crates/packager/src/package/nsis/languages/PortugueseBR.nsh ================================================ LangString addOrReinstall ${LANG_PORTUGUESEBR} "Adicionar/Reinstalar componentes" LangString alreadyInstalled ${LANG_PORTUGUESEBR} "Já instalado" LangString alreadyInstalledLong ${LANG_PORTUGUESEBR} "${PRODUCTNAME} ${VERSION} já está instalado. Selecione a operação que deseja realizar e clique Próximo para continuar." LangString appRunning ${LANG_PORTUGUESEBR} "${PRODUCTNAME} está aberto! Por favor feche a janela dele e tente novamente." LangString appRunningOkKill ${LANG_PORTUGUESEBR} "${PRODUCTNAME} está aberto!$\nClique OK para fechar ele." LangString chooseMaintenanceOption ${LANG_PORTUGUESEBR} "Escolha a opção de manutenção a realizar." LangString choowHowToInstall ${LANG_PORTUGUESEBR} "Escolha como deseja instalar ${PRODUCTNAME}." LangString createDesktop ${LANG_PORTUGUESEBR} "Criar atalho na área de trabalho" LangString dontUninstall ${LANG_PORTUGUESEBR} "Não desinstalar" LangString dontUninstallDowngrade ${LANG_PORTUGUESEBR} "Não desinstalar (Instalar versão anterior sem desinstalar está desabilitado nesse instalador)" LangString failedToKillApp ${LANG_PORTUGUESEBR} "Falha ao fechar ${PRODUCTNAME}. Por favor feche a janela dele primeiro e tente novamente" LangString installingWebview2 ${LANG_PORTUGUESEBR} "Instalando WebView2..." LangString newerVersionInstalled ${LANG_PORTUGUESEBR} "Uma nova versão do ${PRODUCTNAME} já está instalado! Não é recomendado instalar uma versão anterior. Se realmente deseja instalar essa versão antiga, é recomendado desinstalar a versão atual primeirl. Selecione a operação que deseja executare clique Próximo para continuar." LangString older ${LANG_PORTUGUESEBR} "mais antiga" LangString olderOrUnknownVersionInstalled ${LANG_PORTUGUESEBR} "Uma versão $R4 do ${PRODUCTNAME} está instalada no seu sistema. É recomendado desinstalar a versão atual antes de prosseguir com a instalação. Selecione a operação que deseja executare clique Próximo para continuar." LangString silentDowngrades ${LANG_PORTUGUESEBR} "Instalar versão anterior está desabilitado nesse instalador, não foi possível proceder com a instalação silenciosa, por favor use a interface gráfica.$\n" LangString unableToUninstall ${LANG_PORTUGUESEBR} "Não foi possível instalar!" LangString uninstallApp ${LANG_PORTUGUESEBR} "Desinstalar ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_PORTUGUESEBR} "Desinstalar antes de instalar" LangString unknown ${LANG_PORTUGUESEBR} "desconhecida" LangString deleteAppData ${LANG_PORTUGUESEBR} "Remover dados do programa" ================================================ FILE: crates/packager/src/package/nsis/languages/SimpChinese.nsh ================================================ LangString addOrReinstall ${LANG_SIMPCHINESE} "添加/重新安装组件" LangString alreadyInstalled ${LANG_SIMPCHINESE} "已安装" LangString alreadyInstalledLong ${LANG_SIMPCHINESE} "${PRODUCTNAME} ${VERSION} 已经安装了。选择你想要执行的操作后点击下一步以继续。" LangString appRunning ${LANG_SIMPCHINESE} "${PRODUCTNAME} 正在运行!请关闭后再试。" LangString appRunningOkKill ${LANG_SIMPCHINESE} "${PRODUCTNAME} 正在运行!$\n点击确定以终止运行。" LangString chooseMaintenanceOption ${LANG_SIMPCHINESE} "选择要执行的操作。" LangString choowHowToInstall ${LANG_SIMPCHINESE} "选择你想要安装 ${PRODUCTNAME} 的方式。" LangString createDesktop ${LANG_SIMPCHINESE} "创建桌面快捷方式" LangString dontUninstall ${LANG_SIMPCHINESE} "不要卸载" LangString dontUninstallDowngrade ${LANG_SIMPCHINESE} "不要卸载(无需卸载的降级在此安装程序上已禁用)" LangString failedToKillApp ${LANG_SIMPCHINESE} "无法终止 ${PRODUCTNAME}。请关闭后再试。" LangString installingWebview2 ${LANG_SIMPCHINESE} "正在安装 WebView2..." LangString newerVersionInstalled ${LANG_SIMPCHINESE} "有一个更新的 ${PRODUCTNAME} 已经安装了!不推荐你安装旧的版本。如果你真的想要安装这个旧的版本,如果你真的想要安装这个版本,推荐先卸载当前版本。选择你想要执行的操作后点击下一步以继续。" LangString older ${LANG_SIMPCHINESE} "旧的" LangString olderOrUnknownVersionInstalled ${LANG_SIMPCHINESE} "系统中已存在版本为 $R4 的 ${PRODUCTNAME}。推荐先卸载当前版本后再进行安装。选择你想要执行的操作后点击下一步以继续。" LangString silentDowngrades ${LANG_SIMPCHINESE} "降级操作在此安装程序上已禁用,无法进行安静安装,请使用图形操作界面。$\n" LangString unableToUninstall ${LANG_SIMPCHINESE} "无法卸载!" LangString uninstallApp ${LANG_SIMPCHINESE} "卸载 ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_SIMPCHINESE} "安装前卸载" LangString unknown ${LANG_SIMPCHINESE} "未知" LangString deleteAppData ${LANG_SIMPCHINESE} "删除应用程序数据" ================================================ FILE: crates/packager/src/package/nsis/languages/Spanish.nsh ================================================ LangString addOrReinstall ${LANG_SPANISH} "Añadir o reinstalar componentes" LangString alreadyInstalled ${LANG_SPANISH} "Ya está instalado" LangString alreadyInstalledLong ${LANG_SPANISH} "${PRODUCTNAME} ${VERSION} ya está instalado. Seleccione la operación que desee realizar y pulse Siguiente para continuar." LangString appRunning ${LANG_SPANISH} "¡${PRODUCTNAME} está abierto! Por favor ciérrelo e intente de nuevo." LangString appRunningOkKill ${LANG_SPANISH} "¡${PRODUCTNAME} está abierto!$\nPulse Aceptar para cerrarlo." LangString chooseMaintenanceOption ${LANG_SPANISH} "Elija la operación de mantenimiento que desee realizar." LangString choowHowToInstall ${LANG_SPANISH} "Elija cómo desea instalar ${PRODUCTNAME}." LangString createDesktop ${LANG_SPANISH} "Crear acceso directo en el escritorio" LangString dontUninstall ${LANG_SPANISH} "No desinstalar" LangString dontUninstallDowngrade ${LANG_SPANISH} "No desinstalar (Disminuir la versión sin desinstalar está deshabilitado para este instalador)" LangString failedToKillApp ${LANG_SPANISH} "No se ha podido cerrar ${PRODUCTNAME}. Por favor ciérrelo e intente de nuevo." LangString installingWebview2 ${LANG_SPANISH} "Instalando WebView2..." LangString newerVersionInstalled ${LANG_SPANISH} "Ya está instalada una versión más reciente de ${PRODUCTNAME}. No se recomienda que instale una versión anterior. Si realmente desea instalar esta versión anterior, es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar." LangString older ${LANG_SPANISH} "anterior" LangString olderOrUnknownVersionInstalled ${LANG_SPANISH} "Una versión $R4 de ${PRODUCTNAME} está instalada en su sistema. Es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar." LangString silentDowngrades ${LANG_SPANISH} "Disminuir la versión está deshabilitado para este instalador. No se puede continuar con el instalador silencioso, por favor use el instalador de interfaz gráfica.$\n" LangString unableToUninstall ${LANG_SPANISH} "No se ha podido desinstalar." LangString uninstallApp ${LANG_SPANISH} "Desinstalar ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_SPANISH} "Desinstalar antes de instalar" LangString unknown ${LANG_SPANISH} "desconocida" LangString deleteAppData ${LANG_SPANISH} "Eliminar los datos de aplicación" ================================================ FILE: crates/packager/src/package/nsis/languages/SpanishInternational.nsh ================================================ LangString addOrReinstall ${LANG_SPANISH} "Añadir o reinstalar componentes" LangString alreadyInstalled ${LANG_SPANISH} "Ya está instalado" LangString alreadyInstalledLong ${LANG_SPANISH} "${PRODUCTNAME} ${VERSION} ya está instalado. Seleccione la operación que desee realizar y pulse Siguiente para continuar." LangString appRunning ${LANG_SPANISH} "¡${PRODUCTNAME} está abierto! Por favor ciérrelo e intente de nuevo." LangString appRunningOkKill ${LANG_SPANISH} "¡${PRODUCTNAME} está abierto!$\nPulse Aceptar para cerrarlo." LangString chooseMaintenanceOption ${LANG_SPANISH} "Elija la operación de mantenimiento que desee realizar." LangString choowHowToInstall ${LANG_SPANISH} "Elija cómo desea instalar ${PRODUCTNAME}." LangString createDesktop ${LANG_SPANISH} "Crear acceso directo en el escritorio" LangString dontUninstall ${LANG_SPANISH} "No desinstalar" LangString dontUninstallDowngrade ${LANG_SPANISH} "No desinstalar (Disminuir la versión sin desinstalar está deshabilitado para este instalador)" LangString failedToKillApp ${LANG_SPANISH} "No se ha podido cerrar ${PRODUCTNAME}. Por favor ciérrelo e intente de nuevo." LangString installingWebview2 ${LANG_SPANISH} "Instalando WebView2..." LangString newerVersionInstalled ${LANG_SPANISH} "Ya está instalada una versión más reciente de ${PRODUCTNAME}. No se recomienda que instale una versión anterior. Si realmente desea instalar esta versión anterior, es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar." LangString older ${LANG_SPANISH} "anterior" LangString olderOrUnknownVersionInstalled ${LANG_SPANISH} "Una versión $R4 de ${PRODUCTNAME} está instalada en su sistema. Es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar." LangString silentDowngrades ${LANG_SPANISH} "Disminuir la versión está deshabilitado para este instalador. No se puede continuar con el instalador silencioso, por favor use el instalador de interfaz gráfica.$\n" LangString unableToUninstall ${LANG_SPANISH} "No se ha podido desinstalar." LangString uninstallApp ${LANG_SPANISH} "Desinstalar ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_SPANISH} "Desinstalar antes de instalar" LangString unknown ${LANG_SPANISH} "desconocida" LangString deleteAppData ${LANG_SPANISH} "Eliminar los datos de aplicación" ================================================ FILE: crates/packager/src/package/nsis/languages/Swedish.nsh ================================================ LangString addOrReinstall ${LANG_SWEDISH} "Lägg till/Installera om komponenter" LangString alreadyInstalled ${LANG_SWEDISH}} "Redan installerad" LangString alreadyInstalledLong ${LANG_SWEDISH}} "${PRODUCTNAME} ${VERSION} är redan installerad. Välj åtgärd och klicka på Nästa för att fortsätta." LangString appRunning ${LANG_SWEDISH} "${PRODUCTNAME} körs! Stäng det först och försök igen." LangString appRunningOkKill ${LANG_SWEDISH} "${PRODUCTNAME} körs!$\nKlicka på OK för att avsluta det." LangString chooseMaintenanceOption ${LANG_SWEDISH} "Välj underhållsåtgärd." LangString choowHowToInstall ${LANG_SWEDISH} "Välj hur du vill installera ${PRODUCTNAME}." LangString createDesktop ${LANG_SWEDISH} "Skapa genväg på skrivbordet" LangString dontUninstall ${LANG_SWEDISH} "Avinstallera inte" LangString dontUninstallDowngrade ${LANG_SWEDISH} "Avinstallera inte (nedgradering utan avinstallation är inaktiverad för den här installationsprogrammet)" LangString failedToKillApp ${LANG_SWEDISH} "Kunde inte avsluta ${PRODUCTNAME}. Stäng det först och försök igen." LangString installingWebview2 ${LANG_SWEDISH} "Installerar WebView2..." LangString newerVersionInstalled ${LANG_SWEDISH} "En nyare version av ${PRODUCTNAME} är redan installerad! Det rekommenderas inte att installera en äldre version. Om du verkligen vill installera denna äldre version är det bättre att avinstallera den nuvarande versionen först. Välj åtgärd och klicka på Nästa för att fortsätta." LangString older ${LANG_SWEDISH} "äldre" LangString olderOrUnknownVersionInstalled ${LANG_SWEDISH} "En $R4-version av ${PRODUCTNAME} är installerad på ditt system. Det rekommenderas att du avinstallerar den nuvarande versionen innan du installerar. Välj åtgärd och klicka på Nästa för att fortsätta." LangString silentDowngrades ${LANG_SWEDISH} "Nedgraderingar är inaktiverade för den här installationsprogrammet. Kan inte fortsätta med installationsprogrammet. Använd det grafiska installationsprogrammet istället.$\n" LangString unableToUninstall ${LANG_SWEDISH} "Kan inte avinstallera!" LangString uninstallApp ${LANG_SWEDISH} "Avinstallera ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_SWEDISH} "Avinstallera innan installation" LangString unknown ${LANG_SWEDISH} "okänd" LangString deleteAppData ${LANG_SWEDISH} "Ta bort applikationsdata" ================================================ FILE: crates/packager/src/package/nsis/languages/TradChinese.nsh ================================================ LangString addOrReinstall ${LANG_TRADCHINESE} "增加或重新安裝元件" LangString alreadyInstalled ${LANG_TRADCHINESE} "已安裝" LangString alreadyInstalledLong ${LANG_TRADCHINESE} "${PRODUCTNAME} ${VERSION} 已經安裝了。選擇你想要進行的操作並且點選下一步。" LangString appRunning ${LANG_TRADCHINESE} "${PRODUCTNAME} 正在執行中!請先關閉再進行嘗試。" LangString appRunningOkKill ${LANG_TRADCHINESE} "${PRODUCTNAME} 正在執行中!點選確定後終止。" LangString chooseMaintenanceOption ${LANG_TRADCHINESE} "請選擇你要進行的維護選項。" LangString choowHowToInstall ${LANG_TRADCHINESE} "選擇你要如何安裝 ${PRODUCTNAME}。" LangString createDesktop ${LANG_TRADCHINESE} "建立桌面捷徑" LangString dontUninstall ${LANG_TRADCHINESE} "請勿解除安裝" LangString dontUninstallDowngrade ${LANG_TRADCHINESE} "請勿解除安裝(本安裝程式不允許未解除安裝就進行版本降低的操作)" LangString failedToKillApp ${LANG_TRADCHINESE} "無法終止 ${PRODUCTNAME}。請先關閉再進行嘗試。" LangString installingWebview2 ${LANG_TRADCHINESE} "WebView2 安裝中..." LangString newerVersionInstalled ${LANG_TRADCHINESE} "已安裝更新版本的 ${PRODUCTNAME}!不建議安裝舊版。如果真的想要安裝舊版的話,最好先解除安裝現在的版本。選擇你想要進行的操作後再進行下一步。" LangString older ${LANG_TRADCHINESE} "舊版" LangString olderOrUnknownVersionInstalled ${LANG_TRADCHINESE} "你的系統已經安裝 ${PRODUCTNAME} 的 $R4 版本。建議你先解除安裝現在的版本後再進行安裝。選擇你想要進行的操作後再進行下一步。" LangString silentDowngrades ${LANG_TRADCHINESE} "本安裝程式不允許未解除安裝就進行版本降低的操作,無法繼續無聲安裝,請改用圖形化介面安裝。$\n" LangString unableToUninstall ${LANG_TRADCHINESE} "無法解除安裝!" LangString uninstallApp ${LANG_TRADCHINESE} "解除安裝 ${PRODUCTNAME}" LangString uninstallBeforeInstalling ${LANG_TRADCHINESE} "安裝前先解除安裝" LangString unknown ${LANG_TRADCHINESE} "未知" LangString deleteAppData ${LANG_TRADCHINESE} "刪除應用程式數據" ================================================ FILE: crates/packager/src/package/nsis/languages/Turkish.nsh ================================================ LangString addOrReinstall ${LANG_TURKISH} "Bileşen Ekle/Yeniden Yükle" LangString alreadyInstalled ${LANG_TURKISH} "Daha Önceden Yüklenmiş" LangString alreadyInstalledLong ${LANG_TURKISH} "${PRODUCTNAME} ${VERSION} daha önceden yüklenmiş. Gerçekleştirmek istediğiniz işlemi seçin ve devam etmek için İleri'ye tıklayın." LangString appRunning ${LANG_TURKISH} "${PRODUCTNAME} çalışır durumda! Lütfen önce uygulamayı kapatın ve sonra tekrar deneyin." LangString appRunningOkKill ${LANG_TURKISH} "${PRODUCTNAME} çalışır durumda!$\nUygulamayı sonlandırmak için Tamam'a tıklayın." LangString chooseMaintenanceOption ${LANG_TURKISH} "Gerçekleştirmek istediğiniz bakım seçeneğini belirleyin." LangString choowHowToInstall ${LANG_TURKISH} "${PRODUCTNAME} uygulamasını nasıl yüklemek istediğinizi seçin." LangString createDesktop ${LANG_TURKISH} "Masaüstü kısayolu oluştur" LangString dontUninstall ${LANG_TURKISH} "Kaldırma işlemini gerçekleştirme" LangString dontUninstallDowngrade ${LANG_TURKISH} "Kaldırma işlemini gerçekleştirme (Kaldırma işlemi yapmadan sürüm düşürme bu yükleyici için devre dışı bırakılmıştır)" LangString failedToKillApp ${LANG_TURKISH} "${PRODUCTNAME} sonlandırılamadı. Lütfen önce kapatın sonra tekrar deneyin." LangString installingWebview2 ${LANG_TURKISH} "WebView2 yükleniyor..." LangString newerVersionInstalled ${LANG_TURKISH} "${PRODUCTNAME} uygulamasının daha yeni bir sürümü zaten yüklü! Daha eski bir sürümü yüklemeniz önerilmez. Bu eski sürümü gerçekten yüklemek istiyorsanız, önce mevcut sürümü kaldırmanız daha uygundur. Gerçekleştirmek istediğiniz işlemi seçin ve devam etmek için İleri'ye tıklayın." LangString older ${LANG_TURKISH} "daha eski" LangString olderOrUnknownVersionInstalled ${LANG_TURKISH} "Sisteminizde ${PRODUCTNAME} uygulamasının $R4 sürümü yüklü. Yükleme işleminden önce mevcut sürümü kaldırmanız önerilir. Gerçekleştirmek istediğiniz işlemi seçin ve devam etmek için İleri'ye tıklayın." LangString silentDowngrades ${LANG_TURKISH} "Bu yükleyici için sürüm düşürme işlemleri devre dışı bırakıldı, sessiz yükleyici ile devam edilemiyor, lütfen bunun yerine grafik arayüz yükleyicisini kullanın.$\n" LangString unableToUninstall ${LANG_TURKISH} "Kaldırma işlemi gerçekleştirilemiyor!" LangString uninstallApp ${LANG_TURKISH} "${PRODUCTNAME}'i kaldır" LangString uninstallBeforeInstalling ${LANG_TURKISH} "Yükleme yapmadan önce kaldırın" LangString unknown ${LANG_TURKISH} "bilinmeyen" LangString deleteAppData ${LANG_TURKISH} "Uygulama verilerini sil" ================================================ FILE: crates/packager/src/package/nsis/mod.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ collections::{BTreeMap, BTreeSet, HashMap}, fmt::Debug, fs, path::{Path, PathBuf}, process::Command, }; use handlebars::{to_json, Handlebars}; use super::Context; use crate::{ codesign::windows::{self as codesign}, util::verify_file_hash, Error, }; use crate::{ config::{Config, LogLevel, NSISInstallerMode, NsisCompression}, shell::CommandExt, util::{self, download, download_and_verify, extract_zip, HashAlgorithm}, }; // URLS for the NSIS toolchain. #[cfg(target_os = "windows")] const NSIS_URL: &str = "https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.9/nsis-3.09.zip"; #[cfg(target_os = "windows")] const NSIS_SHA1: &str = "586855a743a6e0ade203d8758af303a48ee0716b"; const NSIS_APPLICATIONID_URL: &str = "https://github.com/tauri-apps/binary-releases/releases/download/nsis-plugins-v0/NSIS-ApplicationID.zip"; const NSIS_TAURI_UTILS_URL: &str = "https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.2.1/nsis_tauri_utils.dll"; const NSIS_TAURI_UTILS_SHA1: &str = "53A7CFAEB6A4A9653D6D5FBFF02A3C3B8720130A"; #[cfg(target_os = "windows")] const NSIS_REQUIRED_FILES: &[&str] = &[ "makensis.exe", "Bin/makensis.exe", "Stubs/lzma-x86-unicode", "Stubs/lzma_solid-x86-unicode", "Plugins/x86-unicode/ApplicationID.dll", "Plugins/x86-unicode/nsis_tauri_utils.dll", "Include/MUI2.nsh", "Include/FileFunc.nsh", "Include/x64.nsh", "Include/nsDialogs.nsh", "Include/WinMessages.nsh", ]; #[cfg(not(target_os = "windows"))] const NSIS_REQUIRED_FILES: &[&str] = &[ "Plugins/x86-unicode/ApplicationID.dll", "Plugins/x86-unicode/nsis_tauri_utils.dll", ]; const NSIS_REQUIRED_FILES_HASH: &[(&str, &str, &str, HashAlgorithm)] = &[( "Plugins/x86-unicode/nsis_tauri_utils.dll", NSIS_TAURI_UTILS_URL, NSIS_TAURI_UTILS_SHA1, HashAlgorithm::Sha1, )]; type DirectoriesSet = BTreeSet; type ResourcesMap = BTreeMap; #[cfg(windows)] fn normalize_resource_path>(path: P) -> PathBuf { path.as_ref().to_owned() } // We need to convert / to \ for nsis to move the files into the correct dirs #[cfg(not(windows))] fn normalize_resource_path>(path: P) -> PathBuf { path.as_ref() .display() .to_string() .replace('/', "\\") .into() } #[tracing::instrument(level = "trace", skip(config))] fn generate_resource_data(config: &Config) -> crate::Result<(DirectoriesSet, ResourcesMap)> { let mut directories = BTreeSet::new(); let mut resources_map = BTreeMap::new(); for r in config.resources()? { // only add if resource has a parent e.g. `files/a.txt` // and is not empty. this is to ensure that we don't // generate `CreateDirectory "$INSTDIR\"` which is useless // since `INSTDIR` is already created. if let Some(parent) = r.target.parent() { if parent.as_os_str() != "" { directories.insert(normalize_resource_path(parent)); } } resources_map.insert(r.src, normalize_resource_path(r.target)); } Ok((directories, resources_map)) } /// BTreeMap type BinariesMap = BTreeMap; #[tracing::instrument(level = "trace", skip(config))] fn generate_binaries_data(config: &Config) -> crate::Result { let mut binaries = BinariesMap::new(); if let Some(external_binaries) = &config.external_binaries { let cwd = std::env::current_dir()?; let target_triple = config.target_triple(); for src in external_binaries { let file_name = src .file_name() .ok_or_else(|| Error::FailedToExtractFilename(src.clone()))? .to_string_lossy(); let src = src.with_file_name(format!("{file_name}-{target_triple}.exe")); let bin_path = cwd.join(src); let bin_path = dunce::canonicalize(&bin_path).map_err(|e| Error::IoWithPath(bin_path, e))?; let dest_file_name = format!("{file_name}.exe"); binaries.insert(bin_path, dest_file_name); } } for bin in &config.binaries { if !bin.main { let bin_path = config.binary_path(bin).with_extension("exe"); let dest_filename = bin_path .file_name() .ok_or_else(|| Error::FailedToExtractFilename(bin_path.clone()))? .to_string_lossy() .to_string(); binaries.insert(bin_path, dest_filename); } } Ok(binaries) } #[tracing::instrument(level = "trace")] fn get_lang_data( lang: &str, custom_lang_files: Option<&HashMap>, ) -> crate::Result)>> { if let Some(path) = custom_lang_files.and_then(|h| h.get(lang)) { let canonicalized = dunce::canonicalize(path).map_err(|e| Error::IoWithPath(path.clone(), e))?; return Ok(Some((canonicalized, None))); } let lang_path = PathBuf::from(format!("{lang}.nsh")); let lang_content = match lang.to_lowercase().as_str() { "arabic" => Some(include_str!("./languages/Arabic.nsh")), "bulgarian" => Some(include_str!("./languages/Bulgarian.nsh")), "dutch" => Some(include_str!("./languages/Dutch.nsh")), "english" => Some(include_str!("./languages/English.nsh")), "japanese" => Some(include_str!("./languages/Japanese.nsh")), "korean" => Some(include_str!("./languages/Korean.nsh")), "portuguesebr" => Some(include_str!("./languages/PortugueseBR.nsh")), "tradchinese" => Some(include_str!("./languages/TradChinese.nsh")), "simpchinese" => Some(include_str!("./languages/SimpChinese.nsh")), "french" => Some(include_str!("./languages/French.nsh")), "spanish" => Some(include_str!("./languages/Spanish.nsh")), "spanishinternational" => Some(include_str!("./languages/SpanishInternational.nsh")), "persian" => Some(include_str!("./languages/Persian.nsh")), "turkish" => Some(include_str!("./languages/Turkish.nsh")), "swedish" => Some(include_str!("./languages/Swedish.nsh")), _ => return Ok(None), }; Ok(Some((lang_path, lang_content))) } #[tracing::instrument(level = "trace")] fn write_ut16_le_with_bom + Debug>(path: P, content: &str) -> crate::Result<()> { tracing::debug!("Writing {path:?} in UTF-16 LE encoding"); use std::fs::File; use std::io::{BufWriter, Write}; let path = path.as_ref(); let file = File::create(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; let mut output = BufWriter::new(file); output.write_all(&[0xFF, 0xFE])?; // the BOM part for utf16 in content.encode_utf16() { output.write_all(&utf16.to_le_bytes())?; } Ok(()) } fn handlebars_or( h: &handlebars::Helper<'_>, _: &Handlebars<'_>, _: &handlebars::Context, _: &mut handlebars::RenderContext<'_, '_>, out: &mut dyn handlebars::Output, ) -> handlebars::HelperResult { let param1 = h.param(0).unwrap().render(); let param2 = h.param(1).unwrap(); out.write(&if param1.is_empty() { param2.render() } else { param1 })?; Ok(()) } fn association_description( h: &handlebars::Helper<'_>, _: &Handlebars<'_>, _: &handlebars::Context, _: &mut handlebars::RenderContext<'_, '_>, out: &mut dyn handlebars::Output, ) -> handlebars::HelperResult { let description = h.param(0).unwrap().render(); let ext = h.param(1).unwrap(); out.write(&if description.is_empty() { format!("{} File", ext.render().to_uppercase()) } else { description })?; Ok(()) } fn unescape_newlines( h: &handlebars::Helper<'_>, _: &Handlebars<'_>, _: &handlebars::Context, _: &mut handlebars::RenderContext<'_, '_>, out: &mut dyn handlebars::Output, ) -> handlebars::HelperResult { let s = h.param(0).unwrap().render(); out.write(&s.replace("$\\n", "\n"))?; Ok(()) } fn unescape_dollar_sign( h: &handlebars::Helper<'_>, _: &Handlebars<'_>, _: &handlebars::Context, _: &mut handlebars::RenderContext<'_, '_>, out: &mut dyn handlebars::Output, ) -> handlebars::HelperResult { let s = h.param(0).unwrap().render(); out.write(&s.replace("$$", "$"))?; Ok(()) } fn add_build_number_if_needed(version_str: &str) -> crate::Result { let version = semver::Version::parse(version_str)?; if !version.build.is_empty() { let build = version.build.parse::(); if build.is_ok() { return Ok(format!( "{}.{}.{}.{}", version.major, version.minor, version.patch, version.build )); } else { return Err(Error::NonNumericBuildMetadata(None)); } } Ok(format!( "{}.{}.{}.0", version.major, version.minor, version.patch, )) } fn file_len>(p: P) -> crate::Result { fs::metadata(&p) .map(|m| m.len()) .map_err(|e| Error::IoWithPath(p.as_ref().to_path_buf(), e)) } fn generate_estimated_size(main: P, other_files: I) -> crate::Result where I: IntoIterator, P: AsRef, P2: AsRef, { let mut size = file_len(main)?; for k in other_files { size += file_len(k)?; } size /= 1000; Ok(format!("{size:#08x}")) } #[tracing::instrument(level = "trace", skip(ctx))] fn get_and_extract_nsis( #[allow(unused)] ctx: &Context, nsis_toolset_path: &Path, ) -> crate::Result<()> { #[cfg(target_os = "windows")] { let data = download_and_verify("nsis-3.09.zip", NSIS_URL, NSIS_SHA1, HashAlgorithm::Sha1)?; tracing::debug!("Extracting nsis-3.09.zip"); extract_zip(&data, &ctx.tools_path)?; let downloaded_nsis = ctx.tools_path.join("nsis-3.09"); fs::rename(&downloaded_nsis, nsis_toolset_path) .map_err(|e| Error::RenameFile(downloaded_nsis, nsis_toolset_path.to_path_buf(), e))?; } let nsis_plugins = nsis_toolset_path.join("Plugins"); let unicode_plugins = nsis_plugins.join("x86-unicode"); fs::create_dir_all(&unicode_plugins) .map_err(|e| Error::IoWithPath(unicode_plugins.clone(), e))?; let data = download(NSIS_APPLICATIONID_URL)?; tracing::debug!("Extracting NSIS ApplicationID plugin"); extract_zip(&data, &nsis_plugins)?; let src = nsis_plugins.join("ReleaseUnicode/ApplicationID.dll"); let dest = unicode_plugins.join("ApplicationID.dll"); fs::copy(&src, &dest).map_err(|e| Error::CopyFile(src, dest, e))?; let data = download_and_verify( "nsis_tauri_utils.dll", NSIS_TAURI_UTILS_URL, NSIS_TAURI_UTILS_SHA1, HashAlgorithm::Sha1, )?; let path = unicode_plugins.join("nsis_tauri_utils.dll"); fs::write(&path, data).map_err(|e| Error::IoWithPath(path, e))?; Ok(()) } #[tracing::instrument(level = "trace", skip(ctx))] fn build_nsis_app_installer(ctx: &Context, nsis_path: &Path) -> crate::Result> { let Context { config, intermediates_path, .. } = ctx; let arch = match config.target_arch()? { "x86_64" => "x64", "x86" => "x86", "aarch64" => "arm64", target => return Err(Error::UnsupportedArch("nsis".into(), target.into())), }; let main_binary = config.main_binary()?; let main_binary_name = config.main_binary_name()?; let main_binary_path = config.binary_path(main_binary).with_extension("exe"); if config.can_sign() { tracing::debug!("Codesigning {}", main_binary_path.display()); codesign::try_sign(&main_binary_path, config)?; } else { #[cfg(not(target_os = "windows"))] tracing::warn!("Codesigning is by default is only supported on Windows hosts, but you can specify a custom signing command in `config.windows.sign_command`, for now, skipping signing the main binary..."); } let intermediates_path = intermediates_path.join("nsis").join(arch); util::create_clean_dir(&intermediates_path)?; let mut data = BTreeMap::new(); #[cfg(not(target_os = "windows"))] { let dir = nsis_path.join("Plugins/x86-unicode"); data.insert("additional_plugins_path", to_json(dir)); } let identifier = config.identifier(); let manufacturer = config.publisher(); data.insert("arch", to_json(arch)); data.insert("identifier", to_json(identifier)); data.insert("manufacturer", to_json(&manufacturer)); data.insert("product_name", to_json(&config.product_name)); data.insert("short_description", to_json(&config.description)); data.insert("copyright", to_json(&config.copyright)); data.insert("version", to_json(&config.version)); data.insert( "version_with_build", to_json(add_build_number_if_needed(&config.version)?), ); data.insert( "allow_downgrades", to_json(config.windows().map(|w| w.allow_downgrades)), ); if config.can_sign() { let sign_cmd = format!("{:?}", codesign::sign_command("%1", &config.sign_params())?); data.insert("uninstaller_sign_cmd", to_json(sign_cmd)); } if let Some(license) = &config.license_file { let canonicalized = dunce::canonicalize(license).map_err(|e| Error::IoWithPath(license.clone(), e))?; data.insert("license", to_json(canonicalized)); } let mut install_mode = NSISInstallerMode::CurrentUser; let mut languages = vec!["English".into()]; let mut custom_template_path = None; let mut custom_language_files = None; if let Some(nsis) = config.nsis() { custom_template_path.clone_from(&nsis.template); custom_language_files.clone_from(&nsis.custom_language_files); install_mode = nsis.install_mode; if let Some(langs) = &nsis.languages { languages.clear(); languages.extend_from_slice(langs); } data.insert( "display_language_selector", to_json(nsis.display_language_selector && languages.len() > 1), ); if let Some(installer_icon) = &nsis.installer_icon { let canonicalized = dunce::canonicalize(installer_icon) .map_err(|e| Error::IoWithPath(installer_icon.clone(), e))?; data.insert("installer_icon", to_json(canonicalized)); } if let Some(header_image) = &nsis.header_image { let canonicalized = dunce::canonicalize(header_image) .map_err(|e| Error::IoWithPath(header_image.clone(), e))?; data.insert("header_image", to_json(canonicalized)); } if let Some(sidebar_image) = &nsis.sidebar_image { let canonicalized = dunce::canonicalize(sidebar_image) .map_err(|e| Error::IoWithPath(sidebar_image.clone(), e))?; data.insert("sidebar_image", to_json(canonicalized)); } if let Some(preinstall_section) = &nsis.preinstall_section { data.insert("preinstall_section", to_json(preinstall_section)); } if let Some(compression) = &nsis.compression { data.insert( "compression", to_json(match &compression { NsisCompression::Zlib => "zlib", NsisCompression::Bzip2 => "bzip2", NsisCompression::Lzma => "lzma", NsisCompression::Off => "off", }), ); } if let Some(appdata_paths) = &nsis.appdata_paths { let appdata_paths = appdata_paths .iter() .map(|p| { p.replace("$PUBLISHER", &manufacturer) .replace("$PRODUCTNAME", &config.product_name) .replace("$IDENTIFIER", config.identifier()) }) .collect::>(); data.insert("appdata_paths", to_json(appdata_paths)); } } data.insert("install_mode", to_json(install_mode)); let mut languages_data = Vec::new(); for lang in &languages { if let Some(data) = get_lang_data(lang, custom_language_files.as_ref())? { languages_data.push(data); } else { tracing::warn!("Custom cargo-packager messages for {lang} are not translated.\nIf it is a valid language listed on , please open a cargo-packager feature request\n or you can provide a custom language file for it in ` nsis.custom_language_files`"); } } data.insert("languages", to_json(languages.clone())); data.insert( "language_files", to_json( languages_data .iter() .map(|d| d.0.clone()) .collect::>(), ), ); data.insert("main_binary_name", to_json(&main_binary_name)); data.insert("main_binary_path", to_json(&main_binary_path)); if let Some(file_associations) = &config.file_associations { data.insert("file_associations", to_json(file_associations)); } if let Some(protocols) = &config.deep_link_protocols { let schemes = protocols .iter() .flat_map(|p| &p.schemes) .collect::>(); data.insert("deep_link_protocols", to_json(schemes)); } let out_file = "nsis-output.exe"; data.insert("out_file", to_json(out_file)); let (resources_dirs, resources) = generate_resource_data(config)?; data.insert("resources_dirs", to_json(resources_dirs)); data.insert("resources", to_json(&resources)); let binaries = generate_binaries_data(config)?; data.insert("binaries", to_json(&binaries)); let estimated_size = generate_estimated_size(main_binary_path, resources.keys().chain(binaries.keys()))?; data.insert("estimated_size", to_json(estimated_size)); let mut handlebars = Handlebars::new(); handlebars.register_helper("or", Box::new(handlebars_or)); handlebars.register_helper("association-description", Box::new(association_description)); handlebars.register_helper("unescape_newlines", Box::new(unescape_newlines)); handlebars.register_helper("unescape_dollar_sign", Box::new(unescape_dollar_sign)); handlebars.register_escape_fn(|s| { let mut output = String::new(); for c in s.chars() { match c { '\"' => output.push_str("$\\\""), '$' => output.push_str("$$"), '`' => output.push_str("$\\`"), '\n' => output.push_str("$\\n"), '\t' => output.push_str("$\\t"), '\r' => output.push_str("$\\r"), _ => output.push(c), } } output }); if let Some(path) = custom_template_path { handlebars .register_template_string("installer.nsi", fs::read_to_string(path)?) .map_err(Box::new)?; } else { handlebars .register_template_string("installer.nsi", include_str!("./installer.nsi")) .map_err(Box::new)?; } write_ut16_le_with_bom( intermediates_path.join("FileAssociation.nsh"), include_str!("./FileAssociation.nsh"), )?; let installer_nsi_path = intermediates_path.join("installer.nsi"); write_ut16_le_with_bom( &installer_nsi_path, handlebars.render("installer.nsi", &data)?.as_str(), )?; for (lang, data) in languages_data.iter() { if let Some(content) = data { write_ut16_le_with_bom(intermediates_path.join(lang).with_extension("nsh"), content)?; } } let nsis_output_path = intermediates_path.join(out_file); let installer_path = config.out_dir().join(format!( "{}_{}_{}-setup.exe", main_binary_name, config.version, arch )); let installer_path_parent = installer_path .parent() .ok_or_else(|| Error::ParentDirNotFound(installer_path.clone()))?; fs::create_dir_all(installer_path_parent) .map_err(|e| Error::IoWithPath(installer_path_parent.to_path_buf(), e))?; tracing::info!( "Running makensis.exe to produce {}", util::display_path(&installer_path) ); #[cfg(target_os = "windows")] let mut nsis_cmd = Command::new(nsis_path.join("makensis.exe")); #[cfg(not(target_os = "windows"))] let mut nsis_cmd = Command::new("makensis"); if let Some(level) = config.log_level { nsis_cmd.arg(match level { LogLevel::Error => "-V1", LogLevel::Warn | LogLevel::Info => "-V2", LogLevel::Debug => "-V3", _ => "-V4", }); } nsis_cmd .arg(installer_nsi_path) .current_dir(intermediates_path) .output_ok() .map_err(Error::NsisFailed)?; fs::rename(&nsis_output_path, &installer_path) .map_err(|e| Error::RenameFile(nsis_output_path, installer_path.clone(), e))?; if config.can_sign() { tracing::debug!("Codesigning {}", installer_path.display()); codesign::try_sign(&installer_path, config)?; } else { #[cfg(not(target_os = "windows"))] tracing::warn!("Codesigning is by default is only supported on Windows hosts, but you can specify a custom signing command in `config.windows.sign_command`, for now, skipping signing the installer..."); } Ok(vec![installer_path]) } #[tracing::instrument(level = "trace", skip(ctx))] pub(crate) fn package(ctx: &Context) -> crate::Result> { let nsis_toolset_path = ctx.tools_path.join("NSIS"); if !nsis_toolset_path.exists() { get_and_extract_nsis(ctx, &nsis_toolset_path)?; } else if NSIS_REQUIRED_FILES .iter() .any(|p| !nsis_toolset_path.join(p).exists()) { tracing::warn!("NSIS directory is missing some files. Recreating it..."); fs::remove_dir_all(&nsis_toolset_path) .map_err(|e| Error::IoWithPath(nsis_toolset_path.clone(), e))?; get_and_extract_nsis(ctx, &nsis_toolset_path)?; } else { let mismatched = NSIS_REQUIRED_FILES_HASH .iter() .filter(|(p, _, hash, hash_algorithm)| { verify_file_hash(nsis_toolset_path.join(p), hash, *hash_algorithm).is_err() }) .collect::>(); if !mismatched.is_empty() { tracing::warn!("NSIS directory contains mis-hashed files. Redownloading them."); for (path, url, hash, hash_algorithim) in mismatched { let path = nsis_toolset_path.join(path); let data = download_and_verify(&path, url, hash, *hash_algorithim)?; fs::write(&path, data).map_err(|e| Error::IoWithPath(path, e))?; } } } build_nsis_app_installer(ctx, &nsis_toolset_path) } ================================================ FILE: crates/packager/src/package/pacman/mod.rs ================================================ // Copyright 2024-2024 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use super::deb; use crate::{config::Config, package::Context, util, Error}; use heck::AsKebabCase; use sha2::{Digest, Sha512}; use std::{ fs::{self, File}, io::{self, Write}, path::{Path, PathBuf}, }; #[tracing::instrument(level = "trace", skip(ctx))] pub(crate) fn package(ctx: &Context) -> crate::Result> { let Context { config, intermediates_path, .. } = ctx; let arch = match config.target_arch()? { "x86" => "i386", "arm" => "armhf", other => other, }; let intermediates_path = intermediates_path.join("pacman"); util::create_clean_dir(&intermediates_path)?; let package_base_name = format!("{}_{}_{}", config.main_binary_name()?, config.version, arch); let package_name = format!("{package_base_name}.tar.gz"); let pkg_dir = intermediates_path.join(&package_base_name); let pkg_path = config.out_dir().join(&package_name); let pkgbuild_path = pkg_path.with_file_name("PKGBUILD"); tracing::info!("Packaging {} ({})", package_name, pkg_path.display()); tracing::debug!("Generating data"); let _ = deb::generate_data(config, &pkg_dir)?; tracing::debug!("Copying files specified in `pacman.files`"); if let Some(files) = config.pacman().and_then(|d| d.files.as_ref()) { deb::copy_custom_files(files, &pkg_dir)?; } // Apply tar/gzip to create the final package file. tracing::debug!("Creating package archive using tar and gzip"); let data_tar_gz_path = deb::tar_and_gzip_dir(pkg_dir)?; fs::copy(&data_tar_gz_path, &pkg_path) .map_err(|e| Error::CopyFile(data_tar_gz_path, pkg_path.clone(), e))?; tracing::info!("Generating PKGBUILD: {}", pkgbuild_path.display()); generate_pkgbuild_file(config, arch, pkgbuild_path.as_path(), pkg_path.as_path())?; Ok(vec![pkg_path]) } /// Generates the pacman PKGBUILD file. /// For more information about the format of this file, see /// fn generate_pkgbuild_file( config: &Config, arch: &str, dest_dir: &Path, package_path: &Path, ) -> crate::Result<()> { let pkgbuild_path = dest_dir.with_file_name("PKGBUILD"); let mut file = util::create_file(&pkgbuild_path)?; if let Some(authors) = &config.authors { writeln!(file, "# Maintainer: {}", authors.join(", "))?; } writeln!(file, "pkgname={}", AsKebabCase(&config.product_name))?; writeln!(file, "pkgver={}", config.version)?; writeln!(file, "pkgrel=1")?; writeln!(file, "epoch=")?; writeln!( file, "pkgdesc=\"{}\"", config.description.as_deref().unwrap_or("") )?; writeln!(file, "arch=('{arch}')")?; if let Some(homepage) = &config.homepage { writeln!(file, "url=\"{homepage}\"")?; } let dependencies = config .pacman() .and_then(|d| d.depends.as_ref()) .map_or_else(|| Ok(Vec::new()), |d| d.to_list())?; writeln!(file, "depends=({})", dependencies.join(" \n"))?; let provides = config .pacman() .and_then(|d| d.provides.clone()) .unwrap_or_default(); writeln!(file, "provides=({})", provides.join(" \n"))?; let conflicts = config .pacman() .and_then(|d| d.conflicts.clone()) .unwrap_or_default(); writeln!(file, "conflicts=({})", conflicts.join(" \n"))?; let replaces = config .pacman() .and_then(|d| d.replaces.clone()) .unwrap_or_default(); writeln!(file, "replaces=({})", replaces.join(" \n"))?; writeln!(file, "options=(!lto)")?; let source = config .pacman() .and_then(|d| d.source.clone()) .unwrap_or_default(); if source.is_empty() { writeln!(file, "source=({:?})", package_path.file_name().unwrap())?; } else { writeln!(file, "source=({})", source.join(" \n"))?; } // Generate SHA512 sum of the package let mut sha_file = File::open(package_path).map_err(|e| Error::IoWithPath(package_path.to_path_buf(), e))?; let mut sha512 = Sha512::new(); io::copy(&mut sha_file, &mut sha512)?; let sha_hash = sha512.finalize(); writeln!(file, "sha512sums=(\"{sha_hash:x}\")")?; writeln!( file, "package() {{\n\tcp -r \"${{srcdir}}\"/* \"${{pkgdir}}\"/\n}}" )?; file.flush()?; Ok(()) } ================================================ FILE: crates/packager/src/package/wix/default-locale-strings.xml ================================================ __language__ __codepage__ Launch __productName__ A newer version of __productName__ is already installed. Add the install location of the __productName__ executable to the PATH system environment variable. This allows the __productName__ executable to be called from any location. Installs __productName__. ================================================ FILE: crates/packager/src/package/wix/languages.json ================================================ { "ar-SA": { "langId": 1025, "asciiCode": 1256 }, "ca-ES": { "langId": 1027, "asciiCode": 1252 }, "zh-TW": { "langId": 1028, "asciiCode": 950 }, "zh-CN": { "langId": 2052, "asciiCode": 936 }, "cs-CZ": { "langId": 1029, "asciiCode": 1250 }, "da-DK": { "langId": 1030, "asciiCode": 1252 }, "de-DE": { "langId": 1031, "asciiCode": 1252 }, "el-GR": { "langId": 1032, "asciiCode": 1253 }, "en-US": { "langId": 1033, "asciiCode": 1252 }, "es-ES": { "langId": 3082, "asciiCode": 1252 }, "et-EE": { "langId": 1061, "asciiCode": 1257 }, "fi-FI": { "langId": 1035, "asciiCode": 1252 }, "fr-FR": { "langId": 1036, "asciiCode": 1252 }, "he-IL": { "langId": 1037, "asciiCode": 1255 }, "hu-HU": { "langId": 1038, "asciiCode": 1250 }, "it-IT": { "langId": 1040, "asciiCode": 1252 }, "ja-JP": { "langId": 1041, "asciiCode": 932 }, "ko-KR": { "langId": 1042, "asciiCode": 949 }, "lt-LT": { "langId": 1063, "asciiCode": 1257 }, "lv-LV": { "langId": 1062, "asciiCode": 1257 }, "nl-NL": { "langId": 1043, "asciiCode": 1252 }, "nb-NO": { "langId": 1044, "asciiCode": 1252 }, "pl-PL": { "langId": 1045, "asciiCode": 1250 }, "pt-BR": { "langId": 1046, "asciiCode": 1252 }, "pt-PT": { "langId": 2070, "asciiCode": 1252 }, "ro-RO": { "langId": 1048, "asciiCode": 1250 }, "ru-RU": { "langId": 1049, "asciiCode": 1251 }, "hr-HR": { "langId": 1050, "asciiCode": 1250 }, "sk-SK": { "langId": 1051, "asciiCode": 1250 }, "sv-SE": { "langId": 1053, "asciiCode": 1252 }, "th-TH": { "langId": 1054, "asciiCode": 874 }, "tr-TR": { "langId": 1055, "asciiCode": 1254 }, "sl-SI": { "langId": 1060, "asciiCode": 1250 }, "vi-VN": { "langId": 1066, "asciiCode": 1258 }, "eu-ES": { "langId": 1069, "asciiCode": 1252 }, "bg-BG": { "langId": 1026, "asciiCode": 1251 }, "uk-UA": { "langId": 1058, "asciiCode": 1251 }, "sr-Latn-CS": { "langId": 2074, "asciiCode": 1250 } } ================================================ FILE: crates/packager/src/package/wix/main.wxs ================================================ {{#if allow_downgrades}} {{else}} {{/if}} Installed AND NOT UPGRADINGPRODUCTCODE {{#if banner_path}} {{/if}} {{#if dialog_image_path}} {{/if}} {{#if license}} {{/if}} {{#if icon_path}} {{/if}} WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed {{#unless license}} 1 1 {{/unless}} {{#each deep_link_protocols as |protocol| ~}} {{/each~}} {{#each file_associations as |association| ~}} {{#each association.ext as |ext| ~}} {{/each~}} {{/each~}} {{#each binaries as |bin| ~}} {{/each~}} {{resources}} {{#each merge_modules as |msm| ~}} {{/each~}} {{#each resource_file_ids as |resource_file_id| ~}} {{/each~}} {{#each binaries as |bin| ~}} {{/each~}} {{#each component_group_refs as |id| ~}} {{/each~}} {{#each component_refs as |id| ~}} {{/each~}} {{#each feature_group_refs as |id| ~}} {{/each~}} {{#each feature_refs as |id| ~}} {{/each~}} {{#each merge_refs as |id| ~}} {{/each~}} {{#each custom_action_refs as |id| ~}} {{/each~}} {{#if install_webview}} {{#if download_bootstrapper}} {{/if}} {{#if webview2_bootstrapper_path}} {{/if}} {{#if webview2_installer_path}} {{/if}} {{/if}} ================================================ FILE: crates/packager/src/package/wix/mod.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ collections::{BTreeMap, HashMap, HashSet}, fs::{self, File}, io::Write, path::{Path, PathBuf}, process::Command, }; use handlebars::{to_json, Handlebars}; use regex::Regex; use serde::{Deserialize, Serialize}; use uuid::Uuid; use super::Context; use crate::{ codesign::windows as codesign, config::{Config, LogLevel, WixLanguage}, shell::CommandExt, util::{self, download_and_verify, extract_zip, HashAlgorithm}, Error, }; pub const WIX_URL: &str = "https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip"; pub const WIX_SHA256: &str = "2c1888d5d1dba377fc7fa14444cf556963747ff9a0a289a3599cf09da03b9e2e"; const WIX_REQUIRED_FILES: &[&str] = &[ "candle.exe", "candle.exe.config", "darice.cub", "light.exe", "light.exe.config", "wconsole.dll", "winterop.dll", "wix.dll", "WixUIExtension.dll", "WixUtilExtension.dll", ]; // A v4 UUID that was generated specifically for cargo-packager, to be used as a // namespace for generating v5 UUIDs from bundle identifier strings. const UUID_NAMESPACE: [u8; 16] = [ 0xfd, 0x85, 0x95, 0xa8, 0x17, 0xa3, 0x47, 0x4e, 0xa6, 0x16, 0x76, 0x14, 0x8d, 0xfa, 0x0c, 0x7b, ]; #[derive(Debug, Deserialize)] struct LanguageMetadata { #[serde(rename = "asciiCode")] ascii_code: usize, #[serde(rename = "langId")] lang_id: usize, } /// Generates a GUID. fn generate_guid(key: &[u8]) -> Uuid { let namespace = Uuid::from_bytes(UUID_NAMESPACE); Uuid::new_v5(&namespace, key) } /// Generates the UUID for the Wix template. fn generate_package_guid(config: &Config) -> Uuid { generate_guid(config.identifier().as_bytes()) } // WiX requires versions to be numeric only in a `major.minor.patch.build` format pub fn convert_version(version_str: &str) -> crate::Result { let version = semver::Version::parse(version_str)?; if version.major > 255 { return Err(Error::InvalidAppVersion( "major number cannot be greater than 255".into(), )); } if version.minor > 255 { return Err(Error::InvalidAppVersion( "minor number cannot be greater than 255".into(), )); } if version.patch > 65535 { return Err(Error::InvalidAppVersion( "patch number cannot be greater than 65535".into(), )); } if !version.build.is_empty() { let build = version.build.parse::(); if build.map(|b| b <= 65535).unwrap_or_default() { return Ok(format!( "{}.{}.{}.{}", version.major, version.minor, version.patch, version.build )); } else { return Err(Error::NonNumericBuildMetadata(Some( "and cannot be greater than 65535 for msi target".into(), ))); } } if !version.pre.is_empty() { let pre = version.pre.parse::(); if pre.is_ok() && pre.unwrap() <= 65535 { return Ok(format!( "{}.{}.{}.{}", version.major, version.minor, version.patch, version.pre )); } else { return Err(Error::NonNumericBuildMetadata(Some( "and cannot be greater than 65535 for msi target".into(), ))); } } Ok(version_str.to_string()) } /// A binary to bundle with WIX. /// External binaries or additional project binaries are represented with this data structure. /// This data structure is needed because WIX requires each path to have its own `id` and `guid`. #[derive(Serialize)] struct Binary { /// the GUID to use on the WIX XML. guid: String, /// the id to use on the WIX XML. id: String, /// the binary path. path: String, } /// Generates the data required for the external binaries. #[tracing::instrument(level = "trace", skip(config))] fn generate_binaries_data(config: &Config) -> crate::Result> { let mut binaries = Vec::new(); let tmp_dir = std::env::temp_dir(); let regex = Regex::new(r"[^\w\d\.]")?; if let Some(external_binaries) = &config.external_binaries { let cwd = std::env::current_dir()?; let target_triple = config.target_triple(); for src in external_binaries { let file_name = src .file_name() .ok_or_else(|| Error::FailedToExtractFilename(src.clone()))? .to_string_lossy(); let src = src.with_file_name(format!("{file_name}-{target_triple}.exe")); let path = cwd.join(src); let bin_path = dunce::canonicalize(&path).map_err(|e| Error::IoWithPath(path, e))?; let dest_file_name = format!("{file_name}.exe"); let dest = tmp_dir.join(&*dest_file_name); fs::copy(&bin_path, &dest).map_err(|e| Error::CopyFile(bin_path, dest.clone(), e))?; binaries.push(Binary { guid: Uuid::new_v4().to_string(), path: dest.into_os_string().into_string().unwrap_or_default(), id: regex .replace_all(&dest_file_name.replace('-', "_"), "") .to_string(), }); } } for bin in &config.binaries { if !bin.main { binaries.push(Binary { guid: Uuid::new_v4().to_string(), path: config .binary_path(bin) .with_extension("exe") .into_os_string() .to_string_lossy() .to_string(), id: regex .replace_all( &bin.path .file_stem() .unwrap() .to_string_lossy() .replace('-', "_"), "", ) .to_string(), }) } } Ok(binaries) } /// A Resource file to bundle with WIX. /// This data structure is needed because WIX requires each path to have its own `id` and `guid`. #[derive(Serialize, Clone)] struct ResourceFile { /// the GUID to use on the WIX XML. guid: String, /// the id to use on the WIX XML. id: String, /// the file path. path: PathBuf, } /// A resource directory to bundle with WIX. /// This data structure is needed because WIX requires each path to have its own `id` and `guid`. #[derive(Serialize)] struct ResourceDirectory { /// the directory path. path: String, /// the directory name of the described resource. name: String, /// the files of the described resource directory. files: Vec, /// the directories that are children of the described resource directory. directories: Vec, } impl ResourceDirectory { /// Adds a file to this directory descriptor. fn add_file(&mut self, file: ResourceFile) { self.files.push(file); } /// Generates the wix XML string to bundle this directory resources recursively fn get_wix_data(self) -> (String, Vec) { let mut files = String::from(""); let mut file_ids = Vec::new(); for file in self.files { file_ids.push(file.id.clone()); files.push_str( format!( r#""#, id = file.id, guid = file.guid, path = file.path.display() ).as_str() ); } let mut directories = String::from(""); for directory in self.directories { let (wix_string, ids) = directory.get_wix_data(); for id in ids { file_ids.push(id) } directories.push_str(wix_string.as_str()); } let wix_string = if self.name.is_empty() { format!("{files}{directories}") } else { format!( r#"{files}{directories}"#, id = Uuid::new_v4().as_simple(), name = self.name, files = files, directories = directories, ) }; (wix_string, file_ids) } } /// Mapper between a resource directory name and its ResourceDirectory descriptor. type ResourceMap = BTreeMap; /// Generates the data required for the resource on wix #[tracing::instrument(level = "trace", skip(config))] fn generate_resource_data(config: &Config) -> crate::Result { let mut resources_map = ResourceMap::new(); for resource in config.resources()? { let resource_entry = ResourceFile { id: format!("I{}", Uuid::new_v4().as_simple()), guid: Uuid::new_v4().to_string(), path: resource.src, }; // split the resource path directories let components_count = resource.target.components().count(); let directories = resource .target .components() .take(components_count - 1) // the last component is the file .collect::>(); // transform the directory structure to a chained vec structure let first_directory = directories .first() .map(|d| d.as_os_str().to_string_lossy().into_owned()) .unwrap_or_else(String::new); if !resources_map.contains_key(&first_directory) { resources_map.insert( first_directory.clone(), ResourceDirectory { path: first_directory.clone(), name: first_directory.clone(), directories: vec![], files: vec![], }, ); } let mut directory_entry = resources_map.get_mut(&first_directory).unwrap(); let mut path = String::new(); // the first component is already parsed on `first_directory` so we skip(1) for directory in directories.into_iter().skip(1) { let directory_name = directory .as_os_str() .to_os_string() .into_string() .unwrap_or_default(); path.push_str(directory_name.as_str()); path.push(std::path::MAIN_SEPARATOR); let index = directory_entry .directories .iter() .position(|f| f.path == path); match index { Some(i) => directory_entry = directory_entry.directories.get_mut(i).unwrap(), None => { directory_entry.directories.push(ResourceDirectory { path: path.clone(), name: directory_name, directories: vec![], files: vec![], }); directory_entry = directory_entry.directories.iter_mut().last().unwrap(); } } } directory_entry.add_file(resource_entry); } Ok(resources_map) } #[derive(Serialize)] struct MergeModule<'a> { name: &'a str, path: &'a PathBuf, } fn clear_env_for_wix(cmd: &mut Command) { cmd.env_clear(); let required_vars: Vec = vec!["SYSTEMROOT".into(), "TMP".into(), "TEMP".into()]; for (k, v) in std::env::vars_os() { let k = k.to_ascii_uppercase(); if required_vars.contains(&k) || k.to_string_lossy().starts_with("CARGO_PACKAGER") { cmd.env(k, v); } } } /// Runs the Candle.exe executable for Wix. Candle parses the wxs file and generates the code for building the installer. fn run_candle( config: &Config, wix_path: &Path, intermediates_path: &Path, arch: &str, wxs_file_path: PathBuf, extensions: Vec, ) -> crate::Result<()> { let main_binary = config.main_binary()?; let mut args = vec![ "-arch".to_string(), arch.to_string(), wxs_file_path.to_string_lossy().to_string(), format!( "-dSourceDir={}", util::display_path(config.binary_path(main_binary)) ), ]; if config.wix().map(|w| w.fips_compliant).unwrap_or_default() { args.push("-fips".into()); } let candle_exe = wix_path.join("candle.exe"); tracing::info!("Running candle for {:?}", wxs_file_path); let mut cmd = Command::new(candle_exe); for ext in extensions { cmd.arg("-ext"); cmd.arg(ext); } clear_env_for_wix(&mut cmd); if let Some(level) = config.log_level { if level >= LogLevel::Debug { cmd.arg("-v"); } } cmd.args(&args) .current_dir(intermediates_path) .output_ok() .map_err(|e| Error::WixFailed("candle.exe".into(), e))?; Ok(()) } /// Runs the Light.exe file. Light takes the generated code from Candle and produces an MSI Installer. fn run_light( config: &Config, wix_path: &Path, intermediates_path: &Path, arguments: Vec, extensions: &Vec, output_path: &Path, ) -> crate::Result<()> { let light_exe = wix_path.join("light.exe"); let mut args: Vec = vec!["-o".to_string(), util::display_path(output_path)]; args.extend(arguments); let mut cmd = Command::new(light_exe); for ext in extensions { cmd.arg("-ext"); cmd.arg(ext); } clear_env_for_wix(&mut cmd); if let Some(level) = config.log_level { if level >= LogLevel::Debug { cmd.arg("-v"); } } cmd.args(&args) .current_dir(intermediates_path) .output_ok() .map_err(|e| Error::WixFailed("light.exe".into(), e))?; Ok(()) } #[tracing::instrument(level = "trace")] fn get_and_extract_wix(path: &Path) -> crate::Result<()> { let data = download_and_verify( "wix311-binaries.zip", WIX_URL, WIX_SHA256, HashAlgorithm::Sha256, )?; tracing::debug!("extracting WIX"); extract_zip(&data, path) } #[tracing::instrument(level = "trace", skip(ctx))] fn build_wix_app_installer(ctx: &Context, wix_path: &Path) -> crate::Result> { let Context { config, intermediates_path, .. } = ctx; let arch = match config.target_arch()? { "x86_64" => "x64", "x86" => "x86", "aarch64" => "arm64", target => return Err(Error::UnsupportedArch("wix".into(), target.into())), }; let main_binary = config.main_binary()?; let main_binary_name = config.main_binary_name()?; let main_binary_path = config.binary_path(main_binary).with_extension("exe"); tracing::debug!("Codesigning {}", main_binary_path.display()); codesign::try_sign(&main_binary_path, config)?; let intermediates_path = intermediates_path.join("wix").join(arch); util::create_clean_dir(&intermediates_path)?; let mut data = BTreeMap::new(); data.insert("product_name", to_json(&config.product_name)); data.insert("version", to_json(convert_version(&config.version)?)); let identifier = config.identifier(); let manufacturer = config.publisher(); data.insert("identifier", to_json(identifier)); data.insert("manufacturer", to_json(manufacturer)); let upgrade_code = Uuid::new_v5( &Uuid::NAMESPACE_DNS, format!("{main_binary_name}.app.x64").as_bytes(), ) .to_string(); data.insert("upgrade_code", to_json(upgrade_code.as_str())); data.insert( "allow_downgrades", to_json(config.windows().map(|w| w.allow_downgrades).unwrap_or(true)), ); let path_guid = generate_package_guid(config).to_string(); data.insert("path_component_guid", to_json(path_guid.as_str())); let shortcut_guid = generate_package_guid(config).to_string(); data.insert("shortcut_guid", to_json(shortcut_guid.as_str())); let binaries = generate_binaries_data(config)?; data.insert("binaries", to_json(binaries)); let resources = generate_resource_data(config)?; let mut resources_wix_string = String::from(""); let mut files_ids = Vec::new(); for (_, dir) in resources { let (wix_string, ids) = dir.get_wix_data(); resources_wix_string.push_str(wix_string.as_str()); for id in ids { files_ids.push(id); } } data.insert("resources", to_json(resources_wix_string)); data.insert("resource_file_ids", to_json(files_ids)); data.insert("app_exe_source", to_json(&main_binary_path)); // copy icon from configured `icons` to resource folder near msi if let Some(icon) = config.find_ico()? { let icon_path = dunce::canonicalize(&icon).map_err(|e| Error::IoWithPath(icon, e))?; data.insert("icon_path", to_json(icon_path)); } if let Some(license) = &config.license_file { if license.ends_with(".rtf") { data.insert("license", to_json(license)); } else { let license_contents = fs::read_to_string(license).map_err(|e| Error::IoWithPath(license.clone(), e))?; let license_rtf = format!( r#"{{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1033{{\fonttbl{{\f0\fnil\fcharset0 Calibri;}}}} {{\*\generator Riched20 10.0.18362}}\viewkind4\uc1 \pard\sa200\sl276\slmult1\f0\fs22\lang9 {}\par }} "#, license_contents.replace('\n', "\\par ") ); let rtf_output_path = intermediates_path.join("LICENSE.rtf"); tracing::debug!("Writing {}", util::display_path(&rtf_output_path)); fs::write(&rtf_output_path, license_rtf) .map_err(|e| Error::IoWithPath(rtf_output_path.clone(), e))?; data.insert("license", to_json(rtf_output_path)); } } let mut fragment_paths = Vec::new(); let mut handlebars = Handlebars::new(); handlebars.register_escape_fn(handlebars::no_escape); let mut custom_template_path = None; if let Some(wix) = config.wix() { data.insert("custom_action_refs", to_json(&wix.custom_action_refs)); data.insert("component_group_refs", to_json(&wix.component_group_refs)); data.insert("component_refs", to_json(&wix.component_refs)); data.insert("feature_group_refs", to_json(&wix.feature_group_refs)); data.insert("feature_refs", to_json(&wix.feature_refs)); data.insert("merge_refs", to_json(&wix.merge_refs)); custom_template_path.clone_from(&wix.template); fragment_paths = wix.fragment_paths.clone().unwrap_or_default(); if let Some(ref inline_fragments) = wix.fragments { tracing::debug!( "Writing inline fragments to {}", util::display_path(&intermediates_path) ); for (idx, fragment) in inline_fragments.iter().enumerate() { let path = intermediates_path.join(format!("inline_fragment{idx}.wxs")); fs::write(&path, fragment).map_err(|e| Error::IoWithPath(path.clone(), e))?; fragment_paths.push(path); } } if let Some(banner_path) = &wix.banner_path { let canonicalized = dunce::canonicalize(banner_path) .map_err(|e| Error::IoWithPath(banner_path.clone(), e))?; data.insert("banner_path", to_json(canonicalized)); } if let Some(dialog_image_path) = &wix.dialog_image_path { let canonicalized = dunce::canonicalize(dialog_image_path) .map_err(|e| Error::IoWithPath(dialog_image_path.clone(), e))?; data.insert("dialog_image_path", to_json(canonicalized)); } if let Some(merge_modules) = &wix.merge_modules { let merge_modules = merge_modules .iter() .map(|path| MergeModule { name: path .file_name() .and_then(|f| f.to_str()) .unwrap_or_default(), path, }) .collect::>(); data.insert("merge_modules", to_json(merge_modules)); } } if let Some(file_associations) = &config.file_associations { data.insert("file_associations", to_json(file_associations)); } if let Some(protocols) = &config.deep_link_protocols { let schemes = protocols .iter() .flat_map(|p| &p.schemes) .collect::>(); data.insert("deep_link_protocols", to_json(schemes)); } if let Some(path) = custom_template_path { handlebars .register_template_string("main.wxs", fs::read_to_string(path)?) .map_err(Box::new)?; } else { handlebars .register_template_string("main.wxs", include_str!("./main.wxs")) .map_err(Box::new)?; } let main_wxs_path = intermediates_path.join("main.wxs"); tracing::debug!("Writing {}", util::display_path(&main_wxs_path)); fs::write(&main_wxs_path, handlebars.render("main.wxs", &data)?) .map_err(|e| Error::IoWithPath(main_wxs_path.clone(), e))?; let mut candle_inputs = vec![(main_wxs_path, Vec::new())]; let current_dir = std::env::current_dir()?; let extension_regex = Regex::new("\"http://schemas.microsoft.com/wix/(\\w+)\"")?; for fragment_path in fragment_paths { let fragment_path = current_dir.join(fragment_path); let fragment = fs::read_to_string(&fragment_path) .map_err(|e| Error::IoWithPath(fragment_path.clone(), e))?; let mut extensions = Vec::new(); for cap in extension_regex.captures_iter(&fragment) { extensions.push(wix_path.join(format!("Wix{}.dll", &cap[1]))); } candle_inputs.push((fragment_path, extensions)); } let mut fragment_extensions = HashSet::new(); //Default extensions fragment_extensions.insert(wix_path.join("WixUIExtension.dll")); fragment_extensions.insert(wix_path.join("WixUtilExtension.dll")); for (path, extensions) in candle_inputs { for ext in &extensions { fragment_extensions.insert(ext.clone()); } run_candle( config, wix_path, &intermediates_path, arch, path, extensions, )?; } let mut output_paths = Vec::new(); let language_map: HashMap = serde_json::from_str(include_str!("./languages.json"))?; let configured_languages = config .wix() .and_then(|w| w.languages.clone()) .unwrap_or_else(|| vec![WixLanguage::default()]); for language in configured_languages { let (language, locale_path) = match language { WixLanguage::Identifier(identifier) => (identifier, None), WixLanguage::Custom { identifier, path } => (identifier, path), }; let language_metadata = language_map.get(&language).ok_or_else(|| { Error::UnsupportedWixLanguage( language.clone(), language_map .keys() .cloned() .collect::>() .join(", "), ) })?; let locale_contents = match locale_path { Some(p) => fs::read_to_string(&p).map_err(|e| Error::IoWithPath(p, e))?, None => format!( r#""#, language.to_lowercase(), ), }; let locale_strings = include_str!("./default-locale-strings.xml") .replace("__language__", &language_metadata.lang_id.to_string()) .replace("__codepage__", &language_metadata.ascii_code.to_string()) .replace("__productName__", &config.product_name); let mut unset_locale_strings = String::new(); let prefix_len = "{value}').unwrap() - prefix_len) .collect::(); if !locale_contents.contains(&id) { unset_locale_strings.push_str(locale_string); } } let locale_contents = locale_contents.replace( "", &format!("{unset_locale_strings}"), ); let locale_path = intermediates_path.join("locale.wxl"); { tracing::debug!("Writing {}", util::display_path(&locale_path)); let mut fileout = File::create(&locale_path) .map_err(|e| Error::IoWithPath(locale_path.clone(), e))?; fileout.write_all(locale_contents.as_bytes())?; } let arguments = vec![ format!( "-cultures:{}", if language == "en-US" { language.to_lowercase() } else { format!("{};en-US", language.to_lowercase()) } ), "-loc".into(), util::display_path(&locale_path), "*.wixobj".into(), ]; let msi_output_path = intermediates_path.join("output.msi"); let msi_path = config.out_dir().join(format!( "{}_{}_{}_{}.msi", main_binary_name, config.version, arch, language )); let msi_path_parent = msi_path .parent() .ok_or_else(|| Error::ParentDirNotFound(msi_path.clone()))?; fs::create_dir_all(msi_path_parent) .map_err(|e| Error::IoWithPath(msi_path_parent.to_path_buf(), e))?; tracing::info!( "Running light.exe to produce {}", util::display_path(&msi_path) ); run_light( config, wix_path, &intermediates_path, arguments, &(fragment_extensions.clone().into_iter().collect()), &msi_output_path, )?; fs::rename(&msi_output_path, &msi_path) .map_err(|e| Error::RenameFile(msi_output_path, msi_path.clone(), e))?; tracing::debug!("Codesigning {}", msi_path.display()); codesign::try_sign(&msi_path, config)?; output_paths.push(msi_path); } Ok(output_paths) } #[tracing::instrument(level = "trace", skip(ctx))] pub(crate) fn package(ctx: &Context) -> crate::Result> { let wix_path = ctx.tools_path.join("WixTools"); if !wix_path.exists() { get_and_extract_wix(&wix_path)?; } else if WIX_REQUIRED_FILES .iter() .any(|p| !wix_path.join(p).exists()) { tracing::warn!("WixTools directory is missing some files. Recreating it."); fs::remove_dir_all(&wix_path).map_err(|e| Error::IoWithPath(wix_path.clone(), e))?; get_and_extract_wix(&wix_path)?; } build_wix_app_installer(ctx, &wix_path) } ================================================ FILE: crates/packager/src/shell.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{ borrow::Cow, io::{BufRead, BufReader}, process::{Command, Output, Stdio}, sync::{Arc, Mutex}, }; pub trait CommandExt { fn output_ok(&mut self) -> std::io::Result; fn output_ok_info(&mut self) -> std::io::Result; fn output_ok_inner(&mut self, level: tracing::Level) -> std::io::Result; } impl CommandExt for Command { fn output_ok(&mut self) -> std::io::Result { self.output_ok_inner(tracing::Level::DEBUG) } fn output_ok_info(&mut self) -> std::io::Result { self.output_ok_inner(tracing::Level::INFO) } fn output_ok_inner(&mut self, level: tracing::Level) -> std::io::Result { tracing::debug!("Running Command `{self:?}`"); self.stdout(Stdio::piped()); self.stderr(Stdio::piped()); let mut child = self.spawn()?; let mut stdout = child.stdout.take().map(BufReader::new).unwrap(); let stdout_lines = Arc::new(Mutex::new(Vec::new())); let stdout_lines_ = stdout_lines.clone(); std::thread::spawn(move || { let mut buf = Vec::new(); let mut lines = stdout_lines_.lock().unwrap(); loop { buf.clear(); if let Ok(0) = stdout.read_until(b'\n', &mut buf) { break; } log( level, "stdout", String::from_utf8_lossy(&buf[..buf.len() - 1]), ); lines.extend(&buf); } }); let mut stderr = child.stderr.take().map(BufReader::new).unwrap(); let stderr_lines = Arc::new(Mutex::new(Vec::new())); let stderr_lines_ = stderr_lines.clone(); std::thread::spawn(move || { let mut buf = Vec::new(); let mut lines = stderr_lines_.lock().unwrap(); loop { buf.clear(); if let Ok(0) = stderr.read_until(b'\n', &mut buf) { break; } log( level, "stderr", String::from_utf8_lossy(&buf[..buf.len() - 1]), ); lines.extend(&buf); } }); let status = child.wait()?; let output = Output { status, stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()), stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()), }; if output.status.success() { Ok(output) } else { Err(std::io::Error::other(format!( "failed to run command: {self:?}\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ))) } } } #[inline] fn log(level: tracing::Level, shell: &str, msg: Cow<'_, str>) { match level { tracing::Level::INFO => tracing::info!(shell = shell, "{msg}"), _ => tracing::debug!(shell = shell, "{msg}"), } } ================================================ FILE: crates/packager/src/sign.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! File singing and signing keys creation and decoding. use std::{ fmt::Debug, fs::{self, OpenOptions}, io::{BufReader, Write}, path::{Path, PathBuf}, str, time::{SystemTime, UNIX_EPOCH}, }; use base64::{engine::general_purpose::STANDARD, Engine}; use serde::{Deserialize, Serialize}; use crate::{ util::{self, PathExt}, Error, }; /// A public and secret key pair. #[derive(Clone, Debug)] pub struct KeyPair { /// Publick key pub pk: String, /// Secret key pub sk: String, } /// Generates a new signing key. If `password` is `None`, it will prompt /// the user for a password, so if you want to skip the prompt, specify and /// empty string as the password. #[tracing::instrument(level = "trace")] pub fn generate_key(password: Option) -> crate::Result { let minisign::KeyPair { pk, sk } = minisign::KeyPair::generate_encrypted_keypair(password)?; let pk_box_str = pk.to_box()?.to_string(); let sk_box_str = sk.to_box(None)?.to_string(); let encoded_pk = base64::engine::general_purpose::STANDARD.encode(pk_box_str); let encoded_sk = base64::engine::general_purpose::STANDARD.encode(sk_box_str); Ok(KeyPair { pk: encoded_pk, sk: encoded_sk, }) } fn decode_base64(base64_key: &str) -> crate::Result { let decoded_str = &base64::engine::general_purpose::STANDARD.decode(base64_key)?[..]; Ok(String::from(str::from_utf8(decoded_str)?)) } /// Decodes a private key using the specified password. #[tracing::instrument(level = "trace")] pub fn decode_private_key( private_key: &str, password: Option<&str>, ) -> crate::Result { let decoded_secret = decode_base64(private_key)?; let sk_box = minisign::SecretKeyBox::from_string(&decoded_secret)?; let sk = sk_box.into_secret_key(password.map(Into::into))?; Ok(sk) } /// Saves a [`KeyPair`] to disk. #[tracing::instrument(level = "trace")] pub fn save_keypair + Debug>( keypair: &KeyPair, path: P, force: bool, ) -> crate::Result<(PathBuf, PathBuf)> { let path = path.as_ref(); let pubkey_path = format!("{}.pub", path.display()); let pk_path = Path::new(&pubkey_path); if path.exists() { if !force { return Err(Error::SigningKeyExists(path.to_path_buf())); } else { fs::remove_file(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; } } if pk_path.exists() { fs::remove_file(pk_path).map_err(|e| Error::IoWithPath(pk_path.to_path_buf(), e))?; } let mut sk_writer = util::create_file(path)?; write!(sk_writer, "{}", keypair.sk)?; sk_writer.flush()?; let mut pk_writer = util::create_file(pk_path)?; write!(pk_writer, "{}", keypair.pk)?; pk_writer.flush()?; Ok(( dunce::canonicalize(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?, dunce::canonicalize(pk_path).map_err(|e| Error::IoWithPath(pk_path.to_path_buf(), e))?, )) } /// Signing configuration. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[non_exhaustive] pub struct SigningConfig { /// The private key to use for signing. pub private_key: String, /// The private key password. /// /// If `None`, user will be prompted to write a password. /// You can skip the prompt by specifying an empty string. pub password: Option, } impl SigningConfig { /// Creates a new [`SigningConfig`]. pub fn new() -> Self { Self::default() } /// Set the private key to use for signing. pub fn private_key>(mut self, private_key: S) -> Self { self.private_key = private_key.into(); self } /// Set the private key password. pub fn password>(mut self, password: S) -> Self { self.password.replace(password.into()); self } } /// Signs a specified file using the specified signing configuration. #[tracing::instrument(level = "trace")] pub fn sign_file + Debug>( config: &SigningConfig, path: P, ) -> crate::Result { let secret_key = decode_private_key(&config.private_key, config.password.as_deref())?; sign_file_with_secret_key(&secret_key, path) } /// Signs a specified file using an already decoded secret key. #[tracing::instrument(level = "trace")] pub fn sign_file_with_secret_key + Debug>( secret_key: &minisign::SecretKey, path: P, ) -> crate::Result { let path = path.as_ref(); let signature_path = path.with_additional_extension("sig"); let signature_path = dunce::simplified(&signature_path); let mut signature_box_writer = util::create_file(signature_path)?; let start = SystemTime::now(); let since_epoch = start.duration_since(UNIX_EPOCH)?.as_secs(); let trusted_comment = format!( "timestamp:{}\tfile:{}", since_epoch, path.file_name() .ok_or_else(|| crate::Error::FailedToExtractFilename(path.to_path_buf()))? .to_string_lossy() ); let file = OpenOptions::new() .read(true) .open(path) .map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; let file_reader = BufReader::new(file); let signature_box = minisign::sign( None, secret_key, file_reader, Some(trusted_comment.as_str()), Some("signature from cargo-packager secret key"), )?; let encoded_signature = STANDARD.encode(signature_box.to_string()); signature_box_writer.write_all(encoded_signature.as_bytes())?; signature_box_writer.flush()?; dunce::canonicalize(signature_path).map_err(|e| crate::Error::IoWithPath(path.to_path_buf(), e)) } ================================================ FILE: crates/packager/src/util.rs ================================================ // Copyright 2016-2019 Cargo-Bundle developers // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use sha2::Digest; use std::{ ffi::OsStr, fs::{self, File}, io::{Cursor, Read, Write}, path::{Path, PathBuf}, process::Command, }; use zip::ZipArchive; use crate::{shell::CommandExt, Error}; #[inline] pub(crate) fn cross_command(script: &str) -> Command { #[cfg(windows)] let mut cmd = Command::new("cmd"); #[cfg(windows)] cmd.arg("/S").arg("/C").arg(script); #[cfg(not(windows))] let mut cmd = Command::new("sh"); cmd.current_dir(dunce::canonicalize(std::env::current_dir().unwrap()).unwrap()); #[cfg(not(windows))] cmd.arg("-c").arg(script); cmd } #[inline] pub fn display_path>(p: P) -> String { dunce::simplified(&p.as_ref().components().collect::()) .display() .to_string() } /// Recursively create a directory and all of its parent components if they /// are missing after Deleting the existing directory (if it exists). #[inline] pub fn create_clean_dir>(path: P) -> crate::Result<()> { let path = path.as_ref(); if path.exists() { fs::remove_dir_all(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; } fs::create_dir_all(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e)) } /// Creates a new file at the given path, creating any parent directories as needed. #[inline] pub(crate) fn create_file(path: &Path) -> crate::Result> { if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; } let file = File::create(path).map_err(|e| Error::IoWithPath(path.to_path_buf(), e))?; Ok(std::io::BufWriter::new(file)) } #[derive(Debug, PartialEq, Eq)] struct RustCfg { target_arch: Option, } fn parse_rust_cfg(cfg: String) -> RustCfg { let target_line = "target_arch=\""; let mut target_arch = None; for line in cfg.split('\n') { if line.starts_with(target_line) { let len = target_line.len(); let arch = line.chars().skip(len).take(line.len() - len - 1).collect(); target_arch.replace(arch); } } RustCfg { target_arch } } /// Try to determine the current target triple. /// /// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an /// error if the current config cannot be determined or is not some combination of the /// following values: /// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc` pub fn target_triple() -> crate::Result { let arch_res = Command::new("rustc").args(["--print", "cfg"]).output_ok(); let arch = match arch_res { Ok(output) => parse_rust_cfg(String::from_utf8_lossy(&output.stdout).into()) .target_arch .expect("could not find `target_arch` when running `rustc --print cfg`."), Err(err) => { tracing:: debug!( "Failed to determine target arch using rustc, error: `{err}`. Falling back to the architecture of the machine that compiled this crate.", ); if cfg!(target_arch = "x86") { "i686".into() } else if cfg!(target_arch = "x86_64") { "x86_64".into() } else if cfg!(target_arch = "arm") { "armv7".into() } else if cfg!(target_arch = "aarch64") { "aarch64".into() } else { return Err(crate::Error::Architecture); } } }; let os = if cfg!(target_os = "linux") { "unknown-linux" } else if cfg!(target_os = "macos") { "apple-darwin" } else if cfg!(target_os = "windows") { "pc-windows" } else if cfg!(target_os = "freebsd") { "unknown-freebsd" } else { return Err(crate::Error::Os); }; let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") { String::from(os) } else { let env = if cfg!(target_env = "gnu") { "gnu" } else if cfg!(target_env = "musl") { "musl" } else if cfg!(target_env = "msvc") { "msvc" } else { return Err(crate::Error::Environment); }; format!("{os}-{env}") }; Ok(format!("{arch}-{os}")) } pub(crate) fn download(url: &str) -> crate::Result> { tracing::debug!("Downloading {}", url); // This is required because ureq does not bind native-tls as the default TLS implementation when rustls is not available. // See #[cfg(feature = "native-tls")] let agent = ureq::AgentBuilder::new() .tls_connector(std::sync::Arc::new( native_tls::TlsConnector::new().unwrap(), )) .try_proxy_from_env(true) .build(); #[cfg(not(feature = "native-tls"))] let agent = ureq::AgentBuilder::new().try_proxy_from_env(true).build(); let response = agent.get(url).call().map_err(Box::new)?; let mut bytes = Vec::new(); response.into_reader().read_to_end(&mut bytes)?; Ok(bytes) } #[derive(Clone, Copy)] pub(crate) enum HashAlgorithm { #[cfg(target_os = "windows")] Sha256, Sha1, } /// Function used to download a file and checks SHA256 to verify the download. pub(crate) fn download_and_verify>( path: P, url: &str, hash: &str, hash_algorithm: HashAlgorithm, ) -> crate::Result> { let data = download(url)?; tracing::debug!("Validating {} hash", path.as_ref().display()); verify_hash(&data, hash, hash_algorithm)?; Ok(data) } pub(crate) fn verify_hash( data: &[u8], hash: &str, hash_algorithm: HashAlgorithm, ) -> crate::Result<()> { match hash_algorithm { #[cfg(target_os = "windows")] HashAlgorithm::Sha256 => { let hasher = sha2::Sha256::new(); verify_data_with_hasher(data, hash, hasher) } HashAlgorithm::Sha1 => { let hasher = sha1::Sha1::new(); verify_data_with_hasher(data, hash, hasher) } } } fn verify_data_with_hasher(data: &[u8], hash: &str, mut hasher: impl Digest) -> crate::Result<()> { hasher.update(data); let url_hash = hasher.finalize().to_vec(); let expected_hash = hex::decode(hash)?; if expected_hash == url_hash { Ok(()) } else { Err(crate::Error::HashError) } } pub(crate) fn verify_file_hash>( path: P, hash: &str, hash_algorithm: HashAlgorithm, ) -> crate::Result<()> { let data = fs::read(&path).map_err(|e| Error::IoWithPath(path.as_ref().to_path_buf(), e))?; verify_hash(&data, hash, hash_algorithm) } /// Extracts the zips from memory into a useable path. pub(crate) fn extract_zip(data: &[u8], path: &Path) -> crate::Result<()> { let cursor = Cursor::new(data); let mut zipa = ZipArchive::new(cursor)?; for i in 0..zipa.len() { let mut file = zipa.by_index(i)?; if let Some(name) = file.enclosed_name() { let dest_path = path.join(name); if file.is_dir() { fs::create_dir_all(&dest_path) .map_err(|e| Error::IoWithPath(dest_path.clone(), e))?; continue; } let parent = dest_path .parent() .ok_or_else(|| crate::Error::ParentDirNotFound(dest_path.clone()))?; if !parent.exists() { fs::create_dir_all(parent) .map_err(|e| Error::IoWithPath(parent.to_path_buf(), e))? } let mut buff: Vec = Vec::new(); file.read_to_end(&mut buff)?; let mut fileout = File::create(dest_path)?; fileout.write_all(&buff)?; } } Ok(()) } #[cfg(windows)] pub(crate) enum Bitness { X86_32, X86_64, Unknown, } #[cfg(windows)] pub(crate) fn os_bitness() -> crate::Result { use windows_sys::Win32::System::{ SystemInformation::{GetNativeSystemInfo, SYSTEM_INFO}, SystemInformation::{PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_INTEL}, }; let mut system_info: SYSTEM_INFO = unsafe { std::mem::zeroed() }; unsafe { GetNativeSystemInfo(&mut system_info) }; Ok( match unsafe { system_info.Anonymous.Anonymous.wProcessorArchitecture } { PROCESSOR_ARCHITECTURE_INTEL => Bitness::X86_32, PROCESSOR_ARCHITECTURE_AMD64 => Bitness::X86_64, _ => Bitness::Unknown, }, ) } /// Returns true if the path has a filename indicating that it is a high-density /// "retina" icon. Specifically, returns true the file stem ends with /// "@2x" (a convention specified by the [Apple developer docs]( /// https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html)).xw pub(crate) fn is_retina>(path: P) -> bool { path.as_ref() .file_stem() .and_then(std::ffi::OsStr::to_str) .map(|stem| stem.ends_with("@2x")) .unwrap_or(false) } // Given a list of icon files, try to produce an ICNS file in the out_dir // and return the path to it. Returns `Ok(None)` if no usable icons // were provided. pub fn create_icns_file(out_dir: &Path, config: &crate::Config) -> crate::Result> { use image::GenericImageView; let icons = config.icons()?; if icons.as_ref().map(|i| i.len()).unwrap_or_default() == 0 { return Ok(None); } // If one of the icon files is already an ICNS file, just use that. if let Some(icons) = icons { fs::create_dir_all(out_dir).map_err(|e| Error::IoWithPath(out_dir.to_path_buf(), e))?; for icon_path in icons { if icon_path.extension() == Some(std::ffi::OsStr::new("icns")) { let dest_path = out_dir.join( icon_path .file_name() .ok_or_else(|| crate::Error::FailedToExtractFilename(icon_path.clone()))?, ); fs::copy(&icon_path, &dest_path) .map_err(|e| Error::CopyFile(icon_path.clone(), dest_path.clone(), e))?; return Ok(Some(dest_path)); } } } // Otherwise, read available images and pack them into a new ICNS file. let mut family = icns::IconFamily::new(); #[inline] fn add_icon_to_family( icon: image::DynamicImage, density: u32, family: &mut icns::IconFamily, ) -> std::io::Result<()> { // Try to add this image to the icon family. Ignore images whose sizes // don't map to any ICNS icon type; print warnings and skip images that // fail to encode. match icns::IconType::from_pixel_size_and_density(icon.width(), icon.height(), density) { Some(icon_type) => { if !family.has_icon_with_type(icon_type) { let icon = make_icns_image(icon)?; family.add_icon_with_type(&icon, icon_type)?; } Ok(()) } None => Err(std::io::Error::new( std::io::ErrorKind::InvalidData, "No matching IconType", )), } } let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![]; if let Some(icons) = config.icons()? { for icon_path in &icons { let icon = image::open(icon_path)?; let density = if is_retina(icon_path) { 2 } else { 1 }; let (w, h) = icon.dimensions(); let orig_size = std::cmp::min(w, h); let next_size_down = 2f32.powf((orig_size as f32).log2().floor()) as u32; if orig_size > next_size_down { images_to_resize.push((icon, next_size_down, density)); } else { add_icon_to_family(icon, density, &mut family)?; } } } for (icon, next_size_down, density) in images_to_resize { let icon = icon.resize_exact( next_size_down, next_size_down, image::imageops::FilterType::Lanczos3, ); add_icon_to_family(icon, density, &mut family)?; } if !family.is_empty() { fs::create_dir_all(out_dir).map_err(|e| Error::IoWithPath(out_dir.to_path_buf(), e))?; let mut dest_path = out_dir.to_path_buf(); dest_path.push(config.product_name.clone()); dest_path.set_extension("icns"); let file = File::create(&dest_path).map_err(|e| Error::IoWithPath(out_dir.to_path_buf(), e))?; let icns_file = std::io::BufWriter::new(file); family.write(icns_file)?; Ok(Some(dest_path)) } else { Err(crate::Error::InvalidIconList) } } // Converts an image::DynamicImage into an icns::Image. fn make_icns_image(img: image::DynamicImage) -> std::io::Result { let pixel_format = match img.color() { image::ColorType::Rgba8 => icns::PixelFormat::RGBA, image::ColorType::Rgb8 => icns::PixelFormat::RGB, image::ColorType::La8 => icns::PixelFormat::GrayAlpha, image::ColorType::L8 => icns::PixelFormat::Gray, _ => { let msg = format!("unsupported ColorType: {:?}", img.color()); return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, msg)); } }; icns::Image::from_data(pixel_format, img.width(), img.height(), img.into_bytes()) } /// Writes a tar file to the given writer containing the given directory. /// /// The generated tar contains the `src_dir` as a whole and not just its files, /// so if we are creating a tar for: /// ```text /// dir/ /// |_ file1 /// |_ file2 /// |_ file3 /// ``` /// the generated tar will contain the following entries: /// ```text /// - dir /// - dir/file1 /// - dir/file2 /// - dir/file3 /// ``` pub fn create_tar_from_dir, W: Write>(src_dir: P, dest_file: W) -> crate::Result { let src_dir = src_dir.as_ref(); let filename = src_dir .file_name() .ok_or_else(|| crate::Error::FailedToExtractFilename(src_dir.to_path_buf()))?; let mut builder = tar::Builder::new(dest_file); builder.follow_symlinks(false); builder.append_dir_all(filename, src_dir)?; builder.into_inner().map_err(Into::into) } pub trait PathExt { fn with_additional_extension(&self, extension: impl AsRef) -> PathBuf; } impl PathExt for Path { fn with_additional_extension(&self, extension: impl AsRef) -> PathBuf { match self.extension() { Some(ext) => { let mut e = ext.to_os_string(); e.push("."); e.push(extension); self.with_extension(e) } None => self.with_extension(extension), } } } #[cfg(test)] mod tests { use super::*; #[test] fn it_appends_ext() { // Something that has an extention getting another suffix. assert_eq!( PathBuf::from("./asset.zip").with_additional_extension("sig"), PathBuf::from("./asset.zip.sig") ); // Something that doesn't have an extention, setting its extension. assert_eq!( PathBuf::from("./executable").with_additional_extension("sig"), PathBuf::from("./executable.sig") ) } } ================================================ FILE: crates/resource-resolver/CHANGELOG.md ================================================ # Changelog ## \[0.1.2] - [`2b6dd55`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2b6dd55eac6733715a4f717af54ff167e1fdcdf8) ([#266](https://www.github.com/crabnebula-dev/cargo-packager/pull/266)) Fix `process-relaunch-dangerous-allow-symlink-macos` feature usage. ### Dependencies - Upgraded to `cargo-packager-utils@0.1.1` ## \[0.1.1] - [`053b50b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/053b50b4b92c769d00a5e8d27b0de5951c034b65)([#192](https://www.github.com/crabnebula-dev/cargo-packager/pull/192)) Added support for Pacman Packages in the Resource Resolver. ## \[0.1.0] - [`cd0242b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/cd0242b8a41b2f7ecb78dfbae04b3a2e1c72c931) Initial Release. ### Dependencies - Upgraded to `cargo-packager-utils@0.1.0` ================================================ FILE: crates/resource-resolver/Cargo.toml ================================================ [package] name = "cargo-packager-resource-resolver" description = "Cargo packager resource resolver" version = "0.1.2" authors = { workspace = true } edition = { workspace = true } license = { workspace = true } repository = { workspace = true } [package.metadata.docs.rs] features = [ "auto-detect-format" ] [dependencies] thiserror = { workspace = true } cargo-packager-utils = { version = "0.1.1", path = "../utils", default-features = false } log = "0.4" heck = "0.5" [features] process-relaunch-dangerous-allow-symlink-macos = [ "cargo-packager-utils/process-relaunch-dangerous-allow-symlink-macos" ] auto-detect-format = [ ] ================================================ FILE: crates/resource-resolver/README.md ================================================ # cargo-packager-resource-resolver Resource resolver for apps that was packaged by [`cargo-packager`](https://docs.rs/cargo-packager). It resolves the root path which contains resources, which was set using the `resources` field of [cargo packager configuration](https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html). ## Get the resource path ```rs use cargo_packager_resource_resolver::{resources_dir, PackageFormat}; let resource_path = resources_dir(PackageFormat::Nsis).unwrap(); ``` ## Automatically detect formats :warning: This feature is only available for Rust apps that were built with cargo packager. 1. Make sure to use the `before_each_package_command` field of [cargo packager configuration](https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html) to build your app (this will not work with the `before_packaging_command` field). 2. Activete the feature `auto-detect-format` for this crate in your Cargo.toml. ```rs use cargo_packager_resource_resolver::{resources_dir, current_format}; let resource_path = resources_dir(current_format().unwrap()).unwrap(); ``` ## Licenses MIT or MIT/Apache 2.0 where applicable. ================================================ FILE: crates/resource-resolver/src/error.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::path::PathBuf; /// The result type of `resource-resolver`. pub type Result = std::result::Result; /// The error type of `resource-resolver`. #[derive(Debug, thiserror::Error)] #[non_exhaustive] pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), /// Unkown package format. #[error("Unkown package format")] UnkownPackageFormat, /// Unsupported package format. #[error("Unsupported package format")] UnsupportedPackageFormat, /// Couldn't find `APPDIR` environment variable. #[error("Couldn't find `APPDIR` environment variable")] AppDirNotFound, /// `APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue. #[error("`APPDIR` or `APPIMAGE` environment variable found but this application was not detected as an AppImage; this might be a security issue.")] InvalidAppImage, /// Couldn't find parent of path. #[error("Couldn't find parent of {0}")] ParentNotFound(PathBuf), } ================================================ FILE: crates/resource-resolver/src/lib.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! # cargo-packager-resource-resolver //! //! Resource resolver for apps that were packaged by [`cargo-packager`](https://docs.rs/cargo-packager). //! //! It resolves the root path which contains resources, which was set using the `resources` //! field of [cargo packager configuration](https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html). //! //! ## Get the resource path //! //! ``` //! use cargo_packager_resource_resolver::{resources_dir, PackageFormat}; //! //! let resource_path = resources_dir(PackageFormat::Nsis).unwrap(); //! ``` //! ## Automatically detect formats //! //!
//! //! This feature is only available for apps that were built with cargo packager. So the node js binding will not work. //! //!
//! //! 1. Make sure to use the `before_each_package_command` field of [cargo packager configuration](https://docs.rs/cargo-packager/latest/cargo_packager/config/struct.Config.html) to build your app (this will not work with the `before_packaging_command` field). //! 2. Active the feature `auto-detect-format`. //! //! ```rs //! use cargo_packager_resource_resolver::{resources_dir, current_format}; //! //! let resource_path = resources_dir(current_format()).unwrap(); //! ``` //! use std::path::PathBuf; use cargo_packager_utils::current_exe::current_exe; pub use cargo_packager_utils::PackageFormat; use error::Result; mod error; pub use error::Error; /// Get the current package format. /// Can only be used if the app was build with cargo-packager /// and when the `before-each-package-command` Cargo feature is enabled. #[cfg(feature = "auto-detect-format")] pub fn current_format() -> crate::Result { // sync with PackageFormat::short_name function of packager crate // maybe having a special crate for the Config struct, // that both packager and resource-resolver could be a // better alternative match std::option_env!("CARGO_PACKAGER_FORMAT") { Some("app") => Ok(PackageFormat::App), Some("dmg") => Ok(PackageFormat::Dmg), Some("wix") => Ok(PackageFormat::Wix), Some("nsis") => Ok(PackageFormat::Nsis), Some("deb") => Ok(PackageFormat::Deb), Some("appimage") => Ok(PackageFormat::AppImage), Some("pacman") => Ok(PackageFormat::Pacman), _ => Err(Error::UnkownPackageFormat), } } /// Retrieve the resource path of your app, packaged with cargo packager. /// /// ## Example /// /// ``` /// use cargo_packager_resource_resolver::{resources_dir, PackageFormat}; /// /// let resource_path = resources_dir(PackageFormat::Nsis).unwrap(); /// ``` pub fn resources_dir(package_format: PackageFormat) -> Result { match package_format { PackageFormat::App | PackageFormat::Dmg => { let exe = current_exe()?; let exe_dir = exe .parent() .ok_or_else(|| Error::ParentNotFound(exe.clone()))?; Ok(exe_dir.join("../Resources")) } PackageFormat::Wix | PackageFormat::Nsis => { let exe = current_exe()?; let exe_dir = exe .parent() .ok_or_else(|| Error::ParentNotFound(exe.clone()))?; Ok(exe_dir.to_path_buf()) } PackageFormat::Deb | PackageFormat::Pacman => { let exe = current_exe()?; let exe_name = exe.file_name().unwrap().to_string_lossy(); let path = format!("/usr/lib/{exe_name}/"); Ok(PathBuf::from(path)) } PackageFormat::AppImage => { let appdir = std::env::var_os("APPDIR").ok_or(Error::AppDirNotFound)?; // validate that we're actually running on an AppImage // an AppImage is mounted to `/$TEMPDIR/.mount_${appPrefix}${hash}` // see https://github.com/AppImage/AppImageKit/blob/1681fd84dbe09c7d9b22e13cdb16ea601aa0ec47/src/runtime.c#L501 // note that it is safe to use `std::env::current_exe` here since we just loaded an AppImage. let is_temp = std::env::current_exe() .map(|p| { p.display() .to_string() .starts_with(&format!("{}/.mount_", std::env::temp_dir().display())) }) .unwrap_or(true); if !is_temp { return Err(Error::InvalidAppImage); } let appdir: &std::path::Path = appdir.as_ref(); let exe = current_exe()?; let exe_name = exe.file_name().unwrap().to_string_lossy(); Ok(PathBuf::from(format!( "{}/usr/lib/{}", appdir.display(), exe_name ))) } _ => Err(Error::UnsupportedPackageFormat), } } ================================================ FILE: crates/updater/CHANGELOG.md ================================================ # Changelog ## \[0.2.3] - [`d861f1a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/d861f1a6b1dfe585014e04234b33d49b1a895219) ([#356](https://www.github.com/crabnebula-dev/cargo-packager/pull/356)) Pull enhancements from tauri-plugin-updater. ## \[0.2.2] - [`af990f8`](https://www.github.com/crabnebula-dev/cargo-packager/commit/af990f848b78fa07fe2aa8f4cc32599557af9bf7) ([#281](https://www.github.com/crabnebula-dev/cargo-packager/pull/281)) Relax `url` dependency version requirement from `2.5` to `2`. ## \[0.2.1] - [`2b6dd55`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2b6dd55eac6733715a4f717af54ff167e1fdcdf8) ([#266](https://www.github.com/crabnebula-dev/cargo-packager/pull/266)) Fix `process-relaunch-dangerous-allow-symlink-macos` feature usage. ### Dependencies - Upgraded to `cargo-packager-utils@0.1.1` ## \[0.2.0] - [`c16d17a`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c16d17ae190f49be3f9e78c5441bee16c0f8fc69) Enable `rustls-tls` feature flag by default. ## \[0.1.4] - [`3ee2290`](https://www.github.com/crabnebula-dev/cargo-packager/commit/3ee2290df518103056b295dae426b38a65293048)([#147](https://www.github.com/crabnebula-dev/cargo-packager/pull/147)) Prevent powershell window from opening when the msi and nsis installer are executed. ## \[0.1.3] - [`0e00ca2`](https://www.github.com/crabnebula-dev/cargo-packager/commit/0e00ca25fc0e71cad4bb7085edda067a184e5ec7)([#146](https://www.github.com/crabnebula-dev/cargo-packager/pull/146)) Enable native certificates via `rustls-native-certs`. ## \[0.1.2] ### Dependencies - Upgraded to `cargo-packager-utils@0.1.0` ## \[0.1.1] - [`feb53a2`](https://www.github.com/crabnebula-dev/cargo-packager/commit/feb53a2f16ef2c8d93ff2d73a4eb318490f33471)([#102](https://www.github.com/crabnebula-dev/cargo-packager/pull/102)) Fix NSIS updater failing to launch when using `basicUi` mode. - [`e58c7e2`](https://www.github.com/crabnebula-dev/cargo-packager/commit/e58c7e2af586927848965aace34139fbe2b7abc4)([#113](https://www.github.com/crabnebula-dev/cargo-packager/pull/113)) Add `process-relaunch-dangerous-allow-symlink-macos` feature flag to control whether to allow relaunching if executable path contains a symlink or not. ## \[0.1.0] - [`c4fa8fd`](https://www.github.com/crabnebula-dev/cargo-packager/commit/c4fa8fd6334b7fd0c32710ea2df0b54aa6bde713) Initial release. ================================================ FILE: crates/updater/Cargo.toml ================================================ [package] name = "cargo-packager-updater" version = "0.2.3" description = "Rust executable updater." authors = ["CrabNebula Ltd.", "Tauri Programme within The Commons Conservancy"] edition = { workspace = true } license = { workspace = true } repository = { workspace = true } [features] default = ["rustls-tls"] process-relaunch-dangerous-allow-symlink-macos = [ "cargo-packager-utils/process-relaunch-dangerous-allow-symlink-macos", ] native-tls = ["reqwest/native-tls"] native-tls-vendored = ["reqwest/native-tls-vendored"] rustls-tls = ["reqwest/rustls-tls-native-roots"] [dependencies] cargo-packager-utils = { version = "0.1.1", path = "../utils" } reqwest = { version = "0.12", default-features = false, features = [ "json", "stream", "blocking", ] } thiserror = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } dunce = { workspace = true } dirs = { workspace = true } semver = { workspace = true } base64 = { workspace = true } time = { workspace = true, features = ["parsing", "formatting"] } http = "1" url = { version = "2", features = ["serde"] } minisign-verify = "0.2" ctor = "0.2" tempfile = "3.12" log = "0.4" percent-encoding = "2" [target."cfg(target_os = \"macos\")".dependencies] tar = { workspace = true } flate2 = "1.0" [dev-dependencies] tiny_http = "0.12" ================================================ FILE: crates/updater/README.md ================================================ # cargo-packager-updater Updater for apps that was packaged by [`cargo-packager`](https://docs.rs/cargo-packager). ## Checking for an update you can check for an update using [`check_update`](https://docs.rs/cargo-packager-updater/latest/cargo_packager_updater/fn.check_update.html) function or construct a new [`Updater`](https://docs.rs/cargo-packager-updater/latest/cargo_packager_updater/struct.Updater.html) using [`UpdaterBuilder`](https://docs.rs/cargo-packager-updater/latest/cargo_packager_updater/struct.UpdaterBuilder.html), both methods require the current version of the app and a [`Config`](https://docs.rs/cargo-packager-updater/latest/cargo_packager_updater/struct.Config.html) that specifies the endpoints to request updates from and the public key of the update signature. ```rs use cargo_packager_updater::{check_update, Config}; let config = Config { endpoints: vec!["http://myserver.com/updates".parse().unwrap()], pubkey: "".into(), ..Default::default() }; if let Some(update) = check_update("0.1.0".parse().unwrap(), config).expect("failed while checking for update") { update.download_and_install().expect("failed to download and install update"); } else { // there is no updates } ``` ## Endpoints Each endpoint optionally could have `{{arch}}`, `{{target}}` or `{{current_version}}` which will be detected and replaced with the appropriate value before making a request to the endpoint. - `{{current_version}}`: The version of the app that is requesting the update. - `{{target}}`: The operating system name (one of `linux`, `windows` or `macos`). - `{{arch}}`: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`). for example: ``` "https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}" ``` will turn into ``` "https://releases.myapp.com/windows/x86_64/0.1.0" ``` if you need more data, you can set additional request headers [`UpdaterBuilder::header`](https://docs.rs/cargo-packager-updater/latest/cargo_packager_updater/struct.UpdaterBuilder.html#method.header) to your liking. ## Endpoint Response The updater expects the endpoint to respond with 2 possible reponses: 1. [`204 No Content`](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2.5) in case there is no updates available. 2. [`200 OK`](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2.1) and a JSON response that could be either a JSON representing all available platform updates or if using endpoints variables (see above) or a header to attach the current updater target, then it can just return information for the requested target. The JSON response is expected to have these fields set: - `version`: must be a valid semver, with or without a leading `v``, meaning that both `1.0.0`and`v1.0.0`are valid. - `url`or`platforms.[target].url`: must be a valid url to the update bundle. - `signature`or`platforms.[target].signature`: must be the content of the generated `.sig`file. The signature may change each time you run build your app so make sure to always update it. - `format`or`platforms.[target].format`: must be one of `app`, `appimage`, `nsis`or`wix`. > [!NOTE] > if using `platforms` object, each key is in the `OS-ARCH` format, where `OS` is one of `linux`, `macos` or `windows`, and `ARCH` is one of `x86_64`, `aarch64`, `i686` or `armv7`, see the example below. It can also contain these optional fields: - `notes`: Here you can add notes about the update, like release notes. - `pub_date`: must be formatted according to [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.8) if present. Here is an example of the two expected JSON formats: - **JSON for all platforms** ```json { "version": "v1.0.0", "notes": "Test version", "pub_date": "2020-06-22T19:25:57Z", "platforms": { "macos-x86_64": { "signature": "Content of app.tar.gz.sig", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz", "format": "app" }, "macos-aarch64": { "signature": "Content of app.tar.gz.sig", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-aarch64.app.tar.gz", "format": "app" }, "linux-x86_64": { "signature": "Content of app.AppImage.sig", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-amd64.AppImage.tar.gz", "format": "appimage" }, "windows-x86_64": { "signature": "Content of app-setup.exe.sig or app.msi.sig, depending on the chosen format", "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x64-setup.nsis.zip", "format": "nsis or wix depending on the chosen format" } } } ``` - **JSON for one platform** ```json { "version": "0.2.0", "pub_date": "2020-09-18T12:29:53+01:00", "url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz", "signature": "Content of the relevant .sig file", "format": "app or nsis or wix or appimage depending on the release target and the chosen format", "notes": "These are some release notes" } ``` ## Update install mode on Windows You can specify which install mode to use on Windows using [`WindowsConfig::install_mode`](https://docs.rs/cargo-packager-updater/latest/cargo_packager_updater/struct.WindowsConfig.html#structfield.install_mode) which can be on of: - `"Passive"`: There will be a small window with a progress bar. The update will be installed without requiring any user interaction. Generally recommended and the default mode. - `"BasicUi"`: There will be a basic user interface shown which requires user interaction to finish the installation. - `"Quiet"`: There will be no progress feedback to the user. With this mode the installer cannot request admin privileges by itself so it only works in user-wide installations or when your app itself already runs with admin privileges. Generally not recommended. ## Licenses MIT or MIT/Apache 2.0 where applicable. ================================================ FILE: crates/updater/src/custom_serialization.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use std::{collections::HashMap, str::FromStr}; use semver::Version; use serde::{de::Error, Deserialize, Deserializer}; use time::OffsetDateTime; use url::Url; use crate::{ReleaseManifestPlatform, RemoteRelease, RemoteReleaseData, UpdateFormat}; fn parse_version<'de, D>(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { let str = String::deserialize(deserializer)?; Version::from_str(str.trim_start_matches('v')).map_err(Error::custom) } impl<'de> Deserialize<'de> for UpdateFormat { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { let lower = String::deserialize(deserializer)?.to_lowercase(); let variant = match lower.as_str() { "nsis" => UpdateFormat::Nsis, "wix" => UpdateFormat::Wix, "app" => UpdateFormat::App, "appimage" => UpdateFormat::AppImage, _ => { return Err(serde::de::Error::custom( "Unkown updater format, expected one of 'nsis', 'wix', 'app' or 'appimage'", )) } }; Ok(variant) } } impl<'de> Deserialize<'de> for RemoteRelease { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct InnerRemoteRelease { #[serde(alias = "name", deserialize_with = "parse_version")] version: Version, notes: Option, pub_date: Option, platforms: Option>, // dynamic platform response url: Option, signature: Option, format: Option, } let release = InnerRemoteRelease::deserialize(deserializer)?; let pub_date = if let Some(date) = release.pub_date { Some( OffsetDateTime::parse(&date, &time::format_description::well_known::Rfc3339) .map_err(|e| { serde::de::Error::custom(format!("invalid value for `pub_date`: {e}")) })?, ) } else { None }; Ok(RemoteRelease { version: release.version, notes: release.notes, pub_date, data: if let Some(platforms) = release.platforms { RemoteReleaseData::Static { platforms } } else { RemoteReleaseData::Dynamic(ReleaseManifestPlatform { url: release.url.ok_or_else(|| { Error::custom("the `url` field was not set on the updater response") })?, signature: release.signature.ok_or_else(|| { Error::custom("the `signature` field was not set on the updater response") })?, format: release.format.ok_or_else(|| { Error::custom("the `format` field was not set on the updater response") })?, }) }, }) } } ================================================ FILE: crates/updater/src/error.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use thiserror::Error; /// All errors that can occur while running the updater. #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { /// Endpoints are not sent. #[error("Updater does not have any endpoints set.")] EmptyEndpoints, /// IO errors. #[error(transparent)] Io(#[from] std::io::Error), /// Semver errors. #[error(transparent)] Semver(#[from] semver::Error), /// Serialization errors. #[error(transparent)] Serialization(#[from] serde_json::Error), /// Could not fetch a valid response from the server. #[error("Could not fetch a valid release JSON from the remote")] ReleaseNotFound, /// Unsupported app architecture. #[error("Unsupported application architecture, expected one of `x86`, `x86_64`, `arm` or `aarch64`.")] UnsupportedArch, /// Unsupported update fromat. #[error("Unsupported update format for the current target")] UnsupportedUpdateFormat, /// Operating system is not supported. #[error("Unsupported OS, expected one of `linux`, `macos` or `windows`.")] UnsupportedOs, /// Failed to determine updater package extract path #[error("Failed to determine updater package extract path.")] FailedToDetermineExtractPath, /// Url parsing errors. #[error(transparent)] UrlParse(#[from] url::ParseError), /// The platform was not found on the updater JSON response. #[error("the platform `{0}` was not found on the response `platforms` object")] TargetNotFound(String), /// Download failed #[error("`{0}`")] Network(String), /// `minisign_verify` errors. #[error(transparent)] Minisign(#[from] minisign_verify::Error), /// `base64` errors. #[error(transparent)] Base64(#[from] base64::DecodeError), /// UTF8 Errors in signature. #[error("The signature {0} could not be decoded, please check if it is a valid base64 string. The signature must be the contents of the `.sig` file generated by the Tauri bundler, as a string.")] SignatureUtf8(String), /// Temp dir is not on same mount mount. This prevents our updater to rename the AppImage to a temp file. #[error("temp directory is not on the same mount point as the AppImage")] TempDirNotOnSameMountPoint, /// The `reqwest` crate errors. #[error(transparent)] Reqwest(#[from] reqwest::Error), /// The `http` crate errors. #[error(transparent)] Http(#[from] http::Error), /// Error returned when persisting a temporary file fails. #[error(transparent)] PersistError(#[from] tempfile::PersistError), } /// Convenience alias for `cargo-packager-updater` crate Result type. pub type Result = std::result::Result; ================================================ FILE: crates/updater/src/lib.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! # cargo-packager-updater //! //! Updater for apps that was packaged by [`cargo-packager`](https://docs.rs/cargo-packager). //! //! ## Checking for an update //! //! you can check for an update using [`check_update`] function or construct a new [`Updater`] //! using [`UpdaterBuilder`], both methods require the current version of the app and //! a [`Config`] that specifies the endpoints to request updates from and the public key of the update signature. //! //! ```no_run //! use cargo_packager_updater::{check_update, Config}; //! //! let config = Config { //! endpoints: vec!["http://myserver.com/updates".parse().unwrap()], //! pubkey: "".into(), //! ..Default::default() //! }; //! if let Some(update) = check_update("0.1.0".parse().unwrap(), config).expect("failed while checking for update") { //! update.download_and_install().expect("failed to download and install update"); //! } else { //! // there is no updates //! } //! //! ``` //! //! ## Endpoints //! //! Each endpoint optionally could have `{{arch}}`, `{{target}}` or `{{current_version}}` //! which will be detected and replaced with the appropriate value before making a request to the endpoint. //! //! - `{{current_version}}`: The version of the app that is requesting the update. //! - `{{target}}`: The operating system name (one of `linux`, `windows` or `macos`). //! - `{{arch}}`: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`). //! //! for example: //! ```text //! "https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}" //! ``` //! will turn into //! ```text //! "https://releases.myapp.com/windows/x86_64/0.1.0" //! ``` //! //! if you need more data, you can set additional request headers [`UpdaterBuilder::header`] to your liking. //! //! ## Endpoint Response //! //! The updater expects the endpoint to respond with 2 possible responses: //! //! 1. [`204 No Content`](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2.5) in case there is no updates available. //! 2. [`200 OK`](https://datatracker.ietf.org/doc/html/rfc2616#section-10.2.1) and a JSON response that could be either a JSON representing all available platform updates //! or if using endpoints variables (see above) or a header to attach the current updater target, //! then it can just return information for the requested target. //! //! The JSON response is expected to have these fields set: //! //! - `version`: must be a valid semver, with or without a leading `v``, meaning that both `1.0.0` and `v1.0.0` are valid. //! - `url` or `platforms.[target].url`: must be a valid url to the update bundle //! - `signature` or `platforms.[target].signature`: must be the content of the generated `.sig` file. The signature may change each time you run build your app so make sure to always update it. //! - `format` or `platforms.[target].format`: must be one of `app`, `appimage`, `nsis` or `wix`. //! //!
//!

//! //! Note //!

//! if using platforms object, each key is in the OS-ARCH format, where OS is one of linux, macos or windows, and ARCH is one of x86_64, aarch64, i686 or armv7, see the example below. //!
//!
//! //! It can also contain these optional fields: //! - `notes`: Here you can add notes about the update, like release notes. //! - `pub_date`: must be formatted according to [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.8) if present. //! //! Here is an example of the two expected JSON formats: //! //! - **JSON for all platforms** //! //! ```json //! { //! "version": "v1.0.0", //! "notes": "Test version", //! "pub_date": "2020-06-22T19:25:57Z", //! "platforms": { //! "macos-x86_64": { //! "signature": "Content of app.tar.gz.sig", //! "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x86_64.app.tar.gz", //! "format": "app" //! }, //! "macos-aarch64": { //! "signature": "Content of app.tar.gz.sig", //! "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-aarch64.app.tar.gz", //! "format": "app" //! }, //! "linux-x86_64": { //! "signature": "Content of app.AppImage.sig", //! "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-amd64.AppImage.tar.gz", //! "format": "appimage" //! }, //! "windows-x86_64": { //! "signature": "Content of app-setup.exe.sig or app.msi.sig, depending on the chosen format", //! "url": "https://github.com/username/reponame/releases/download/v1.0.0/app-x64-setup.nsis.zip", //! "format": "nsis or wix depending on the chosen format" //! } //! } //! } //! ``` //! //! - **JSON for one platform** //! //! ```json //! { //! "version": "0.2.0", //! "pub_date": "2020-09-18T12:29:53+01:00", //! "url": "https://mycompany.example.com/myapp/releases/myrelease.tar.gz", //! "signature": "Content of the relevant .sig file", //! "format": "app or nsis or wix or appimage depending on the release target and the chosen format", //! "notes": "These are some release notes" //! } //! ``` //! //! //! ## Update install mode on Windows //! //! You can specify which install mode to use on Windows using [`WindowsConfig::install_mode`] which can be one of: //! //! - [`"Passive"`](WindowsUpdateInstallMode::Passive): There will be a small window with a progress bar. The update will be installed without requiring any user interaction. Generally recommended and the default mode. //! - [`"BasicUi"`](WindowsUpdateInstallMode::BasicUi): There will be a basic user interface shown which requires user interaction to finish the installation. //! - [`"Quiet"`](WindowsUpdateInstallMode::Quiet): There will be no progress feedback to the user. With this mode the installer cannot request admin privileges by itself so it only works in user-wide installations or when your app itself already runs with admin privileges. Generally not recommended. #![deny(missing_docs)] use base64::Engine; use cargo_packager_utils::current_exe::current_exe; use http::{ header::{ACCEPT, USER_AGENT}, HeaderName, }; use minisign_verify::{PublicKey, Signature}; use percent_encoding::{AsciiSet, CONTROLS}; use reqwest::{ blocking::Client, header::{HeaderMap, HeaderValue}, StatusCode, }; use semver::Version; use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, io::{Cursor, Read}, path::{Path, PathBuf}, time::Duration, }; use time::OffsetDateTime; use url::Url; mod custom_serialization; mod error; pub use crate::error::*; pub use http; pub use reqwest; pub use semver; pub use url; /// Install modes for the Windows update. #[derive(Debug, PartialEq, Eq, Clone, Default, Deserialize, Serialize)] pub enum WindowsUpdateInstallMode { /// Specifies there's a basic UI during the installation process, including a final dialog box at the end. BasicUi, /// The quiet mode means there's no user interaction required. /// Requires admin privileges if the installer does. Quiet, /// Specifies unattended mode, which means the installation only shows a progress bar. #[default] Passive, } impl WindowsUpdateInstallMode { /// Returns the associated `msiexec.exe` arguments. pub fn msiexec_args(&self) -> &'static [&'static str] { match self { Self::BasicUi => &["/qb+"], Self::Quiet => &["/quiet"], Self::Passive => &["/passive"], } } /// Returns the associated nsis arguments. pub fn nsis_args(&self) -> &'static [&'static str] { match self { Self::Passive => &["/P", "/R"], Self::Quiet => &["/S", "/R"], _ => &[], } } } /// The updater configuration for Windows. #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct WindowsConfig { /// Additional arguments given to the NSIS or WiX installer. pub installer_args: Option>, /// The installation mode for the update on Windows. Defaults to `passive`. pub install_mode: Option, } /// Updater configuration. #[derive(Debug, Clone, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Config { /// The updater endpoints. /// /// Each endpoint optionally could have `{{arch}}`, `{{target}}` or `{{current_version}}` /// which will be detected and replaced with the appropriate value before making a request to the endpoint. /// /// - `{{current_version}}`: The version of the app that is requesting the update. /// - `{{target}}`: The operating system name (one of `linux`, `windows` or `macos`). /// - `{{arch}}`: The architecture of the machine (one of `x86_64`, `i686`, `aarch64` or `armv7`). pub endpoints: Vec, /// Signature public key. pub pubkey: String, /// The Windows configuration for the updater. pub windows: Option, } /// Supported update format #[derive(Debug, Serialize, Copy, Clone)] pub enum UpdateFormat { /// The NSIS installer (.exe). Nsis, /// The Microsoft Software Installer (.msi) through WiX Toolset. Wix, /// The Linux AppImage package (.AppImage). AppImage, /// The macOS application bundle (.app). App, } impl std::fmt::Display for UpdateFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{}", match self { UpdateFormat::Nsis => "nsis", UpdateFormat::Wix => "wix", UpdateFormat::AppImage => "appimage", UpdateFormat::App => "app", } ) } } /// Information about a release #[derive(Debug, Deserialize, Serialize, Clone)] pub struct ReleaseManifestPlatform { /// Download URL for the platform pub url: Url, /// Signature for the platform pub signature: String, /// Update format pub format: UpdateFormat, } /// Information about a release data. #[derive(Debug, Deserialize, Serialize, Clone)] #[serde(untagged)] pub enum RemoteReleaseData { /// Dynamic release data based on the platform the update has been requested from. Dynamic(ReleaseManifestPlatform), /// A map of release data for each platform, where the key is `-`. Static { /// A map of release data for each platform, where the key is `-`. platforms: HashMap, }, } /// Information about a release returned by the remote update server. /// /// This type can have one of two shapes: Server Format (Dynamic Format) and Static Format. #[derive(Debug, Clone)] pub struct RemoteRelease { /// Version to install. pub version: Version, /// Release notes. pub notes: Option, /// Release date. pub pub_date: Option, /// Release data. pub data: RemoteReleaseData, } impl RemoteRelease { /// The release's download URL for the given target. pub fn download_url(&self, target: &str) -> Result<&Url> { match self.data { RemoteReleaseData::Dynamic(ref platform) => Ok(&platform.url), RemoteReleaseData::Static { ref platforms } => platforms .get(target) .map_or(Err(Error::TargetNotFound(target.to_string())), |p| { Ok(&p.url) }), } } /// The release's signature for the given target. pub fn signature(&self, target: &str) -> Result<&String> { match self.data { RemoteReleaseData::Dynamic(ref platform) => Ok(&platform.signature), RemoteReleaseData::Static { ref platforms } => platforms .get(target) .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| { Ok(&platform.signature) }), } } /// The release's update format for the given target. pub fn format(&self, target: &str) -> Result { match self.data { RemoteReleaseData::Dynamic(ref platform) => Ok(platform.format), RemoteReleaseData::Static { ref platforms } => platforms .get(target) .map_or(Err(Error::TargetNotFound(target.to_string())), |platform| { Ok(platform.format) }), } } } /// An [`Updater`] builder. pub struct UpdaterBuilder { current_version: Version, config: Config, version_comparator: Option bool + Send + Sync>>, executable_path: Option, target: Option, headers: HeaderMap, timeout: Option, } impl UpdaterBuilder { /// Create a new updater builder request. pub fn new(current_version: Version, config: crate::Config) -> Self { Self { current_version, config, version_comparator: None, executable_path: None, target: None, headers: Default::default(), timeout: None, } } /// A custom function to compare whether a new version exists or not. pub fn version_comparator bool + Send + Sync + 'static>( mut self, f: F, ) -> Self { self.version_comparator = Some(Box::new(f)); self } /// Specify a public key to use when checking if the update is valid. pub fn pub_key(mut self, pub_key: impl Into) -> Self { self.config.pubkey = pub_key.into(); self } /// Specify the target to request an update for. pub fn target(mut self, target: impl Into) -> Self { self.target.replace(target.into()); self } /// Specify the endpoints where an update will be requested from. pub fn endpoints(mut self, endpoints: Vec) -> Self { self.config.endpoints = endpoints; self } /// Specify the path to the current executable where the updater will try to update in the same directory. pub fn executable_path>(mut self, p: P) -> Self { self.executable_path.replace(p.as_ref().into()); self } /// Add a header to the updater request. pub fn header(mut self, key: K, value: V) -> Result where HeaderName: TryFrom, >::Error: Into, HeaderValue: TryFrom, >::Error: Into, { let key: std::result::Result = key.try_into().map_err(Into::into); let value: std::result::Result = value.try_into().map_err(Into::into); self.headers.insert(key?, value?); Ok(self) } /// Specify a timeout for the updater request. pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self } /// Specify custom installer args on Windows. pub fn installer_args(mut self, args: I) -> Self where I: IntoIterator, S: Into, { if self.config.windows.is_none() { self.config.windows.replace(Default::default()); } self.config .windows .as_mut() .unwrap() .installer_args .replace(args.into_iter().map(Into::into).collect()); self } /// Build the updater. pub fn build(self) -> Result { if self.config.endpoints.is_empty() { return Err(Error::EmptyEndpoints); }; let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?; let (target, json_target) = if let Some(target) = self.target { (target.clone(), target) } else { let target = get_updater_target().ok_or(Error::UnsupportedOs)?; (target.to_string(), format!("{target}-{arch}")) }; let executable_path = match self.executable_path { Some(p) => p, #[cfg(not(any(windows, target_os = "macos")))] None => { if let Some(appimage) = std::env::var_os("APPIMAGE").map(PathBuf::from) { appimage } else { current_exe()? } } #[cfg(any(windows, target_os = "macos"))] _ => current_exe()?, }; // Get the extract_path from the provided executable_path #[cfg(any(windows, target_os = "macos"))] let extract_path = extract_path_from_executable(&executable_path)?; #[cfg(not(any(windows, target_os = "macos")))] let extract_path = executable_path; Ok(Updater { config: self.config, current_version: self.current_version, version_comparator: self.version_comparator, timeout: self.timeout, arch, target, json_target, headers: self.headers, extract_path, }) } } /// A type that can check for updates and created by [`UpdaterBuilder`]. pub struct Updater { config: Config, current_version: Version, version_comparator: Option bool + Send + Sync>>, timeout: Option, arch: &'static str, // The `{{target}}` variable we replace in the endpoint target: String, // The value we search if the updater server returns a JSON with the `platforms` object json_target: String, headers: HeaderMap, extract_path: PathBuf, } impl Updater { /// Check for an update. Returns `None` if an update was not found, otherwise it will be `Some`. pub fn check(&self) -> Result> { // we want JSON only let mut headers = self.headers.clone(); if !headers.contains_key(ACCEPT) { headers.insert(ACCEPT, HeaderValue::from_str("application/json").unwrap()); } // Set SSL certs for linux if they aren't available. #[cfg(target_os = "linux")] { if std::env::var_os("SSL_CERT_FILE").is_none() { std::env::set_var("SSL_CERT_FILE", "/etc/ssl/certs/ca-certificates.crt"); } if std::env::var_os("SSL_CERT_DIR").is_none() { std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs"); } } let mut remote_release: Option = None; let mut last_error: Option = None; let version = self.current_version.to_string(); let version = version.as_bytes(); const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+'); let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD); let encoded_version = encoded_version.to_string(); for url in &self.config.endpoints { // replace {{current_version}}, {{target}} and {{arch}} in the provided URL // this is useful if we need to query example // https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}} // will be translated into -> // https://releases.myapp.com/update/macos/aarch64/1.0.0 // The main objective is if the update URL is defined via the Cargo.toml // the URL will be generated dynamically let url: Url = url .to_string() // url::Url automatically url-encodes the path components .replace("%7B%7Bcurrent_version%7D%7D", &encoded_version) .replace("%7B%7Btarget%7D%7D", &self.target) .replace("%7B%7Barch%7D%7D", self.arch) // but not query parameters .replace("{{current_version}}", &encoded_version) .replace("{{target}}", &self.target) .replace("{{arch}}", self.arch) .parse()?; log::debug!("checking for updates {url}"); let mut request = Client::new().get(url).headers(headers.clone()); if let Some(timeout) = self.timeout { request = request.timeout(timeout); } let response = request.send(); match response { Ok(res) => { if res.status().is_success() { // no updates found! if StatusCode::NO_CONTENT == res.status() { log::debug!("update endpoint returned 204 No Content"); return Ok(None); }; let update_response: serde_json::Value = res.json()?; log::debug!("update response: {update_response:?}"); match serde_json::from_value::(update_response) .map_err(Into::into) { Ok(release) => { log::debug!("parsed release response {release:?}"); last_error = None; remote_release = Some(release); // we found a relase, break the loop break; } Err(err) => { log::error!("failed to deserialize update response: {err}"); last_error = Some(err) } } } else { log::error!( "update endpoint did not respond with a successful status code" ); } } Err(err) => { log::error!("failed to check for updates: {err}"); last_error = Some(err.into()); } } } // Last error is cleaned on success. // Shouldn't be triggered if we had a successfull call if let Some(error) = last_error { return Err(error); } // Extracted remote metadata let release = remote_release.ok_or(Error::ReleaseNotFound)?; let should_update = match self.version_comparator.as_ref() { Some(comparator) => comparator(self.current_version.clone(), release.clone()), None => release.version > self.current_version, }; let update = if should_update { Some(Update { current_version: self.current_version.to_string(), config: self.config.clone(), target: self.target.clone(), extract_path: self.extract_path.clone(), version: release.version.to_string(), date: release.pub_date, download_url: release.download_url(&self.json_target)?.to_owned(), body: release.notes.clone(), signature: release.signature(&self.json_target)?.to_owned(), timeout: self.timeout, headers: self.headers.clone(), format: release.format(&self.json_target)?, }) } else { None }; Ok(update) } } /// Information about an update and associted methods to perform the update. #[derive(Debug, Clone)] pub struct Update { /// Config used to check for this update. pub config: Config, /// Update description pub body: Option, /// Version used to check for update pub current_version: String, /// Version announced pub version: String, /// Update publish date pub date: Option, /// Target pub target: String, /// Extract path pub extract_path: PathBuf, /// Download URL announced pub download_url: Url, /// Signature announced pub signature: String, /// Request timeout pub timeout: Option, /// Request headers pub headers: HeaderMap, /// Update format pub format: UpdateFormat, } impl Update { /// Downloads the updater package, verifies it then return it as bytes. /// /// Use [`Update::install`] to install it pub fn download(&self) -> Result> { self.download_extended_inner( None::)>>, None::>, ) } /// Downloads the updater package, verifies it then return it as bytes. /// /// Takes two callbacks, the first will be excuted when receiveing each chunk /// while the second will be called only once when the download finishes. /// /// Use [`Update::install`] to install it pub fn download_extended), D: FnOnce()>( &self, on_chunk: C, on_download_finish: D, ) -> Result> { self.download_extended_inner(Some(on_chunk), Some(on_download_finish)) } fn download_extended_inner), D: FnOnce()>( &self, on_chunk: Option, on_download_finish: Option, ) -> Result> { // set our headers let mut headers = self.headers.clone(); if !headers.contains_key(ACCEPT) { headers.insert( ACCEPT, HeaderValue::from_str("application/octet-stream").unwrap(), ); } if !headers.contains_key(USER_AGENT) { headers.insert( USER_AGENT, HeaderValue::from_str("cargo-packager-updater").unwrap(), ); } let mut request = Client::new() .get(self.download_url.clone()) .headers(headers); if let Some(timeout) = self.timeout { request = request.timeout(timeout); } struct DownloadProgress)> { content_length: Option, inner: R, on_chunk: Option, } impl)> Read for DownloadProgress { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.inner.read(buf).inspect(|&n| { if let Some(on_chunk) = &self.on_chunk { (on_chunk)(n, self.content_length); } }) } } let response = request.send()?; if !response.status().is_success() { return Err(Error::Network(format!( "Download request failed with status: {}", response.status() ))); } let mut source = DownloadProgress { content_length: response .headers() .get("Content-Length") .and_then(|value| value.to_str().ok()) .and_then(|value| value.parse().ok()), inner: response, on_chunk, }; let mut buffer = Vec::new(); let _ = std::io::copy(&mut source, &mut buffer)?; if let Some(on_download_finish) = on_download_finish { on_download_finish(); } let mut update_buffer = Cursor::new(&buffer); verify_signature(&mut update_buffer, &self.signature, &self.config.pubkey)?; Ok(buffer) } /// Installs the updater package downloaded by [`Update::download`] pub fn install(&self, bytes: Vec) -> Result<()> { self.install_inner(bytes) } /// Downloads and installs the updater package pub fn download_and_install(&self) -> Result<()> { let bytes = self.download()?; self.install(bytes) } /// Downloads and installs the updater package /// /// Takes two callbacks, the first will be excuted when receiveing each chunk /// while the second will be called only once when the download finishes. pub fn download_and_install_extended), D: FnOnce()>( &self, on_chunk: C, on_download_finish: D, ) -> Result<()> { let bytes = self.download_extended(on_chunk, on_download_finish)?; self.install(bytes) } // Windows // // ### Expected installers: // │── [AppName]_[version]_x64.msi # Application MSI // │── [AppName]_[version]_x64-setup.exe # NSIS installer // └── ... #[cfg(windows)] fn install_inner(&self, bytes: Vec) -> Result<()> { use std::{io::Write, os::windows::process::CommandExt, process::Command}; let extension = match self.format { UpdateFormat::Nsis => ".exe", UpdateFormat::Wix => ".msi", _ => return Err(crate::Error::UnsupportedUpdateFormat), }; let mut temp_file = tempfile::Builder::new().suffix(extension).tempfile()?; temp_file.write_all(&bytes)?; let (f, path) = temp_file.keep()?; drop(f); let system_root = std::env::var("SYSTEMROOT"); let powershell_path = system_root.as_ref().map_or_else( |_| "powershell.exe".to_string(), |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"), ); const CREATE_NO_WINDOW: u32 = 0x08000000; // we support 2 type of files exe & msi for now // If it's an `exe` we expect an installer not a runtime. match self.format { UpdateFormat::Nsis => { // we need to wrap the installer path in quotes for Start-Process let mut installer_path = std::ffi::OsString::new(); installer_path.push("\""); installer_path.push(&path); installer_path.push("\""); let installer_args = self .config .windows .as_ref() .and_then(|w| w.installer_args.clone()) .unwrap_or_default(); let installer_args = [ self.config .windows .as_ref() .and_then(|w| w.install_mode.clone()) .unwrap_or_default() .nsis_args(), installer_args .iter() .map(AsRef::as_ref) .collect::>() .as_slice(), ] .concat(); // Run the installer let mut cmd = Command::new(powershell_path); cmd.creation_flags(CREATE_NO_WINDOW) .args(["-NoProfile", "-WindowStyle", "Hidden"]) .args(["Start-Process"]) .arg(installer_path); if !installer_args.is_empty() { cmd.arg("-ArgumentList").arg(installer_args.join(", ")); } cmd.spawn().expect("installer failed to start"); std::process::exit(0); } UpdateFormat::Wix => { { // we need to wrap the current exe path in quotes for Start-Process let mut current_exe_arg = std::ffi::OsString::new(); current_exe_arg.push("\""); current_exe_arg.push(current_exe()?); current_exe_arg.push("\""); let mut mis_path = std::ffi::OsString::new(); mis_path.push("\"\"\""); mis_path.push(&path); mis_path.push("\"\"\""); let installer_args = self .config .windows .as_ref() .and_then(|w| w.installer_args.clone()) .unwrap_or_default(); let installer_args = [ self.config .windows .as_ref() .and_then(|w| w.install_mode.clone()) .unwrap_or_default() .msiexec_args(), installer_args .iter() .map(AsRef::as_ref) .collect::>() .as_slice(), ] .concat(); // run the installer and relaunch the application let powershell_install_res = Command::new(powershell_path) .creation_flags(CREATE_NO_WINDOW) .args(["-NoProfile", "-WindowStyle", "Hidden"]) .args([ "Start-Process", "-Wait", "-FilePath", "$env:SYSTEMROOT\\System32\\msiexec.exe", "-ArgumentList", ]) .arg("/i,") .arg(&mis_path) .arg(format!(", {}, /promptrestart;", installer_args.join(", "))) .arg("Start-Process") .arg(current_exe_arg) .spawn(); if powershell_install_res.is_err() { // fallback to running msiexec directly - relaunch won't be available // we use this here in case powershell fails in an older machine somehow let msiexec_path = system_root.as_ref().map_or_else( |_| "msiexec.exe".to_string(), |p| format!("{p}\\System32\\msiexec.exe"), ); let _ = Command::new(msiexec_path) .arg("/i") .arg(mis_path) .args(installer_args) .arg("/promptrestart") .spawn(); } std::process::exit(0); } } _ => unreachable!(), } } // Linux (AppImage) // // ### Expected structure: // ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by cargo-packager // │ └──[AppName]_[version]_amd64.AppImage # Application AppImage // └── ... // // We should have an AppImage already installed to be able to copy and install // the extract_path is the current AppImage path // tmp_dir is where our new AppImage is found #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] fn install_inner(&self, bytes: Vec) -> Result<()> { use std::fs; match self.format { UpdateFormat::AppImage => {} _ => return Err(crate::Error::UnsupportedUpdateFormat), }; let extract_path_metadata = self.extract_path.metadata()?; let tmp_dir_locations = vec![ Box::new(|| Some(std::env::temp_dir())) as Box Option>, Box::new(dirs::cache_dir), Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())), ]; for tmp_dir_location in tmp_dir_locations { if let Some(tmp_dir_root) = tmp_dir_location() { use std::os::unix::fs::{MetadataExt, PermissionsExt}; let tmp_dir = tempfile::Builder::new() .prefix("current_app") .tempdir_in(tmp_dir_root)?; let tmp_dir_metadata = tmp_dir.path().metadata()?; if extract_path_metadata.dev() == tmp_dir_metadata.dev() { let mut perms = tmp_dir_metadata.permissions(); perms.set_mode(0o700); fs::set_permissions(&tmp_dir, perms)?; let tmp_app_image = tmp_dir.path().join("current_app.AppImage"); // get metadata to restore later let metadata = self.extract_path.metadata()?; // create a backup of our current app image fs::rename(&self.extract_path, &tmp_app_image)?; // if something went wrong during the extraction, we should restore previous app if let Err(err) = fs::write(&self.extract_path, bytes).and_then(|_| { fs::set_permissions(&self.extract_path, metadata.permissions()) }) { fs::rename(tmp_app_image, &self.extract_path)?; return Err(err.into()); } // early finish we have everything we need here return Ok(()); } } } Err(Error::TempDirNotOnSameMountPoint) } // MacOS // // ### Expected structure: // ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by cargo-packager // │ └──[AppName].app # Main application // │ └── Contents # Application contents... // │ └── ... // └── ... #[cfg(target_os = "macos")] fn install_inner(&self, bytes: Vec) -> Result<()> { use flate2::read::GzDecoder; let cursor = Cursor::new(bytes); let mut extracted_files: Vec = Vec::new(); // Create temp directories for backup and extraction let tmp_backup_dir = tempfile::Builder::new() .prefix("packager_current_app") .tempdir()?; let tmp_extract_dir = tempfile::Builder::new() .prefix("packager_updated_app") .tempdir()?; let decoder = GzDecoder::new(cursor); let mut archive = tar::Archive::new(decoder); // Extract files to temporary directory for entry in archive.entries()? { let mut entry = entry?; let collected_path: PathBuf = entry.path()?.iter().skip(1).collect(); let extraction_path = tmp_extract_dir.path().join(&collected_path); // Ensure parent directories exist if let Some(parent) = extraction_path.parent() { std::fs::create_dir_all(parent)?; } if let Err(err) = entry.unpack(&extraction_path) { // Cleanup on error std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); return Err(err.into()); } extracted_files.push(extraction_path); } // Try to move the current app to backup let move_result = std::fs::rename( &self.extract_path, tmp_backup_dir.path().join("current_app"), ); let need_authorization = if let Err(err) = move_result { if err.kind() == std::io::ErrorKind::PermissionDenied { true } else { std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); return Err(err.into()); } } else { false }; if need_authorization { log::debug!("app installation needs admin privileges"); // Use AppleScript to perform moves with admin privileges let apple_script = format!( "do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges", src = self.extract_path.display(), new = tmp_extract_dir.path().display() ); let res = std::process::Command::new("osascript") .arg("-e") .arg(apple_script) .status(); if res.is_err() || !res.unwrap().success() { log::error!("failed to install update using AppleScript"); std::fs::remove_dir_all(tmp_extract_dir.path()).ok(); return Err(Error::Io(std::io::Error::new( std::io::ErrorKind::PermissionDenied, "Failed to move the new app into place", ))); } } else { // Remove existing directory if it exists if self.extract_path.exists() { std::fs::remove_dir_all(&self.extract_path)?; } // Move the new app to the target path std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?; } let _ = std::process::Command::new("touch") .arg(&self.extract_path) .status(); Ok(()) } } /// Check for an update using the provided pub fn check_update(current_version: Version, config: crate::Config) -> Result> { UpdaterBuilder::new(current_version, config) .build()? .check() } /// Get the updater target for the current platform. #[doc(hidden)] pub fn target() -> Option { if let (Some(target), Some(arch)) = (get_updater_target(), get_updater_arch()) { Some(format!("{target}-{arch}")) } else { None } } pub(crate) fn get_updater_target() -> Option<&'static str> { if cfg!(target_os = "linux") { Some("linux") } else if cfg!(target_os = "macos") { Some("macos") } else if cfg!(target_os = "windows") { Some("windows") } else { None } } pub(crate) fn get_updater_arch() -> Option<&'static str> { if cfg!(target_arch = "x86") { Some("i686") } else if cfg!(target_arch = "x86_64") { Some("x86_64") } else if cfg!(target_arch = "arm") { Some("armv7") } else if cfg!(target_arch = "aarch64") { Some("aarch64") } else if cfg!(target_arch = "riscv64") { Some("riscv64") } else { None } } #[cfg(any(windows, target_os = "macos"))] fn extract_path_from_executable(executable_path: &Path) -> Result { // Return the path of the current executable by default // Example C:\Program Files\My App\ let extract_path = executable_path .parent() .map(PathBuf::from) .ok_or(Error::FailedToDetermineExtractPath)?; // MacOS example binary is in /Applications/TestApp.app/Contents/MacOS/myApp // We need to get /Applications/.app // TODO(lemarier): Need a better way here // Maybe we could search for <*.app> to get the right path #[cfg(target_os = "macos")] if extract_path .display() .to_string() .contains("Contents/MacOS") { return extract_path .parent() .map(PathBuf::from) .ok_or(Error::FailedToDetermineExtractPath)? .parent() .map(PathBuf::from) .ok_or(Error::FailedToDetermineExtractPath); } Ok(extract_path) } // Validate signature // need to be public because its been used // by our tests in the bundler // // NOTE: The buffer position is not reset. fn verify_signature( archive_reader: &mut R, release_signature: &str, pub_key: &str, ) -> Result where R: Read, { // we need to convert the pub key let pub_key_decoded = base64_to_string(pub_key)?; let public_key = PublicKey::decode(&pub_key_decoded)?; let signature_base64_decoded = base64_to_string(release_signature)?; let signature = Signature::decode(&signature_base64_decoded)?; // read all bytes until EOF in the buffer let mut data = Vec::new(); archive_reader.read_to_end(&mut data)?; // Validate signature or bail out public_key.verify(&data, &signature, true)?; Ok(true) } fn base64_to_string(base64_string: &str) -> Result { let decoded_string = &base64::engine::general_purpose::STANDARD.decode(base64_string)?; let result = std::str::from_utf8(decoded_string) .map_err(|_| Error::SignatureUtf8(base64_string.into()))? .to_string(); Ok(result) } ================================================ FILE: crates/updater/tests/app/Cargo.toml ================================================ [package] name = "cargo-packager-updater-app-test" version = "0.1.0" edition = "2021" [dependencies] cargo-packager-updater = { path = "../.." } ================================================ FILE: crates/updater/tests/app/src/main.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT fn main() { #[allow(clippy::option_env_unwrap)] let version = option_env!("APP_VERSION").unwrap(); let mut builder = cargo_packager_updater::UpdaterBuilder::new( version.parse().unwrap(), cargo_packager_updater::Config { pubkey: include_str!("../../dummy.pub.key").into(), endpoints: vec!["http://localhost:3007".parse().unwrap()], ..Default::default() }, ); let format = std::env::var("UPDATER_FORMAT").unwrap_or_default(); match format.as_str() { "nsis" | "wix" => { // NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder builder = builder.installer_args(vec![format!( "{}={}", if format == "nsis" { "/D" } else { "INSTALLDIR" }, std::env::current_exe().unwrap().parent().unwrap().display() )]); } #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] "appimage" => { if let Some(p) = std::env::var_os("APPIMAGE") { builder = builder.executable_path(p); } } _ => {} } let updater = builder.build().unwrap(); println!("{version}"); match updater.check() { Ok(Some(update)) => { if let Err(e) = update.download_and_install() { println!("{e}"); std::process::exit(1); } std::process::exit(0); } Ok(None) => { std::process::exit(0); } Err(e) => { println!("{e}"); std::process::exit(1); } } } ================================================ FILE: crates/updater/tests/dummy.key ================================================ dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5VU1qSHBMT0E4R0JCVGZzbUMzb3ZXeGpGY1NSdm9OaUxaVTFuajd0T2ZKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWlhRnNPUmxKWjBiWnJ6M29Cd0RwOUpqTW1yOFFQK3JTOGdKSi9CajlHZktHajI2ZnprbEM0VUl2MHhGdFdkZWpHc1BpTlJWK2hOTWo0UVZDemMvaFlYVUM4U2twRW9WV1JHenNzUkRKT2RXQ1FCeXlkYUwxelhacmtxOGZJOG1Nb1R6b0VEcWFLVUk9Cg== ================================================ FILE: crates/updater/tests/dummy.pub.key ================================================ dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQ2Njc0OTE5Mzk2Q0ExODkKUldTSm9XdzVHVWxuUmtJdjB4RnRXZGVqR3NQaU5SVitoTk1qNFFWQ3pjL2hZWFVDOFNrcEVvVlcK ================================================ FILE: crates/updater/tests/update.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT #![allow(dead_code, unused_imports)] use std::{ collections::HashMap, fs::{self, File}, path::{Path, PathBuf}, process::Command, }; use serde::Serialize; const UPDATER_PRIVATE_KEY: &str = include_str!("./dummy.key"); #[derive(Serialize)] struct PlatformUpdate { signature: String, url: &'static str, format: &'static str, } #[derive(Serialize)] struct Update { version: &'static str, date: String, platforms: HashMap, } fn build_app(cwd: &Path, root_dir: &Path, version: &str, target: &[UpdaterFormat]) { let mut command = Command::new("cargo"); command .args([ "run", "--package", "cargo-packager", "--", "-vvv", "-f", &target.iter().map(|t|t.name()).collect::>().join(","), "-c", ]) .arg(format!(r#"{{"outDir":"{}","beforePackagingCommand": "cargo build", "identifier": "com.updater-app.test", "productName": "CargoPackagerAppUpdaterTest", "version": "{version}", "icons": ["32x32.png"], "binaries": [{{"path": "cargo-packager-updater-app-test", "main": true}}]}}"#, dunce::simplified(&root_dir.join("target/debug")).to_string_lossy().replace('\\', "\\\\"))) .env("CARGO_PACKAGER_SIGN_PRIVATE_KEY", UPDATER_PRIVATE_KEY) .env("CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD", "") // This is read by the updater app test .env("APP_VERSION", version) .current_dir(cwd.join("tests/app")); let status = command .status() .expect("failed to run cargo-packager to package app"); if !status.code().map(|c| c == 0).unwrap_or(true) { panic!("failed to package app with exit code: {:?}", status.code()); } } #[derive(Copy, Clone)] enum UpdaterFormat { AppImage, App, Wix, Nsis, } impl UpdaterFormat { fn name(self) -> &'static str { match self { Self::AppImage => "appimage", Self::App => "app", Self::Wix => "wix", Self::Nsis => "nsis", } } fn default() -> &'static [Self] { #[cfg(any(target_os = "macos", target_os = "ios"))] return &[Self::App]; #[cfg(target_os = "linux")] return &[Self::AppImage]; #[cfg(windows)] return &[Self::Nsis, Self::Wix]; } } #[test] #[ignore] fn update_app() { let target = cargo_packager_updater::target().expect("running updater test in an unsupported platform"); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let root_dir = manifest_dir.join("../..").canonicalize().unwrap(); // bundle app update build_app(&manifest_dir, &root_dir, "1.0.0", UpdaterFormat::default()); #[cfg(target_os = "linux")] let generated_packages = vec![( UpdaterFormat::AppImage, root_dir.join("target/debug/cargo-packager-updater-app-test_1.0.0_x86_64.AppImage"), )]; #[cfg(target_os = "macos")] let generated_packages: Vec<_> = vec![( UpdaterFormat::App, root_dir.join("target/debug/CargoPackagerAppUpdaterTest.app.tar.gz"), )]; #[cfg(windows)] let generated_packages: Vec<_> = vec![ ( UpdaterFormat::Nsis, root_dir.join("target/debug/cargo-packager-updater-app-test_1.0.0_x64-setup.exe"), ), ( UpdaterFormat::Wix, root_dir.join("target/debug/cargo-packager-updater-app-test_1.0.0_x64_en-US.msi"), ), ]; for (format, update_package_path) in generated_packages { let ext = update_package_path.extension().unwrap().to_str().unwrap(); let signature_path = update_package_path.with_extension(format!("{ext}.sig",)); let signature = fs::read_to_string(&signature_path).unwrap_or_else(|_| { panic!("failed to read signature file {}", signature_path.display()) }); // on macOS, generated bundle doesn't have the version in its name // so we need to move it otherwise it'll be overwritten when we build the next app #[cfg(target_os = "macos")] let update_package_path = { let filename = update_package_path.file_name().unwrap().to_str().unwrap(); let new_path = update_package_path.with_file_name(format!("update-{filename}",)); fs::rename(&update_package_path, &new_path).expect("failed to rename bundle"); new_path }; let update_package = update_package_path.clone(); let target = target.clone(); std::thread::spawn(move || { // start the updater server let server = tiny_http::Server::http("localhost:3007").expect("failed to start updater server"); loop { if let Ok(request) = server.recv() { match request.url() { "/" => { let mut platforms = HashMap::new(); platforms.insert( target.clone(), PlatformUpdate { signature: signature.clone(), url: "http://localhost:3007/download", format: format.name(), }, ); let body = serde_json::to_vec(&Update { version: "1.0.0", date: time::OffsetDateTime::now_utc() .format(&time::format_description::well_known::Rfc3339) .unwrap(), platforms, }) .unwrap(); let len = body.len(); let response = tiny_http::Response::new( tiny_http::StatusCode(200), Vec::new(), std::io::Cursor::new(body), Some(len), None, ); let _ = request.respond(response); } "/download" => { let _ = request.respond(tiny_http::Response::from_file( File::open(&update_package).unwrap_or_else(|_| { panic!("failed to open package {}", update_package.display()) }), )); // close server return; } _ => (), } } } }); // bundle initial app version build_app(&manifest_dir, &root_dir, "0.1.0", &[format]); // install the inital app on Windows to `installdir` #[cfg(windows)] { let install_dir = dunce::simplified(&root_dir.join("target/debug/installdir")) .display() .to_string(); let installer_path = root_dir.join(match format { UpdaterFormat::Nsis => { "target/debug/cargo-packager-updater-app-test_0.1.0_x64-setup.exe" } UpdaterFormat::Wix => { "target/debug/cargo-packager-updater-app-test_0.1.0_x64_en-US.msi" } _ => unreachable!(), }); let installer_path = dunce::simplified(&installer_path); let mut installer_arg = std::ffi::OsString::new(); installer_arg.push("\""); installer_arg.push(installer_path.display().to_string()); installer_arg.push("\""); let status = Command::new("powershell.exe") .args(["-NoProfile", "-WindowStyle", "Hidden"]) .args(["Start-Process"]) .arg(installer_arg) .arg("-Wait") .arg("-ArgumentList") .arg( [ match format { UpdaterFormat::Wix => "/passive", UpdaterFormat::Nsis => "/P", _ => unreachable!(), }, &format!( "{}={}", match format { UpdaterFormat::Wix => "INSTALLDIR", UpdaterFormat::Nsis => "/D", _ => unreachable!(), }, install_dir ), ] .join(", "), ) .status() .expect("failed to run installer"); if !status.success() { panic!("failed to run installer"); } } #[cfg(windows)] let app = root_dir.join("target/debug/installdir/cargo-packager-updater-app-test.exe"); #[cfg(target_os = "linux")] let app = root_dir.join("target/debug/cargo-packager-updater-app-test_0.1.0_x86_64.AppImage"); #[cfg(target_os = "macos")] let app = root_dir.join("target/debug/CargoPackagerAppUpdaterTest.app/Contents/MacOS/cargo-packager-updater-app-test"); // save the current creation time let ctime1 = fs::metadata(&app) .expect("failed to read app metadata") .created() .unwrap(); // run initial app Command::new(&app) // This is read by the updater app test .env("UPDATER_FORMAT", format.name()) .status() .expect("failed to start initial app"); // wait until the update is finished and the new version has been installed // before starting another updater test, this is because we use the same starting binary // and we can't use it while the updater is installing it let mut counter = 0; loop { // check if the main binary creation time has changed since `ctime1` if app.exists() { let ctime2 = fs::metadata(&app) .expect("failed to read app metadata") .created() .unwrap(); if ctime1 != ctime2 { match Command::new(&app).output() { Ok(o) => { let output = String::from_utf8_lossy(&o.stdout).to_string(); let version = output.split_once('\n').unwrap().0; if version == "1.0.0" { println!("app is updated, new version: {version}"); break; } println!("unexpected output (stdout): {output}"); eprintln!("stderr: {}", String::from_utf8_lossy(&o.stderr)); } Err(e) => { eprintln!("failed to check if app was updated: {e}"); } } } } counter += 1; if counter == 10 { panic!("updater test timedout and couldn't verify the update has happened") } std::thread::sleep(std::time::Duration::from_secs(5)); } // force a new build of the updater app test // so `APP_VERSION` env arg would be embedded correctly // for the next format test let _ = Command::new("cargo") .args(["clean", "--package", "cargo-packager-updater-app-test"]) .current_dir(&manifest_dir) .output(); } } ================================================ FILE: crates/utils/CHANGELOG.md ================================================ # Changelog ## \[0.1.1] - [`2b6dd55`](https://www.github.com/crabnebula-dev/cargo-packager/commit/2b6dd55eac6733715a4f717af54ff167e1fdcdf8) ([#266](https://www.github.com/crabnebula-dev/cargo-packager/pull/266)) Fix `process-relaunch-dangerous-allow-symlink-macos` feature usage. ## \[0.1.0] - [`cd0242b`](https://www.github.com/crabnebula-dev/cargo-packager/commit/cd0242b8a41b2f7ecb78dfbae04b3a2e1c72c931) Initial Release. ================================================ FILE: crates/utils/Cargo.toml ================================================ [package] name = "cargo-packager-utils" description = "Utilities for cargo-packager crates" version = "0.1.1" authors = { workspace = true } edition = { workspace = true } license = { workspace = true } repository = { workspace = true } [dependencies] ctor = "0.2" schemars = { workspace = true, optional = true } clap = { workspace = true, optional = true } serde = { workspace = true, optional = true } [features] default = [ "cli" ] cli = [ ] schema = [ "schemars" ] clap = [ "dep:clap" ] serde = [ "dep:serde" ] process-relaunch-dangerous-allow-symlink-macos = [] ================================================ FILE: crates/utils/README.md ================================================ # cargo-packager-utils Utilities for cargo-packager crate and related crates. ================================================ FILE: crates/utils/src/current_exe.rs ================================================ // Copyright 2019-2023 Tauri Programme within The Commons Conservancy // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT /// Retrieves the currently running binary's path, taking into account security considerations. /// /// The path is cached as soon as possible (before even `main` runs) and that value is returned /// repeatedly instead of fetching the path every time. It is possible for the path to not be found, /// or explicitly disabled (see following macOS specific behavior). /// /// # Platform-specific behavior /// /// On `macOS`, this function will return an error if the original path contained any symlinks /// due to less protection on macOS regarding symlinks. This behavior can be disabled by setting the /// `process-relaunch-dangerous-allow-symlink-macos` feature, although it is *highly discouraged*. /// /// # Security /// /// If the above platform-specific behavior does **not** take place, this function uses the /// following resolution. /// /// We canonicalize the path we received from [`std::env::current_exe`] to resolve any soft links. /// This avoids the usual issue of needing the file to exist at the passed path because a valid /// current executable result for our purpose should always exist. Notably, /// [`std::env::current_exe`] also has a security section that goes over a theoretical attack using /// hard links. Let's cover some specific topics that relate to different ways an attacker might /// try to trick this function into returning the wrong binary path. /// /// ## Symlinks ("Soft Links") /// /// [`std::path::Path::canonicalize`] is used to resolve symbolic links to the original path, /// including nested symbolic links (`link2 -> link1 -> bin`). On macOS, any results that include /// a symlink are rejected by default due to lesser symlink protections. This can be disabled, /// **although discouraged**, with the `process-relaunch-dangerous-allow-symlink-macos` feature. /// /// ## Hard Links /// /// A [Hard Link] is a named entry that points to a file in the file system. /// On most systems, this is what you would think of as a "file". The term is /// used on filesystems that allow multiple entries to point to the same file. /// The linked [Hard Link] Wikipedia page provides a decent overview. /// /// In short, unless the attacker was able to create the link with elevated /// permissions, it should generally not be possible for them to hard link /// to a file they do not have permissions to - with exception to possible /// operating system exploits. /// /// There are also some platform-specific information about this below. /// /// ### Windows /// /// Windows requires a permission to be set for the user to create a symlink /// or a hard link, regardless of ownership status of the target. Elevated /// permissions users have the ability to create them. /// /// ### macOS /// /// macOS allows for the creation of symlinks and hard links to any file. /// Accessing through those links will fail if the user who owns the links /// does not have the proper permissions on the original file. /// /// ### Linux /// /// Linux allows for the creation of symlinks to any file. Accessing the /// symlink will fail if the user who owns the symlink does not have the /// proper permissions on the original file. /// /// Linux additionally provides a kernel hardening feature since version /// 3.6 (30 September 2012). Most distributions since then have enabled /// the protection (setting `fs.protected_hardlinks = 1`) by default, which /// means that a vast majority of desktop Linux users should have it enabled. /// **The feature prevents the creation of hardlinks that the user does not own /// or have read/write access to.** [See the patch that enabled this]. /// /// [Hard Link]: https://en.wikipedia.org/wiki/Hard_link /// [See the patch that enabled this]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=800179c9b8a1e796e441674776d11cd4c05d61d7 pub fn current_exe() -> std::io::Result { STARTING_BINARY.cloned() } use ctor::ctor; use std::{ io::{Error, ErrorKind, Result}, path::{Path, PathBuf}, }; /// A cached version of the current binary using [`ctor`] to cache it before even `main` runs. #[ctor] #[used] static STARTING_BINARY: StartingBinary = StartingBinary::new(); /// Represents a binary path that was cached when the program was loaded. struct StartingBinary(std::io::Result); impl StartingBinary { /// Find the starting executable as safely as possible. fn new() -> Self { // see notes on current_exe() for security implications let dangerous_path = match std::env::current_exe() { Ok(dangerous_path) => dangerous_path, error @ Err(_) => return Self(error), }; // note: this only checks symlinks on problematic platforms, see implementation below if let Some(symlink) = Self::has_symlink(&dangerous_path) { return Self(Err(Error::new( ErrorKind::InvalidData, format!("StartingBinary found current_exe() that contains a symlink on a non-allowed platform: {}", symlink.display()), ))); } // we canonicalize the path to resolve any symlinks to the real exe path Self(dangerous_path.canonicalize()) } /// A clone of the [`PathBuf`] found to be the starting path. /// /// Because [`Error`] is not clone-able, it is recreated instead. pub(super) fn cloned(&self) -> Result { // false positive #[allow(clippy::useless_asref)] self.0 .as_ref() .map(Clone::clone) .map_err(|e| Error::new(e.kind(), e.to_string())) } /// We only care about checking this on macOS currently, as it has the least symlink protections. #[cfg(any( not(target_os = "macos"), feature = "process-relaunch-dangerous-allow-symlink-macos" ))] fn has_symlink(_: &Path) -> Option<&Path> { None } /// We only care about checking this on macOS currently, as it has the least symlink protections. #[cfg(all( target_os = "macos", not(feature = "process-relaunch-dangerous-allow-symlink-macos") ))] fn has_symlink(path: &Path) -> Option<&Path> { use std::fs; path.ancestors().find(|ancestor| { matches!( ancestor .symlink_metadata() .as_ref() .map(fs::Metadata::file_type) .as_ref() .map(fs::FileType::is_symlink), Ok(true) ) }) } } ================================================ FILE: crates/utils/src/lib.rs ================================================ // Copyright 2023-2023 CrabNebula Ltd. // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT //! # cargo-packager-utils //! //! Contain reusable components of the cargo-packager ecosystem. use std::fmt::Display; pub mod current_exe; // NOTE: When making changes to this enum, // make sure to also update in updater and resource-resolver bindings if needed /// Types of supported packages by [`cargo-packager`](https://docs.rs/cargo-packager). #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "clap", value(rename_all = "lowercase"))] #[non_exhaustive] pub enum PackageFormat { /// All available package formats for the current platform. /// /// See [`PackageFormat::platform_all`] #[cfg(feature = "cli")] All, /// The default list of package formats for the current platform. /// /// See [`PackageFormat::platform_default`] #[cfg(feature = "cli")] Default, /// The macOS application bundle (.app). App, /// The macOS DMG package (.dmg). Dmg, /// The Microsoft Software Installer (.msi) through WiX Toolset. Wix, /// The NSIS installer (.exe). Nsis, /// The Linux Debian package (.deb). Deb, /// The Linux AppImage package (.AppImage). AppImage, /// The Linux Pacman package (.tar.gz and PKGBUILD) Pacman, } impl Display for PackageFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.short_name()) } } impl PackageFormat { /// Maps a short name to a [PackageFormat]. /// Possible values are "deb", "pacman", "appimage", "dmg", "app", "wix", "nsis". pub fn from_short_name(name: &str) -> Option { // Other types we may eventually want to support: apk. match name { "app" => Some(PackageFormat::App), "dmg" => Some(PackageFormat::Dmg), "wix" => Some(PackageFormat::Wix), "nsis" => Some(PackageFormat::Nsis), "deb" => Some(PackageFormat::Deb), "appimage" => Some(PackageFormat::AppImage), _ => None, } } /// Gets the short name of this [PackageFormat]. pub fn short_name(&self) -> &'static str { match *self { #[cfg(feature = "cli")] PackageFormat::All => "all", #[cfg(feature = "cli")] PackageFormat::Default => "default", PackageFormat::App => "app", PackageFormat::Dmg => "dmg", PackageFormat::Wix => "wix", PackageFormat::Nsis => "nsis", PackageFormat::Deb => "deb", PackageFormat::AppImage => "appimage", PackageFormat::Pacman => "pacman", } } /// Gets the list of the possible package types on the current OS. /// /// - **macOS**: App, Dmg /// - **Windows**: Nsis, Wix /// - **Linux**: Deb, AppImage, Pacman pub fn platform_all() -> &'static [PackageFormat] { &[ #[cfg(target_os = "macos")] PackageFormat::App, #[cfg(target_os = "macos")] PackageFormat::Dmg, #[cfg(target_os = "windows")] PackageFormat::Wix, #[cfg(target_os = "windows")] PackageFormat::Nsis, #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::Deb, #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::AppImage, #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::Pacman, ] } /// Returns the default list of targets this platform /// /// - **macOS**: App, Dmg /// - **Windows**: Nsis /// - **Linux**: Deb, AppImage, Pacman pub fn platform_default() -> &'static [PackageFormat] { &[ #[cfg(target_os = "macos")] PackageFormat::App, #[cfg(target_os = "macos")] PackageFormat::Dmg, #[cfg(target_os = "windows")] PackageFormat::Nsis, #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::Deb, #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::AppImage, #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] PackageFormat::Pacman, ] } /// Gets a number representing priority which used to sort package types /// in an order that guarantees that if a certain package type /// depends on another (like Dmg depending on MacOsBundle), the dependency /// will be built first /// /// The lower the number, the higher the priority pub fn priority(&self) -> u32 { match self { #[cfg(feature = "cli")] PackageFormat::All => 0, #[cfg(feature = "cli")] PackageFormat::Default => 0, PackageFormat::App => 0, PackageFormat::Wix => 0, PackageFormat::Nsis => 0, PackageFormat::Deb => 0, PackageFormat::AppImage => 0, PackageFormat::Pacman => 0, PackageFormat::Dmg => 1, } } } ================================================ FILE: deny.toml ================================================ # Target triples to include when checking. This is essentially our supported target list. [graph] targets = [ { triple = "x86_64-unknown-linux-gnu" }, { triple = "aarch64-unknown-linux-gnu" }, { triple = "x86_64-pc-windows-msvc" }, { triple = "i686-pc-windows-msvc" }, { triple = "x86_64-apple-darwin" }, { triple = "aarch64-apple-darwin" }, ] # exclude examples and their dependecies exclude = [ "dioxus-example", "egui-example", "slint-example", "tauri-example", "wry-example", "cargo-packager-updater-app-test", ] [licenses] # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. allow = [ "MIT", "Apache-2.0", "ISC", # Apparently for us it's equivalent to BSD-3 which is considered compatible with MIT and Apache-2.0 "Unicode-DFS-2016", "Unicode-3.0", # Used by webpki-roots and option-ext which we are using without modifications in a larger work, therefore okay. "CDLA-Permissive-2.0", "MPL-2.0", "BSD-3-Clause", "BSD-2-Clause", "OpenSSL", "Zlib", ] # Sigh [[licenses.clarify]] name = "ring" # SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses # https://spdx.org/licenses/OpenSSL.html # ISC - Both BoringSSL and ring use this for their new files # MIT - "Files in third_party/ have their own licenses, as described therein. The MIT # license, for third_party/fiat, which, unlike other third_party directories, is # compiled into non-test libraries, is included below." # OpenSSL - Obviously expression = "ISC AND MIT AND OpenSSL" license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }] [licenses.private] # If true, ignores workspace crates that aren't published, or are only # published to private registries. # To see how to mark a crate as unpublished (to the official registry), # visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. ignore = true ================================================ FILE: examples/deno/.gitignore ================================================ /dist ================================================ FILE: examples/deno/README.md ================================================ ## Deno example 1. install `deno` first https://deno.land/manual/getting_started/installation 2. package the app ```sh cargo r -p cargo-packager -- -p deno-example --release ``` ================================================ FILE: examples/deno/deno-example.js ================================================ console.log("Hello World!"); ================================================ FILE: examples/deno/packager.json ================================================ { "$schema": "../../crates/packager/schema.json", "name": "deno-example", "outDir": "./dist", "beforePackagingCommand": "deno compile --output dist/deno-example deno-example.js", "productName": "Deno example", "version": "0.0.0", "identifier": "com.deno.example", "resources": ["deno-example.js", "README.md"], "binaries": [{ "path": "deno-example", "main": true }], "icons": ["32x32.png"] } ================================================ FILE: examples/dioxus/.gitignore ================================================ /target /dist ================================================ FILE: examples/dioxus/Cargo.toml ================================================ [package] name = "dioxus-example" version = "0.0.0" edition = "2021" publish = false [dependencies] dioxus = { version = "0.7", features = ["desktop"] } [package.metadata.packager] # TODO: Needs https://github.com/crabnebula-dev/cargo-packager/issues/360 # before-packaging-command = "dx build --platform desktop --release" before-packaging-command = "cargo build --release" out-dir = "./dist" product-name = "Dioxus example" identifier = "com.dioxus.example" resources = ["src/**/*", "Cargo.toml", "README.md"] icons = ["32x32.png"] [package.metadata.packager.deb] depends = ["libgtk-3-0", "libwebkit2gtk-4.1-0", "libayatana-appindicator3-1"] [package.metadata.packager.appimage] libs = [ "WebKitNetworkProcess", "WebKitWebProcess", "libwebkit2gtkinjectedbundle.so", "libayatana-appindicator3.so.1", ] [package.metadata.packager.appimage.linuxdeploy-plugins] "gtk" = "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" [package.metadata.packager.nsis] preinstall-section = """ ; Setup messages ; English LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." LangString webview2DownloadError ${LANG_ENGLISH} "Error: Downloading WebView2 Failed - $0" LangString webview2DownloadSuccess ${LANG_ENGLISH} "WebView2 bootstrapper downloaded successfully" LangString webview2Downloading ${LANG_ENGLISH} "Downloading WebView2 bootstrapper..." LangString webview2InstallError ${LANG_ENGLISH} "Error: Installing WebView2 failed with exit code $1" LangString webview2InstallSuccess ${LANG_ENGLISH} "WebView2 installed successfully" Section PreInstall ; Check if Webview2 is already installed and skip this section ${If} ${RunningX64} ReadRegStr $4 HKLM "SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${Else} ReadRegStr $4 HKLM "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${EndIf} ReadRegStr $5 HKCU "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" StrCmp $4 "" 0 webview2_done StrCmp $5 "" 0 webview2_done Delete "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(webview2Downloading)" nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\\MicrosoftEdgeWebview2Setup.exe" Pop $0 ${If} $0 == 0 DetailPrint "$(webview2DownloadSuccess)" ${Else} DetailPrint "$(webview2DownloadError)" Abort "$(webview2AbortError)" ${EndIf} StrCpy $6 "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(installingWebview2)" ; $6 holds the path to the webview2 installer ExecWait "$6 /install" $1 ${If} $1 == 0 DetailPrint "$(webview2InstallSuccess)" ${Else} DetailPrint "$(webview2InstallError)" Abort "$(webview2AbortError)" ${EndIf} webview2_done: SectionEnd """ ================================================ FILE: examples/dioxus/README.md ================================================ ## Dioxus example 1. install `dioxus-cli` first ```sh cargo install dioxus-cli --locked ``` 2. package the app ```sh cargo r -p cargo-packager -- -p dioxus-example --release ``` ================================================ FILE: examples/dioxus/src/main.rs ================================================ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![allow(non_snake_case)] // import the prelude to get access to the `rsx!` macro and the `Scope` and `Element` types use dioxus::prelude::*; fn main() { // launch the dioxus app in a webview launch(App); } // define a component that renders a div with the text "Hello, world!" fn App() -> Element { rsx! { div { "Hello, world!" } } } ================================================ FILE: examples/egui/.gitignore ================================================ /target ================================================ FILE: examples/egui/Cargo.toml ================================================ [package] name = "egui-example" version = "0.0.0" edition = "2021" publish = false [dependencies] eframe = "0.33" [package.metadata.packager] before-packaging-command = "cargo build --release" product-name = "Egui example" identifier = "com.egui.example" resources = ["Cargo.toml", "src", "32x32.png"] icons = ["32x32.png"] ================================================ FILE: examples/egui/README.md ================================================ ## EGUI example 1. package the app ```sh cargo r -p cargo-packager -- -p egui-example --release ``` ================================================ FILE: examples/egui/src/main.rs ================================================ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release use eframe::egui; fn main() -> Result<(), eframe::Error> { eframe::run_native( "My egui App", eframe::NativeOptions::default(), Box::new(|_cc| Ok(Box::::default())), ) } struct MyApp { name: String, age: u32, } impl Default for MyApp { fn default() -> Self { Self { name: "Arthur".to_owned(), age: 42, } } } impl eframe::App for MyApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.heading("My egui Application"); ui.horizontal(|ui| { let name_label = ui.label("Your name: "); ui.text_edit_singleline(&mut self.name) .labelled_by(name_label.id); }); ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age")); if ui.button("Click each year").clicked() { self.age += 1; } ui.label(format!("Hello '{}', age {}", self.name, self.age)); }); } } ================================================ FILE: examples/electron/.gitignore ================================================ node_modules/ dist/ ================================================ FILE: examples/electron/index.html ================================================ Hello World!

Hello World!

We are using Node.js , Chromium , and Electron . ================================================ FILE: examples/electron/main.js ================================================ const { app, BrowserWindow } = require("electron"); const path = require("node:path"); const createWindow = () => { const win = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, "preload.js"), }, }); win.loadFile("index.html"); }; app.whenReady().then(() => { createWindow(); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }); ================================================ FILE: examples/electron/package.json ================================================ { "name": "electron-app", "productName": "ElectronApp", "version": "1.0.0", "description": "Hello World!", "main": "main.js", "repository": { "type": "git", "url": "git+https://github.com/crabnebula-dev/cargo-packager.git" }, "author": "CrabNebula Ltd.", "license": "MIT", "bugs": { "url": "https://github.com/crabnebula-dev/cargo-packager/issues" }, "homepage": "https://github.com/crabnebula-dev/cargo-packager#readme", "devDependencies": { "electron": "^27.0.3" }, "packager": { "outDir": "./dist", "identifier": "com.electron.example", "icons": [ "electron.png" ] } } ================================================ FILE: examples/electron/preload.js ================================================ window.addEventListener("DOMContentLoaded", () => { const replaceText = (selector, text) => { const element = document.getElementById(selector); if (element) element.innerText = text; }; for (const dependency of ["chrome", "node", "electron"]) { replaceText(`${dependency}-version`, process.versions[dependency]); } }); ================================================ FILE: examples/slint/.gitignore ================================================ /target ================================================ FILE: examples/slint/Cargo.toml ================================================ [package] name = "slint-example" version = "0.0.0" edition = "2021" publish = false [dependencies] slint = "1.14" [build-dependencies] slint-build = "1.14" [package.metadata.packager] before-packaging-command = "cargo build --release" product-name = "Slint example" identifier = "com.slint.example" resources = ["Cargo.toml", "src", "32x32.png"] icons = ["32x32.png"] ================================================ FILE: examples/slint/README.md ================================================ ## Slint example 1. package the app ```sh cargo r -p cargo-packager -- -p slint-example --release ``` ================================================ FILE: examples/slint/build.rs ================================================ fn main() { slint_build::compile("ui/appwindow.slint").unwrap(); } ================================================ FILE: examples/slint/src/main.rs ================================================ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![allow(dead_code)] slint::include_modules!(); fn main() -> Result<(), slint::PlatformError> { let ui = AppWindow::new()?; let ui_handle = ui.as_weak(); ui.on_request_increase_value(move || { let ui = ui_handle.unwrap(); ui.set_counter(ui.get_counter() + 1); }); ui.run() } ================================================ FILE: examples/slint/ui/appwindow.slint ================================================ import { Button, VerticalBox } from "std-widgets.slint"; export component AppWindow inherits Window { in-out property counter: 42; callback request-increase-value(); VerticalBox { Text { text: "Counter: \{root.counter}"; } Button { text: "Increase value"; clicked => { root.request-increase-value(); } } } } ================================================ FILE: examples/tauri/.gitignore ================================================ /target /gen ================================================ FILE: examples/tauri/Cargo.toml ================================================ [package] name = "tauri-example" version = "0.0.0" description = "Tauri Example" edition = "2021" publish = false [build-dependencies] tauri-build = { version = "2.5", features = [] } [dependencies] tauri = { version = "2.9", features = ["devtools"] } serde.workspace = true serde_json.workspace = true cargo-packager-updater = { path = "../../crates/updater" } [features] # this feature is used for production builds or when `devPath` points to the filesystem # DO NOT REMOVE!! custom-protocol = ["tauri/custom-protocol"] [package.metadata.packager] before-packaging-command = "cargo tauri build" product-name = "Tauri example" identifier = "com.tauri.example" resources = [ "Cargo.toml", "../../README.md", "icons/*", "src", { src = "src-ui/assets/tauri.svg", target = "path/to/tauri.svg" }, { src = "src-ui", target = "path/to/src-ui" }, { src = "src-ui/assets/*", target = "public" }, ] icons = [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico", ] [package.metadata.packager.deb] depends = ["libgtk-3-0", "libwebkit2gtk-4.1-0", "libayatana-appindicator3-1"] section = "rust" [package.metadata.packager.appimage] bins = ["/usr/bin/xdg-open"] libs = [ "WebKitNetworkProcess", "WebKitWebProcess", "libwebkit2gtkinjectedbundle.so", "libayatana-appindicator3.so.1", ] [package.metadata.packager.appimage.linuxdeploy-plugins] "gtk" = "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" [package.metadata.packager.nsis] appdata-paths = ["$LOCALAPPDATA/$IDENTIFIER"] preinstall-section = """ ; Setup messages ; English LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." LangString webview2DownloadError ${LANG_ENGLISH} "Error: Downloading WebView2 Failed - $0" LangString webview2DownloadSuccess ${LANG_ENGLISH} "WebView2 bootstrapper downloaded successfully" LangString webview2Downloading ${LANG_ENGLISH} "Downloading WebView2 bootstrapper..." LangString webview2InstallError ${LANG_ENGLISH} "Error: Installing WebView2 failed with exit code $1" LangString webview2InstallSuccess ${LANG_ENGLISH} "WebView2 installed successfully" Section PreInstall ; Check if Webview2 is already installed and skip this section ${If} ${RunningX64} ReadRegStr $4 HKLM "SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${Else} ReadRegStr $4 HKLM "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${EndIf} ReadRegStr $5 HKCU "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" StrCmp $4 "" 0 webview2_done StrCmp $5 "" 0 webview2_done Delete "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(webview2Downloading)" nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\\MicrosoftEdgeWebview2Setup.exe" Pop $0 ${If} $0 == 0 DetailPrint "$(webview2DownloadSuccess)" ${Else} DetailPrint "$(webview2DownloadError)" Abort "$(webview2AbortError)" ${EndIf} StrCpy $6 "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(installingWebview2)" ; $6 holds the path to the webview2 installer ExecWait "$6 /install" $1 ${If} $1 == 0 DetailPrint "$(webview2InstallSuccess)" ${Else} DetailPrint "$(webview2InstallError)" Abort "$(webview2AbortError)" ${EndIf} webview2_done: SectionEnd """ ================================================ FILE: examples/tauri/README.md ================================================ ## Tauri example 1. install `tauri-cli` first ```sh cargo install tauri-cli --version "2.0.0-rc.10" --locked ``` 2. Change `UPDATER_ENDPOINT` value in `src/main.rs` to point to your updater server or static update file. 3. package the app ```sh cargo r -p cargo-packager -- -p tauri-example --release --private-key dummy.key --password "" ``` 4. increase the version in `Cargo.toml` 5. do step 3 again 6. upload the resulting package from step 5 to your endpoint 7. run the app generated from step 3 ================================================ FILE: examples/tauri/build.rs ================================================ fn main() { tauri_build::build() } ================================================ FILE: examples/tauri/capabilities/base.json ================================================ { "identifier": "base", "description": "app base capabilities", "permissions": ["core:default"] } ================================================ FILE: examples/tauri/dummy.key ================================================ dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5VU1qSHBMT0E4R0JCVGZzbUMzb3ZXeGpGY1NSdm9OaUxaVTFuajd0T2ZKZ0FBQkFBQUFBQUFBQUFBQUlBQUFBQWlhRnNPUmxKWjBiWnJ6M29Cd0RwOUpqTW1yOFFQK3JTOGdKSi9CajlHZktHajI2ZnprbEM0VUl2MHhGdFdkZWpHc1BpTlJWK2hOTWo0UVZDemMvaFlYVUM4U2twRW9WV1JHenNzUkRKT2RXQ1FCeXlkYUwxelhacmtxOGZJOG1Nb1R6b0VEcWFLVUk9Cg== ================================================ FILE: examples/tauri/dummy.pub.key ================================================ dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQ2Njc0OTE5Mzk2Q0ExODkKUldTSm9XdzVHVWxuUmtJdjB4RnRXZGVqR3NQaU5SVitoTk1qNFFWQ3pjL2hZWFVDOFNrcEVvVlcK ================================================ FILE: examples/tauri/src/main.rs ================================================ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use cargo_packager_updater::{Config, Update, UpdaterBuilder}; use tauri::{AppHandle, Emitter, Manager, Runtime}; // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command #[tauri::command] fn greet(name: &str) -> String { format!("Hello, {name}! You've been greeted from Rust!") } #[tauri::command] fn version() -> &'static str { env!("CARGO_PKG_VERSION") } const UPDATER_PUB_KEY: &str = include_str!("../dummy.pub.key"); const UPDATER_ENDPOINT: &str = "http://localhost:2342"; #[tauri::command] fn check_update(app: AppHandle) -> Result<(bool, Option), ()> { let config = Config { pubkey: UPDATER_PUB_KEY.into(), endpoints: vec![UPDATER_ENDPOINT.parse().unwrap()], ..Default::default() }; let updater = { #[allow(unused_mut)] let mut updater_builder = UpdaterBuilder::new(app.package_info().version.clone(), config); #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd" ))] { if let Some(appimage) = app.env().appimage { updater_builder = updater_builder.executable_path(appimage) } } updater_builder.build().unwrap() }; let update = updater.check().unwrap(); let has_update = update.is_some(); let version = update.as_ref().map(|u| u.version.clone()); if let Some(update) = update { app.manage(update); } Ok((has_update, version)) } struct UpdateBytes(Vec); #[derive(serde::Serialize, Clone)] struct ProgressPayload { chunk_len: usize, content_len: Option, } #[tauri::command] fn download_update(app: AppHandle) -> Result<(), ()> { let app_1 = app.clone(); std::thread::spawn(move || { let update = app.state::(); let update_bytes = update .download_extended( move |chunk_len, content_len| { app_1 .emit( "update_progress", ProgressPayload { chunk_len, content_len, }, ) .unwrap(); }, move || {}, ) .unwrap(); app.manage(UpdateBytes(update_bytes)); }); Ok(()) } #[tauri::command] fn install_update(app: AppHandle) -> Result<(), ()> { let update = app.state::(); let update_bytes = app.state::(); update.install(update_bytes.0.clone()).unwrap(); Ok(()) } fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ greet, version, check_update, download_update, install_update ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ================================================ FILE: examples/tauri/src-ui/index.html ================================================ Tauri App
================================================ FILE: examples/tauri/src-ui/main.js ================================================ import { createApp, ref, onMounted, onUnmounted, computed, } from "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"; const { invoke } = window.__TAURI__; const { listen } = window.__TAURI__.event; createApp({ setup() { const greetInput = ref(""); const greetMsg = ref(""); async function greet() { // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command greetMsg.value = await invoke("greet", { name: greetInput.value, }); } const version = ref(""); const updateStatus = ref("unchecked"); // variants: "unchecked" | "has-update" | "no-updates" | "downloading" | "ready-for-install" | "installing" const updateContentLen = ref(0); const updateDownloadedDataLen = ref(0); const updateProgress = ref(0); const updateVersion = ref(""); const checkBtnDisabled = computed( () => updateStatus.value === "has-update" || updateStatus.value == "downloading" || updateStatus.value === "ready-for-install", ); async function checkUpdate() { const [hasUpdate, version] = await invoke("check_update"); if (version) updateVersion.value = version; updateStatus.value = hasUpdate ? "has-update" : "no-updates"; } async function downloadUpdate() { updateStatus.value = "downloading"; await invoke("download_update"); updateStatus.value = "ready-for-install"; } async function installUpdate() { await invoke("install_update"); } let removeProgressListener; onMounted(async () => { version.value = await invoke("version"); removeProgressListener = await listen("update_progress", (event) => { const { chunk_len, content_len } = event.payload; if (content_len) { updateContentLen.value = content_len; } updateDownloadedDataLen.value = updateDownloadedDataLen.value + chunk_len; updateProgress.value = (updateDownloadedDataLen.value / updateContentLen.value) * 100; }); }); onUnmounted(() => removeProgressListener()); return { greetInput, greetMsg, greet, version, updateStatus, updateVersion, updateContentLen, updateDownloadedDataLen, updateProgress, checkBtnDisabled, checkUpdate, downloadUpdate, installUpdate, }; }, template: `

Welcome to Tauri!

Click on the Tauri logo to learn more about the framework

{{greetMsg}}

Current Verion: {{version}}

`, }).mount("#app"); ================================================ FILE: examples/tauri/src-ui/styles.css ================================================ :root { font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color: #0f0f0f; background-color: #f6f6f6; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } .container { margin: 0; padding-top: 10vh; display: flex; flex-direction: column; justify-content: center; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: 0.75s; } .logo.tauri:hover { filter: drop-shadow(0 0 2em #24c8db); } .row { display: flex; justify-content: center; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } h1 { text-align: center; } input, button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; color: #0f0f0f; background-color: #ffffff; transition: border-color 0.25s; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); } button { cursor: pointer; } button:hover { border-color: #396cd8; } button:active { border-color: #396cd8; background-color: #e8e8e8; } input, button { outline: none; } #greet-input { margin-right: 5px; } @media (prefers-color-scheme: dark) { :root { color: #f6f6f6; background-color: #2f2f2f; } a:hover { color: #24c8db; } input, button { color: #ffffff; background-color: #0f0f0f98; } button:active { background-color: #0f0f0f69; } } ================================================ FILE: examples/tauri/tauri.conf.json ================================================ { "productName": "tauri-example", "identifier": "com.tauri.example", "version": "0.0.1", "build": { "beforeDevCommand": "", "beforeBuildCommand": "", "frontendDist": "./src-ui" }, "app": { "withGlobalTauri": true, "windows": [ { "fullscreen": false, "resizable": true, "title": "tauri", "width": 800, "height": 600 } ] }, "bundle": { "active": false, "targets": "all", "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" ] } } ================================================ FILE: examples/wails/.gitignore ================================================ build/bin node_modules frontend/dist ================================================ FILE: examples/wails/Packager.toml ================================================ name = "wails-example" before-packaging-command = "wails build -nopackage" out-dir = "./build/bin" version = "0.0.0" product-name = "Wails example" identifier = "com.wails.example" resources = ["Cargo.toml", "src", "32x32.png"] icons = ["32x32.png"] binaries = [{ path = "wails_example", main = true }] [deb] depends = ["libgtk-3-0", "libwebkit2gtk-4.1-0"] [appimage] libs = [ "WebKitNetworkProcess", "WebKitWebProcess", "libwebkit2gtkinjectedbundle.so", ] [appimage.linuxdeploy-plugins] "gtk" = "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" [nsis] appdata-paths = ["$LOCALAPPDATA/$IDENTIFIER"] preinstall-section = """ ; Setup messages ; English LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." LangString webview2DownloadError ${LANG_ENGLISH} "Error: Downloading WebView2 Failed - $0" LangString webview2DownloadSuccess ${LANG_ENGLISH} "WebView2 bootstrapper downloaded successfully" LangString webview2Downloading ${LANG_ENGLISH} "Downloading WebView2 bootstrapper..." LangString webview2InstallError ${LANG_ENGLISH} "Error: Installing WebView2 failed with exit code $1" LangString webview2InstallSuccess ${LANG_ENGLISH} "WebView2 installed successfully" Section PreInstall ; Check if Webview2 is already installed and skip this section ${If} ${RunningX64} ReadRegStr $4 HKLM "SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${Else} ReadRegStr $4 HKLM "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${EndIf} ReadRegStr $5 HKCU "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" StrCmp $4 "" 0 webview2_done StrCmp $5 "" 0 webview2_done Delete "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(webview2Downloading)" nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\\MicrosoftEdgeWebview2Setup.exe" Pop $0 ${If} $0 == 0 DetailPrint "$(webview2DownloadSuccess)" ${Else} DetailPrint "$(webview2DownloadError)" Abort "$(webview2AbortError)" ${EndIf} StrCpy $6 "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(installingWebview2)" ; $6 holds the path to the webview2 installer ExecWait "$6 /install" $1 ${If} $1 == 0 DetailPrint "$(webview2InstallSuccess)" ${Else} DetailPrint "$(webview2InstallError)" Abort "$(webview2AbortError)" ${EndIf} webview2_done: SectionEnd """ ================================================ FILE: examples/wails/README.md ================================================ ## Wails example 1. install The Go programming language: https://go.dev/dl/ 2. install `wails` CLI first ```sh go install github.com/wailsapp/wails/v2/cmd/wails@latest ``` 3. package the app ```sh cargo r -p cargo-packager -- -p wails-example --release ``` ================================================ FILE: examples/wails/app.go ================================================ package main import ( "context" "fmt" ) // App struct type App struct { ctx context.Context } // NewApp creates a new App application struct func NewApp() *App { return &App{} } // startup is called when the app starts. The context is saved // so we can call the runtime methods func (a *App) startup(ctx context.Context) { a.ctx = ctx } // Greet returns a greeting for the given name func (a *App) Greet(name string) string { return fmt.Sprintf("Hello %s, It's show time!", name) } ================================================ FILE: examples/wails/build.sh ================================================ #!/usr/bin/env -S pkgx +npm +go +gum +jq zsh # ^^ curl https://pkgx.sh | sh # ^^ pkgx makes all those tools (including bash!) available to the script # no packages are installed; your system remains pristine go install github.com/wailsapp/wails/v2/cmd/wails@latest # works on mac export PATH="$HOME/go/bin:$PATH" if [ -d wails_example ]; then cd wails_example elif [ ! -d .git ] && gum confirm 'Create new wails app?'; then wails init -n wails_example -t vanilla cd wails_example fi # probably not resilient if wails changes wails build | grep "Built" | cut -d " " -f 2 | read buildpath echo "Your binary is available at ${buildpath}" mkdir -p ./dist # I am stupid why doesn't this work # cp ${buildpath} ./dist/ cp ./build/bin/wails_example.app/Contents/MacOS/wails_example ./dist # also doesn't work here, sadly # cargo r -p cargo-packager -- -p wails-example --release -c packager.json ================================================ FILE: examples/wails/frontend/app.css ================================================ #logo { display: block; width: 50%; height: 50%; margin: auto; padding: 10% 0 0; background-position: center; background-repeat: no-repeat; background-size: 100% 100%; background-origin: content-box; } .result { height: 20px; line-height: 20px; margin: 1.5rem auto; } .input-box .btn { width: 60px; height: 30px; line-height: 30px; border-radius: 3px; border: none; margin: 0 0 0 20px; padding: 0 8px; cursor: pointer; } .input-box .btn:hover { background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); color: #333333; } .input-box .input { border: none; border-radius: 3px; outline: none; height: 30px; line-height: 30px; padding: 0 10px; background-color: rgba(240, 240, 240, 1); -webkit-font-smoothing: antialiased; } .input-box .input:hover { border: none; background-color: rgba(255, 255, 255, 1); } .input-box .input:focus { border: none; background-color: rgba(255, 255, 255, 1); } ================================================ FILE: examples/wails/frontend/index.html ================================================ wails-example
Please enter your name below 👇
================================================ FILE: examples/wails/frontend/main.js ================================================ import { Greet } from "/wailsjs/go/main/App.js"; let nameElement = document.getElementById("name"); nameElement.focus(); let resultElement = document.getElementById("result"); // Setup the greet function window.greet = () => { // Get name let name = nameElement.value; // Check if the input is empty if (name === "") return; // Call App.Greet(name) try { Greet(name) .then((result) => { // Update result with data back from App.Greet() resultElement.innerText = result; }) .catch((err) => { console.error(err); }); } catch (err) { console.error(err); } }; ================================================ FILE: examples/wails/frontend/style.css ================================================ html { background-color: rgba(27, 38, 54, 1); text-align: center; color: white; } body { margin: 0; color: white; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; } #app { height: 100vh; text-align: center; } ================================================ FILE: examples/wails/frontend/wailsjs/go/main/App.d.ts ================================================ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT export function Greet(arg1: string): Promise; ================================================ FILE: examples/wails/frontend/wailsjs/go/main/App.js ================================================ // @ts-check // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT export function Greet(arg1) { return window["go"]["main"]["App"]["Greet"](arg1); } ================================================ FILE: examples/wails/frontend/wailsjs/runtime/package.json ================================================ { "name": "@wailsapp/runtime", "version": "2.0.0", "description": "Wails Javascript runtime library", "main": "runtime.js", "types": "runtime.d.ts", "scripts": {}, "repository": { "type": "git", "url": "git+https://github.com/wailsapp/wails.git" }, "keywords": [ "Wails", "Javascript", "Go" ], "author": "Lea Anthony ", "license": "MIT", "bugs": { "url": "https://github.com/wailsapp/wails/issues" }, "homepage": "https://github.com/wailsapp/wails#readme" } ================================================ FILE: examples/wails/frontend/wailsjs/runtime/runtime.d.ts ================================================ /* _ __ _ __ | | / /___ _(_) /____ | | /| / / __ `/ / / ___/ | |/ |/ / /_/ / / (__ ) |__/|__/\__,_/_/_/____/ The electron alternative for Go (c) Lea Anthony 2019-present */ export interface Position { x: number; y: number; } export interface Size { w: number; h: number; } export interface Screen { isCurrent: boolean; isPrimary: boolean; width: number; height: number; } // Environment information such as platform, buildtype, ... export interface EnvironmentInfo { buildType: string; platform: string; arch: string; } // [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit) // emits the given event. Optional data may be passed with the event. // This will trigger any event listeners. export function EventsEmit(eventName: string, ...data: any): void; // [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name. export function EventsOn( eventName: string, callback: (...data: any) => void, ): () => void; // [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple) // sets up a listener for the given event name, but will only trigger a given number times. export function EventsOnMultiple( eventName: string, callback: (...data: any) => void, maxCallbacks: number, ): () => void; // [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce) // sets up a listener for the given event name, but will only trigger once. export function EventsOnce( eventName: string, callback: (...data: any) => void, ): () => void; // [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff) // unregisters the listener for the given event name. export function EventsOff( eventName: string, ...additionalEventNames: string[] ): void; // [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall) // unregisters all listeners. export function EventsOffAll(): void; // [LogPrint](https://wails.io/docs/reference/runtime/log#logprint) // logs the given message as a raw message export function LogPrint(message: string): void; // [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace) // logs the given message at the `trace` log level. export function LogTrace(message: string): void; // [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug) // logs the given message at the `debug` log level. export function LogDebug(message: string): void; // [LogError](https://wails.io/docs/reference/runtime/log#logerror) // logs the given message at the `error` log level. export function LogError(message: string): void; // [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal) // logs the given message at the `fatal` log level. // The application will quit after calling this method. export function LogFatal(message: string): void; // [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo) // logs the given message at the `info` log level. export function LogInfo(message: string): void; // [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning) // logs the given message at the `warning` log level. export function LogWarning(message: string): void; // [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload) // Forces a reload by the main application as well as connected browsers. export function WindowReload(): void; // [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp) // Reloads the application frontend. export function WindowReloadApp(): void; // [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop) // Sets the window AlwaysOnTop or not on top. export function WindowSetAlwaysOnTop(b: boolean): void; // [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme) // *Windows only* // Sets window theme to system default (dark/light). export function WindowSetSystemDefaultTheme(): void; // [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme) // *Windows only* // Sets window to light theme. export function WindowSetLightTheme(): void; // [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme) // *Windows only* // Sets window to dark theme. export function WindowSetDarkTheme(): void; // [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter) // Centers the window on the monitor the window is currently on. export function WindowCenter(): void; // [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle) // Sets the text in the window title bar. export function WindowSetTitle(title: string): void; // [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen) // Makes the window full screen. export function WindowFullscreen(): void; // [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen) // Restores the previous window dimensions and position prior to full screen. export function WindowUnfullscreen(): void; // [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen) // Returns the state of the window, i.e. whether the window is in full screen mode or not. export function WindowIsFullscreen(): Promise; // [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize) // Sets the width and height of the window. export function WindowSetSize(width: number, height: number): Promise; // [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize) // Gets the width and height of the window. export function WindowGetSize(): Promise; // [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize) // Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions. // Setting a size of 0,0 will disable this constraint. export function WindowSetMaxSize(width: number, height: number): void; // [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize) // Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions. // Setting a size of 0,0 will disable this constraint. export function WindowSetMinSize(width: number, height: number): void; // [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition) // Sets the window position relative to the monitor the window is currently on. export function WindowSetPosition(x: number, y: number): void; // [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition) // Gets the window position relative to the monitor the window is currently on. export function WindowGetPosition(): Promise; // [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide) // Hides the window. export function WindowHide(): void; // [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow) // Shows the window, if it is currently hidden. export function WindowShow(): void; // [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise) // Maximises the window to fill the screen. export function WindowMaximise(): void; // [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise) // Toggles between Maximised and UnMaximised. export function WindowToggleMaximise(): void; // [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise) // Restores the window to the dimensions and position prior to maximising. export function WindowUnmaximise(): void; // [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised) // Returns the state of the window, i.e. whether the window is maximised or not. export function WindowIsMaximised(): Promise; // [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise) // Minimises the window. export function WindowMinimise(): void; // [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise) // Restores the window to the dimensions and position prior to minimising. export function WindowUnminimise(): void; // [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised) // Returns the state of the window, i.e. whether the window is minimised or not. export function WindowIsMinimised(): Promise; // [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal) // Returns the state of the window, i.e. whether the window is normal or not. export function WindowIsNormal(): Promise; // [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour) // Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels. export function WindowSetBackgroundColour( R: number, G: number, B: number, A: number, ): void; // [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall) // Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system. export function ScreenGetAll(): Promise; // [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl) // Opens the given URL in the system browser. export function BrowserOpenURL(url: string): void; // [Environment](https://wails.io/docs/reference/runtime/intro#environment) // Returns information about the environment export function Environment(): Promise; // [Quit](https://wails.io/docs/reference/runtime/intro#quit) // Quits the application. export function Quit(): void; // [Hide](https://wails.io/docs/reference/runtime/intro#hide) // Hides the application. export function Hide(): void; // [Show](https://wails.io/docs/reference/runtime/intro#show) // Shows the application. export function Show(): void; // [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext) // Returns the current text stored on clipboard export function ClipboardGetText(): Promise; // [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext) // Sets a text on the clipboard export function ClipboardSetText(text: string): Promise; ================================================ FILE: examples/wails/frontend/wailsjs/runtime/runtime.js ================================================ /* _ __ _ __ | | / /___ _(_) /____ | | /| / / __ `/ / / ___/ | |/ |/ / /_/ / / (__ ) |__/|__/\__,_/_/_/____/ The electron alternative for Go (c) Lea Anthony 2019-present */ export function LogPrint(message) { window.runtime.LogPrint(message); } export function LogTrace(message) { window.runtime.LogTrace(message); } export function LogDebug(message) { window.runtime.LogDebug(message); } export function LogInfo(message) { window.runtime.LogInfo(message); } export function LogWarning(message) { window.runtime.LogWarning(message); } export function LogError(message) { window.runtime.LogError(message); } export function LogFatal(message) { window.runtime.LogFatal(message); } export function EventsOnMultiple(eventName, callback, maxCallbacks) { return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks); } export function EventsOn(eventName, callback) { return EventsOnMultiple(eventName, callback, -1); } export function EventsOff(eventName, ...additionalEventNames) { return window.runtime.EventsOff(eventName, ...additionalEventNames); } export function EventsOnce(eventName, callback) { return EventsOnMultiple(eventName, callback, 1); } export function EventsEmit(eventName) { let args = [eventName].slice.call(arguments); return window.runtime.EventsEmit.apply(null, args); } export function WindowReload() { window.runtime.WindowReload(); } export function WindowReloadApp() { window.runtime.WindowReloadApp(); } export function WindowSetAlwaysOnTop(b) { window.runtime.WindowSetAlwaysOnTop(b); } export function WindowSetSystemDefaultTheme() { window.runtime.WindowSetSystemDefaultTheme(); } export function WindowSetLightTheme() { window.runtime.WindowSetLightTheme(); } export function WindowSetDarkTheme() { window.runtime.WindowSetDarkTheme(); } export function WindowCenter() { window.runtime.WindowCenter(); } export function WindowSetTitle(title) { window.runtime.WindowSetTitle(title); } export function WindowFullscreen() { window.runtime.WindowFullscreen(); } export function WindowUnfullscreen() { window.runtime.WindowUnfullscreen(); } export function WindowIsFullscreen() { return window.runtime.WindowIsFullscreen(); } export function WindowGetSize() { return window.runtime.WindowGetSize(); } export function WindowSetSize(width, height) { window.runtime.WindowSetSize(width, height); } export function WindowSetMaxSize(width, height) { window.runtime.WindowSetMaxSize(width, height); } export function WindowSetMinSize(width, height) { window.runtime.WindowSetMinSize(width, height); } export function WindowSetPosition(x, y) { window.runtime.WindowSetPosition(x, y); } export function WindowGetPosition() { return window.runtime.WindowGetPosition(); } export function WindowHide() { window.runtime.WindowHide(); } export function WindowShow() { window.runtime.WindowShow(); } export function WindowMaximise() { window.runtime.WindowMaximise(); } export function WindowToggleMaximise() { window.runtime.WindowToggleMaximise(); } export function WindowUnmaximise() { window.runtime.WindowUnmaximise(); } export function WindowIsMaximised() { return window.runtime.WindowIsMaximised(); } export function WindowMinimise() { window.runtime.WindowMinimise(); } export function WindowUnminimise() { window.runtime.WindowUnminimise(); } export function WindowSetBackgroundColour(R, G, B, A) { window.runtime.WindowSetBackgroundColour(R, G, B, A); } export function ScreenGetAll() { return window.runtime.ScreenGetAll(); } export function WindowIsMinimised() { return window.runtime.WindowIsMinimised(); } export function WindowIsNormal() { return window.runtime.WindowIsNormal(); } export function BrowserOpenURL(url) { window.runtime.BrowserOpenURL(url); } export function Environment() { return window.runtime.Environment(); } export function Quit() { window.runtime.Quit(); } export function Hide() { window.runtime.Hide(); } export function Show() { window.runtime.Show(); } export function ClipboardGetText() { return window.runtime.ClipboardGetText(); } export function ClipboardSetText(text) { return window.runtime.ClipboardSetText(text); } ================================================ FILE: examples/wails/go.mod ================================================ module changeme go 1.18 require github.com/wailsapp/wails/v2 v2.6.0 require ( github.com/bep/debounce v1.2.1 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/labstack/echo/v4 v4.10.2 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/leaanthony/go-ansi-parser v1.6.0 // indirect github.com/leaanthony/gosod v1.0.3 // indirect github.com/leaanthony/slicer v1.6.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/samber/lo v1.38.1 // indirect github.com/tkrajina/go-reflector v0.5.6 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wailsapp/go-webview2 v1.0.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect ) // replace github.com/wailsapp/wails/v2 v2.6.0 => C:\Users\amr\go\pkg\mod ================================================ FILE: examples/wails/go.sum ================================================ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc= github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA= github.com/leaanthony/go-ansi-parser v1.6.0 h1:T8TuMhFB6TUMIUm0oRrSbgJudTFw9csT3ZK09w0t4Pg= github.com/leaanthony/go-ansi-parser v1.6.0/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= github.com/leaanthony/gosod v1.0.3 h1:Fnt+/B6NjQOVuCWOKYRREZnjGyvg+mEhd1nkkA04aTQ= github.com/leaanthony/gosod v1.0.3/go.mod h1:BJ2J+oHsQIyIQpnLPjnqFGTMnOZXDbvWtRCSG7jGxs4= github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js= github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE= github.com/tkrajina/go-reflector v0.5.6/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/wailsapp/go-webview2 v1.0.1 h1:dEJIeEApW/MhO2tTMISZBFZPuW7kwrFA1NtgFB1z1II= github.com/wailsapp/go-webview2 v1.0.1/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/wails/v2 v2.6.0 h1:EyH0zR/EO6dDiqNy8qU5spaXDfkluiq77xrkabPYD4c= github.com/wailsapp/wails/v2 v2.6.0/go.mod h1:WBG9KKWuw0FKfoepBrr/vRlyTmHaMibWesK3yz6nNiM= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= ================================================ FILE: examples/wails/main.go ================================================ package main import ( "embed" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" ) //go:embed all:frontend var assets embed.FS func main() { // Create an instance of the app structure app := NewApp() // Create application with options err := wails.Run(&options.App{ Title: "wails-example", Width: 1024, Height: 768, AssetServer: &assetserver.Options{ Assets: assets, }, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, OnStartup: app.startup, Bind: []interface{}{ app, }, }) if err != nil { println("Error:", err.Error()) } } ================================================ FILE: examples/wails/wails.json ================================================ { "$schema": "https://wails.io/schemas/config.v2.json", "name": "wails-example", "outputfilename": "wails_example" } ================================================ FILE: examples/wry/.gitignore ================================================ /target ================================================ FILE: examples/wry/Cargo.toml ================================================ [package] name = "wry-example" version = "0.0.0" edition = "2021" publish = false [dependencies] wry = "0.53" tao = "0.34" [package.metadata.packager] before-packaging-command = "cargo build --release" product-name = "WRY example" identifier = "com.wry.example" resources = ["Cargo.toml", "src", "32x32.png"] icons = ["32x32.png"] [package.metadata.packager.deb] depends = ["libgtk-3-0", "libwebkit2gtk-4.1-0"] [package.metadata.packager.appimage] libs = [ "WebKitNetworkProcess", "WebKitWebProcess", "libwebkit2gtkinjectedbundle.so", ] [package.metadata.packager.appimage.linuxdeploy-plugins] "gtk" = "https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh" [package.metadata.packager.nsis] appdata-paths = ["$LOCALAPPDATA/$IDENTIFIER"] preinstall-section = """ ; Setup messages ; English LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer." LangString webview2DownloadError ${LANG_ENGLISH} "Error: Downloading WebView2 Failed - $0" LangString webview2DownloadSuccess ${LANG_ENGLISH} "WebView2 bootstrapper downloaded successfully" LangString webview2Downloading ${LANG_ENGLISH} "Downloading WebView2 bootstrapper..." LangString webview2InstallError ${LANG_ENGLISH} "Error: Installing WebView2 failed with exit code $1" LangString webview2InstallSuccess ${LANG_ENGLISH} "WebView2 installed successfully" Section PreInstall ; Check if Webview2 is already installed and skip this section ${If} ${RunningX64} ReadRegStr $4 HKLM "SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${Else} ReadRegStr $4 HKLM "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" ${EndIf} ReadRegStr $5 HKCU "SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" StrCmp $4 "" 0 webview2_done StrCmp $5 "" 0 webview2_done Delete "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(webview2Downloading)" nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\\MicrosoftEdgeWebview2Setup.exe" Pop $0 ${If} $0 == 0 DetailPrint "$(webview2DownloadSuccess)" ${Else} DetailPrint "$(webview2DownloadError)" Abort "$(webview2AbortError)" ${EndIf} StrCpy $6 "$TEMP\\MicrosoftEdgeWebview2Setup.exe" DetailPrint "$(installingWebview2)" ; $6 holds the path to the webview2 installer ExecWait "$6 /install" $1 ${If} $1 == 0 DetailPrint "$(webview2InstallSuccess)" ${Else} DetailPrint "$(webview2InstallError)" Abort "$(webview2AbortError)" ${EndIf} webview2_done: SectionEnd """ ================================================ FILE: examples/wry/README.md ================================================ ## WRY example 1. package the app ```sh cargo r -p cargo-packager -- -p wry-example --release ``` ================================================ FILE: examples/wry/src/main.rs ================================================ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release fn main() -> Result<(), Box> { use tao::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; use wry::WebViewBuilder; let event_loop = EventLoop::new(); let window = WindowBuilder::new() .with_title("html5test") .build(&event_loop)?; let _webview = WebViewBuilder::new() .with_url("https://html5test.com/") .build(&window)?; event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { Event::NewEvents(StartCause::Init) => println!("Wry has started!"), Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => *control_flow = ControlFlow::Exit, _ => (), } }); } ================================================ FILE: package.json ================================================ { "private": true, "scripts": { "build": "pnpm run -r --parallel build", "build:debug": "pnpm run -r --parallel build:debug", "format": "prettier --write .", "format:check": "prettier --check ." }, "devDependencies": { "prettier": "^3.1.0" } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - bindings/*/nodejs ================================================ FILE: renovate.json ================================================ { "extends": ["config:recommended"], "rangeStrategy": "replace", "packageRules": [ { "semanticCommitType": "chore", "matchPackageNames": ["*"] } ] }