diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 42274da..a9237eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -122,6 +122,65 @@ jobs: if: matrix.target == 'windows' uses: negrutiu/nsis-install@v3 + # The README promises a notarized Mac app, so a release here can't go out without these secrets. A fork or a + # build-only run without them signs the app ad-hoc and skips notarization. + - name: Import the Developer ID certificate + if: matrix.target == 'macos' + env: + RELEASING: ${{ needs.version.outputs.tag != '' && github.repository == 'Bare7a/BarSQL' }} + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + run: | + set -euo pipefail + missing() { + if [ "$RELEASING" = true ]; then + echo "::error::$1" + exit 1 + fi + echo "::warning::$1" + exit 0 + } + if [ -z "$MACOS_CERTIFICATE" ]; then + missing "No MACOS_CERTIFICATE secret, so the macOS app would be signed ad-hoc and not notarized." + fi + keychain="$RUNNER_TEMP/signing.keychain-db" + keychain_password="$(openssl rand -base64 24)" + security create-keychain -p "$keychain_password" "$keychain" + # Recorded at once, so the cleanup step deletes it even when the import below fails. + echo "BARSQL_SIGNING_KEYCHAIN=$keychain" >> "$GITHUB_ENV" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$keychain_password" "$keychain" + # A .p12 from Keychain Access leaves out Apple's intermediate, and the runner image may not have it either. + security import packaging/macos/DeveloperIDG2CA.cer -k "$keychain" + printf '%s' "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12" + security import "$RUNNER_TEMP/certificate.p12" -k "$keychain" -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + rm "$RUNNER_TEMP/certificate.p12" + security set-key-partition-list -S apple-tool:,apple: -s -k "$keychain_password" "$keychain" > /dev/null + security list-keychains -d user -s "$keychain" "$HOME/Library/Keychains/login.keychain-db" + # The SHA-1 hash rather than the name, which codesign refuses when two certificates share it, as after a renewal. + identity="$(security find-identity -v -p codesigning "$keychain" | awk '/"Developer ID Application: /{print $2; exit}')" + if [ -z "$identity" ]; then + echo "::error::MACOS_CERTIFICATE holds no Developer ID Application identity." + exit 1 + fi + echo "BARSQL_MACOS_IDENTITY=$identity" >> "$GITHUB_ENV" + if [ -z "$APPLE_API_KEY" ]; then + missing "No APPLE_API_KEY secret, so the macOS app would be signed but not notarized, and Gatekeeper would block it." + fi + if [ -z "$APPLE_API_KEY_ID" ] || [ -z "$APPLE_API_ISSUER" ]; then + echo "::error::APPLE_API_KEY needs the APPLE_API_KEY_ID and APPLE_API_ISSUER secrets too." + exit 1 + fi + printf '%s\n' "$APPLE_API_KEY" > "$RUNNER_TEMP/notary.p8" + { + echo "BARSQL_NOTARY_KEY=$RUNNER_TEMP/notary.p8" + echo "BARSQL_NOTARY_KEY_ID=$APPLE_API_KEY_ID" + echo "BARSQL_NOTARY_ISSUER=$APPLE_API_ISSUER" + } >> "$GITHUB_ENV" + - name: Package env: APPIMAGE_EXTRACT_AND_RUN: 1 @@ -138,6 +197,14 @@ jobs: if-no-files-found: error retention-days: 7 + - name: Delete the signing keychain and the keys + if: always() && matrix.target == 'macos' + run: | + if [ -n "${BARSQL_SIGNING_KEYCHAIN:-}" ]; then + security delete-keychain "$BARSQL_SIGNING_KEYCHAIN" + fi + rm -f "$RUNNER_TEMP/certificate.p12" "$RUNNER_TEMP/notary.p8" + # makepkg needs Arch and refuses to run as root. arch: needs: [version, build] @@ -182,6 +249,8 @@ jobs: timeout-minutes: 10 permissions: contents: write + id-token: write + attestations: write steps: - uses: actions/download-artifact@v8 with: @@ -196,6 +265,12 @@ jobs: sha256sum -- * > SHA256SUMS cat SHA256SUMS + # Every file the release publishes, SHA256SUMS included, so gh attestation verify --repo Bare7a/BarSQL + # confirms any of them came from this workflow. + - uses: actions/attest-build-provenance@v4 + with: + subject-path: dist/* + - uses: softprops/action-gh-release@v3 with: tag_name: ${{ needs.version.outputs.tag }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de47492..13ad0c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,6 +53,23 @@ For a single screenshot, build with `cargo build -p barsql --features snapshot`, - the update dialog: `update`, `update-current`, `update-downloading`, `update-ready`, `update-failed` - other screens: `plan`, `quick`, `quick=`, `suggest=`, `toasts`, `palette` (the command palette), `chart` (the first result as a chart, with `BARSQL_SNAPSHOT_RUN=1`), `er` (the `main` schema's ER diagram), `structure` (a table's Structure tab: the first in `main`, or `structure=`) +## Translations + +BarSQL speaks English, Deutsch, Български and Español. A new language is mostly JSON and needs little of the rest of the code; [#25](https://github.com/Bare7a/BarSQL/pull/25) added Spanish and touches every step below. + +1. Copy `crates/barsql-ui/locales/base/en.json` and `crates/barsql-ui/locales/en.json` to the language's code, such as `fr.json`, and translate the values. Keep the keys and placeholders like `{count}` as they are. Terms developers know in English, such as Commit or Rollback, can stay in English. +2. Add the language's own name under `language` in every `locales/base/*.json`. +3. Register it in `crates/barsql-ui/src`: + - `i18n.rs`: `LANGUAGES`, its two `include_str!` constants and `source`, plus its digit grouping in `format_number` + - `actions.rs`: a `Language…` action and its item in View → Language + - `command_palette.rs` and `workspace.rs`: the same action + - `relative_time.rs`: phrases such as "yesterday" and "in 3 days" +4. Add the language to `locales` in `fixtures/golden/ui/relative_time.json`, with a column for it in each row. +5. Run `cargo test -p barsql-ui`. It names any key the new files miss. +6. List the language in `guide/app.md` and on the landing page's "Instant and polished" card. + +Plurals have only two forms, `key_one` and `key_other`. A language that needs more, such as Polish or Russian, needs a change to `t_count` first, so open an issue before translating it. + ## Docs - `README.md` - the front page on GitHub: the pitch, downloads, screenshots and the engines table diff --git a/README.md b/README.md index 152f24c..7bc2d2d 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ # ⚡ BarSQL ![Rust](https://img.shields.io/badge/Rust-1.99+-B7410E?style=for-the-badge&logo=rust) -![GPUI](https://img.shields.io/badge/GPUI_Kit-0.8-4B275F?style=for-the-badge) +![GPUI](https://img.shields.io/badge/GPUI_Kit-0.9-4B275F?style=for-the-badge) ![License](https://img.shields.io/badge/license-GPL--3.0-blue?style=for-the-badge) -![Status](https://img.shields.io/badge/status-active-success?style=for-the-badge) +[![Release](https://img.shields.io/github/v/release/Bare7a/BarSQL?style=for-the-badge&color=success)](../../releases/latest) > **A fast, local-first SQL workbench built with Rust and GPUI.** No cloud. No accounts. No telemetry. -Just a focused, native desktop tool for working with databases. +Just a focused, native desktop tool for **PostgreSQL**, **MySQL / MariaDB**, **SQL Server**, **ClickHouse**, **Turso / libSQL** and **SQLite**, on Windows, macOS and Linux. 🌐 **[barsql.bare7a.eu](https://barsql.bare7a.eu)** · 📖 **[User Guide](guide/README.md)** +![BarSQL's editor with schema-aware suggestions, a saved query's results in the grid and the focused row as JSON](.github/screenshots/1.png?raw=true) + --- ## ⬇️ Download @@ -34,6 +36,14 @@ No account required - just download and run. Every build above updates itself. The installer, the Linux packages and copies in protected folders ask for admin rights first. +### First launch + +The macOS app is signed with a Developer ID and notarized by Apple, so it opens like any other app. + +The Windows build isn't code-signed yet, so Windows asks once before BarSQL first opens: when SmartScreen says *Windows protected your PC*, click **More info → Run anyway**. Smart App Control, where it's turned on, blocks unsigned apps without an exception, so BarSQL can't run there until it's signed. + +Every release lists each file's SHA-256 in `SHA256SUMS`, which the updater checks its downloads against, and every file carries a signed [build provenance attestation](../../attestations) from the GitHub Actions run that built it. + Prefer to build it yourself? See **Build from source** below. --- @@ -184,6 +194,17 @@ Requirements, tests and the project layout are in **[CONTRIBUTING.md](CONTRIBUTI --- +## 🤝 Contributing + +Issues and pull requests are welcome. Good places to start: + +- **[Report a bug or ask for a feature](../../issues/new/choose)** - especially anything that misbehaves on your engine or server version +- **[Add a language](CONTRIBUTING.md#translations)** - mostly JSON, plus a few lines of Rust to register it + +If BarSQL is useful to you, a ⭐ helps other people find it. + +--- + ## 📄 License GPL-3.0-only; see [LICENSE](LICENSE). diff --git a/RELEASING.md b/RELEASING.md index a25f1d0..bff4fbf 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -2,7 +2,7 @@ 1. The workspace starts at 1.0.0. For later releases, run `cargo xtask bump-version --patch` (or `--minor`, `--major`, or an explicit version such as `1.1.0`), then commit and push. `cargo xtask version` prints the current version. 2. Tag and push (`git tag v1.0.0 && git push origin v1.0.0`), or start the workflow by hand on any branch (Actions → Release BarSQL → Run workflow) with the version, and it tags the commit it runs on. -3. `.github/workflows/release.yml` checks that the tag matches `Cargo.toml` and isn't already on another commit, then builds on Ubuntu 22.04, Windows and macOS while the full test suite runs on all three. Once both pass, it publishes every asset plus `SHA256SUMS`. +3. `.github/workflows/release.yml` checks that the tag matches `Cargo.toml` and isn't already on another commit, then builds on Ubuntu 22.04, Windows and macOS while the full test suite runs on all three. Once both pass, it publishes every asset plus `SHA256SUMS`, each with a build provenance attestation that `gh attestation verify --repo Bare7a/BarSQL` checks. Run the workflow by hand with the version left empty to build the packages without publishing them; they stay on the run's page for 7 days. @@ -19,12 +19,30 @@ Run the workflow by hand with the version left empty to build the packages witho - Only the portable updater assets name both a platform and an architecture, so portable copies never pick another download. The AppImage and the packages update from their own asset, and the installer and the dmg from the `.zip`. - Release builds turn on the `production` feature, which enables the update check at launch. The release profile uses thin LTO and strips symbols. -- **macOS:** an `.app` for macOS 12 and later with an ad-hoc signature. Each zip holds its own architecture alone, half the size of a universal app, since the updater downloads the zip for the Mac's own chip, even from an Intel build running under Rosetta. The dmg holds a `lipo` universal app and an `Applications` link, so a copy installed from it keeps only its own architecture after its first update. `BARSQL_NATIVE_ONLY=1` builds this Mac's architecture alone. -- **Windows:** `crates/barsql/build.rs` embeds the icon and the version block. `packaging/windows/installer.nsi` installs the app with the `SQLite Database` file class for `.db`, `.sqlite`, `.sqlite3`, `.s3db` and `.sl3`. +- **macOS:** an `.app` for macOS 12 and later, signed with the Developer ID and notarized through the [signing secrets](#macos-signing-and-notarization), which a release requires. A fork or a build-only run without them signs it ad-hoc. Each zip holds its own architecture alone, half the size of a universal app, since the updater downloads the zip for the Mac's own chip, even from an Intel build running under Rosetta. The dmg holds a `lipo` universal app and an `Applications` link, so a copy installed from it keeps only its own architecture after its first update. `BARSQL_NATIVE_ONLY=1` builds this Mac's architecture alone. +- **Windows:** `crates/barsql/build.rs` embeds the icon and the version block. Neither the `.exe` nor the installer is code-signed, so SmartScreen asks once before the first launch. `packaging/windows/installer.nsi` installs the app with the `SQLite Database` file class for `.db`, `.sqlite`, `.sqlite3`, `.s3db` and `.sl3`. - **Linux:** [`cargo-deb`](https://crates.io/crates/cargo-deb) and [`cargo-generate-rpm`](https://crates.io/crates/cargo-generate-rpm) build the deb and rpm from `crates/barsql/Cargo.toml`, and `makepkg` builds the Arch package from `packaging/linux/PKGBUILD`. Each installs `/usr/bin/BarSQL` and a desktop entry that opens SQLite files; the deb and rpm work out their library dependencies from the binary. The window's app id is `BarSQL`, so Wayland docks match it to `BarSQL.desktop`. - **Compression:** the dmg is LZMA (`ULMO`, which macOS 10.15 and later open), the installer uses a 64 MB LZMA dictionary, the rpm an xz payload and the Arch package `zstd --ultra -20`. The deb keeps cargo-deb's xz and the AppImage appimagetool's zstd. The updater assets stay deflate `.zip` and gzip `.tar.gz`, the only formats released versions unpack. - The icon's source is `packaging/icon.svg`. The macOS `.icns` sets it on Apple's icon grid (an 824px body on a 1024px canvas, with a shadow). Windows and Linux show the stack without its tile, since their taskbars and docks are mostly dark and the dark tile would disappear there, leaving the stack looking small. The title bar shows the stack too, in a dark and a light version (`crates/barsql-ui/images/barsql-mark-*.svg`). `cargo xtask icons` regenerates the set from it: the Linux PNGs, `icon.ico`, `icons.icns` and `docs/icon.png`, each size rendered from the vector, and the SVG copies for the app (the marks and its icon) and `docs/`. +## macOS Signing and Notarization + +With these repository secrets, the release workflow signs the apps and the dmg with the Developer ID and the hardened runtime, has Apple notarize them, and staples the tickets, so Gatekeeper opens BarSQL without asking. The README promises a notarized Mac app, so a release of Bare7a/BarSQL fails without them. A fork or a build-only run without them signs the apps ad-hoc and skips notarization. + +| Secret | Value | +| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `MACOS_CERTIFICATE` | The Developer ID Application certificate with its private key, exported from Keychain Access as a `.p12`, in base64: `base64 -i DeveloperID.p12 \| pbcopy` | +| `MACOS_CERTIFICATE_PASSWORD` | The password the `.p12` was exported with | +| `APPLE_API_KEY` | The text of an App Store Connect API key's `.p8` file: a team key with the Developer role, from Users and Access → Integrations → App Store Connect API | +| `APPLE_API_KEY_ID` | That key's Key ID | +| `APPLE_API_ISSUER` | The Issuer ID shown above the keys | + +- Only the account holder can create a Developer ID Application certificate: Xcode → Settings → Accounts → Manage Certificates → + → Developer ID Application. +- `packaging/macos/BarSQL.entitlements` turns off library validation, which the hardened runtime would otherwise use to refuse SQLite extensions signed by other developers or ad-hoc. +- `packaging/macos/DeveloperIDG2CA.cer` is Apple's Developer ID intermediate certificate, which a `.p12` export leaves out and the workflow adds to its keychain. It expires in September 2031; replace it from [apple.com/certificateauthority](https://www.apple.com/certificateauthority/) before then. +- To try the secrets without publishing, run the workflow by hand with the version left empty. Unzip `BarSQL-darwin-arm64.zip` from the run's `dist-macos` artifact: `spctl -a -vv BarSQL.app` should report `source=Notarized Developer ID`, and `xcrun stapler validate` should pass for the app and the dmg. +- Locally, `BARSQL_MACOS_IDENTITY` signs with a certificate in the login keychain, named as `"Developer ID Application: …"` or, when two share that name, by the SHA-1 hash `security find-identity -v -p codesigning` lists. `BARSQL_NOTARY_KEY` (the `.p8` file's path), `BARSQL_NOTARY_KEY_ID` and `BARSQL_NOTARY_ISSUER` notarize too. + ## How Updates Work Release builds check [Bare7a/BarSQL](https://github.com/Bare7a/BarSQL)'s releases at launch and offer a newer one with a toast. About → Check for Updates does the same at any time. The code lives in `crates/barsql-app/src/update*` and `crates/barsql-ui/src/update_dialog.rs`. diff --git a/docs/index.html b/docs/index.html index 23287df..198646e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -22,15 +22,11 @@ + + + + + + + + +
+
+ Privacy +

Privacy policy

+

Last updated November 4, 2026

+

BarSQL is a desktop app that runs on your computer. It has no accounts, no analytics and no telemetry, and its developer never receives your connections, queries or data.

+ +

What BarSQL stores

+

Everything BarSQL keeps is in one BarSQL-data folder on your computer, next to the app or in your user folder. The guide says where.

+
    +
  • connections.json holds your connections, with their database passwords, Turso auth tokens, SSH passwords and key passphrases in plain text. Keep the folder as private as the credentials in it.
  • +
  • query_history.json has the last 500 statements you ran, saved_queries.json your saved queries, editor_session.json your open tabs and their SQL, and settings.json your preferences.
  • +
  • crash.log appears if BarSQL hits a bug, mcp.log once an AI assistant uses it, and update.log when it updates itself.
  • +
+

None of it is sent anywhere. A crash report stays in crash.log unless you share it, for example in a GitHub issue.

+ +

What BarSQL connects to

+
    +
  • Your databases and SSH hosts. BarSQL connects to the servers you set up, and to nothing else on your behalf.
  • +
  • GitHub, for updates. Release builds ask GitHub's API for the latest release at launch, and download it only when you choose to install it. GitHub sees your IP address and BarSQL's version, as the GitHub Privacy Statement describes.
  • +
  • Links. The website, GitHub and release notes links in the app open in your browser only when you click them.
  • +
+ +

AI assistants

+

AI apps such as Claude, Cursor or VS Code can use BarSQL as an MCP server, but only for connections where you tick Allow AI assistants (MCP). The AI app starts BarSQL on your computer and talks to it directly; BarSQL itself sends nothing to the AI's provider. The assistant sees the shared connections' names, tables and query results, never their passwords, and changes data only where you allow it. What it reads goes to its AI service, under that app's privacy policy, so share only what you'd show it. Every call is logged in mcp.log.

+ +

Downloads

+

BarSQL is downloaded from GitHub, under the GitHub Privacy Statement. The developer sees how many times each file was downloaded, not who downloaded it.

+ +

This website

+

barsql.bare7a.eu is a static site on GitHub Pages. It sets no cookies, runs no analytics and loads nothing from other sites. Your choice of light or dark stays in your browser's local storage. GitHub logs visitors' IP addresses to keep its service secure, as the GitHub Privacy Statement explains.

+ +

Your control

+

Your BarSQL data is all in the BarSQL-data folder: open it to see it, copy it to keep it, or delete it to remove it. Uninstalling BarSQL may leave the folder behind, so delete it yourself if you want everything gone. The developer holds none of your data, so there's nothing to request, correct or delete on that side.

+ +

Changes and contact

+

If BarSQL starts handling data differently, this page changes with it and gets a new date, and its history stays on GitHub. Ask questions in GitHub issues, or email bare7a@gmail.com about anything you'd rather not post publicly.

+
+
+ + + + + diff --git a/docs/script.js b/docs/script.js index b32dceb..7a3cc57 100644 --- a/docs/script.js +++ b/docs/script.js @@ -1,6 +1,7 @@ // A saved theme wins. Otherwise follow the system setting until the visitor picks one. function setUpTheme() { const root = document.documentElement; + // Each page's inline head script reads it before the first paint. const KEY = "barsql-theme"; const chosen = () => { @@ -37,15 +38,15 @@ function detectOS() { } // Point the hero button at the visitor's own build, highlight its card and pick the modifier key. -// Phones and tablets keep the generic button, which leads to every download. +// Phones and tablets keep the generic button, which leads to every download, and pages without downloads skip this. function pointDownloadAtVisitor() { const os = detectOS(); - if (!os) return; + const hero = document.getElementById("hero-download"); + const card = os && document.getElementById(`dl-${os}`); + if (!hero || !card) return; const names = { windows: "Windows", macos: "macOS", linux: "Linux" }; - const card = document.getElementById(`dl-${os}`); const main = card.querySelector(".dl-main"); - const hero = document.getElementById("hero-download"); const icon = hero.querySelector("svg"); hero.href = main.href; icon.setAttribute("class", "logo"); @@ -65,6 +66,7 @@ function pointDownloadAtVisitor() { // Lightbox with every screenshot on the page, full size and in order. Arrow keys, swipes and the buttons move between them. function setUpLightbox() { const box = document.getElementById("lightbox"); + if (!box) return; const img = box.querySelector("img"); const caption = box.querySelector(".lightbox-caption"); const count = box.querySelector(".lightbox-count"); diff --git a/docs/styles.css b/docs/styles.css index e5145ad..095f5c9 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -676,6 +676,21 @@ kbd { .dl-all { max-width: 760px; margin: 40px auto 0; text-align: center; color: var(--faint); font-size: 0.92rem; text-wrap: balance; } .dl-star { display: flex; justify-content: center; margin-top: 24px; } +/* Text pages, like the privacy policy: one column at a comfortable line length */ + +.doc { max-width: calc(720px + 2 * var(--gutter)); padding-top: 56px; padding-bottom: 112px; } +.doc h1 { margin-top: 14px; font-size: clamp(2.25rem, 5vw, 3.25rem); font-weight: 650; letter-spacing: -0.035em; line-height: 1.08; } +.doc-meta { margin: 14px 0 0; color: var(--faint); font-size: 0.92rem; } +.doc-lead { margin: 28px 0 0; font-size: 1.15rem; } +.doc h2 { margin-top: 52px; font-size: 1.4rem; font-weight: 650; letter-spacing: -0.02em; line-height: 1.25; } +.doc p:not(.doc-lead, .doc-meta), .doc ul { margin: 14px 0 0; color: var(--muted); } +.doc ul { padding-left: 1.2em; } +.doc li + li { margin-top: 10px; } +.doc li::marker { color: var(--faint); } +.doc strong { color: var(--text); font-weight: 600; } +.doc li a { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; } +.doc li a:hover { text-decoration-thickness: 2px; } + /* Footer */ .site-footer { padding: 64px 0 48px; border-top: 1px solid var(--line); } @@ -686,7 +701,8 @@ kbd { .footer-cols div { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; } .footer-title { margin-bottom: 4px; color: var(--text); font-size: 0.85rem; font-weight: 600; } .footer-cols a { color: var(--muted); font-size: 0.9rem; } -.footer-cols a:hover { color: var(--text); text-decoration: none; } +.footer-cols a:hover, +.footer-cols a[aria-current="page"] { color: var(--text); text-decoration: none; } /* Lightbox */ @@ -766,6 +782,7 @@ kbd { :root { --gutter: 16px; } .section { padding: 80px 0; } .hero { padding: 40px 0 48px; } + .doc { padding-top: 32px; padding-bottom: 80px; } .engines { display: grid; grid-template-columns: repeat(2, auto); justify-content: start; } .hero-actions .btn { flex: 1 1 auto; } .hero-shot { margin-top: 48px; } diff --git a/packaging/macos/BarSQL.entitlements b/packaging/macos/BarSQL.entitlements new file mode 100644 index 0000000..8cc185a --- /dev/null +++ b/packaging/macos/BarSQL.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.cs.disable-library-validation + + + diff --git a/packaging/macos/DeveloperIDG2CA.cer b/packaging/macos/DeveloperIDG2CA.cer new file mode 100644 index 0000000..8cbcf6f Binary files /dev/null and b/packaging/macos/DeveloperIDG2CA.cer differ diff --git a/scripts/package-macos.sh b/scripts/package-macos.sh index 7faeb62..a3e9bc3 100755 --- a/scripts/package-macos.sh +++ b/scripts/package-macos.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash # Builds the updater zips, each with BarSQL.app for one architecture, and the universal dmg in target/package/dist. # BARSQL_NATIVE_ONLY=1 builds only this Mac's architecture and its zip. +# BARSQL_MACOS_IDENTITY, the name or SHA-1 hash of a Developer ID Application identity in the keychain, signs with the +# hardened runtime instead of ad-hoc. Adding BARSQL_NOTARY_KEY (an App Store Connect API key's .p8 file), BARSQL_NOTARY_KEY_ID and +# BARSQL_NOTARY_ISSUER has Apple notarize the apps and the dmg, and staples their tickets. set -euo pipefail root_dir="$(cd "$(dirname "$0")/.." && pwd)" @@ -10,6 +13,17 @@ out="$root_dir/target/package/macos" dist="$root_dir/target/package/dist" app="$out/BarSQL.app" +identity="${BARSQL_MACOS_IDENTITY:-}" +notarizing="" +if [ -n "$identity" ] && [ -n "${BARSQL_NOTARY_KEY:-}" ]; then + notarizing=1 + notary=( + --key "$BARSQL_NOTARY_KEY" + --key-id "${BARSQL_NOTARY_KEY_ID:?set it to the Key ID of the BARSQL_NOTARY_KEY API key}" + --issuer "${BARSQL_NOTARY_ISSUER:?set it to the Issuer ID of the BARSQL_NOTARY_KEY API key}" + ) +fi + targets=(aarch64-apple-darwin x86_64-apple-darwin) if [ "${BARSQL_NATIVE_ONLY:-}" = 1 ]; then targets=("$(uname -m | sed 's/arm64/aarch64/')-apple-darwin") @@ -25,6 +39,37 @@ for target in "${targets[@]}"; do binaries+=("$root_dir/target/$target/release/BarSQL") done +# The entitlements let the hardened runtime load SQLite extensions, which other developers sign or leave ad-hoc. +sign_app() { + if [ -n "$identity" ]; then + codesign --force --options runtime --timestamp --entitlements "$root_dir/packaging/macos/BarSQL.entitlements" \ + --sign "$identity" "$1" + codesign --verify --strict "$1" + else + codesign --deep --sign - "$1" + fi +} + +# Waits for Apple's verdict and prints its log when the submission isn't accepted. +notarize() { + local result id + result="$(xcrun notarytool submit "$1" "${notary[@]}" --wait --timeout 30m --output-format json)" || true + [ "$(plutil -extract status raw -o - - <<<"$result" 2>/dev/null)" = Accepted ] && return + echo "notarizing $(basename "$1") failed: ${result:-no response}" >&2 + id="$(plutil -extract id raw -o - - <<<"$result" 2>/dev/null)" && xcrun notarytool log "$id" "${notary[@]}" >&2 + exit 1 +} + +# A new ticket can take a moment to reach Apple's servers. +staple() { + for _ in 1 2 3; do + xcrun stapler staple "$1" && return + sleep 20 + done + echo "stapling $1 failed" >&2 + exit 1 +} + make_app() { local dest="$1" shift @@ -36,18 +81,34 @@ make_app() { -c "Set :CFBundleShortVersionString $version" \ -c "Set :CFBundleVersion $version" \ "$dest/Contents/Info.plist" - codesign --deep --sign - "$dest" + sign_app "$dest" } rm -rf "$out" mkdir -p "$dist" make_app "$app" "${binaries[@]}" - -# The updater downloads the zip for its own architecture, so each zip carries that one alone. -rm -f "$dist"/BarSQL-darwin-*.zip "$dist/BarSQL-macos-universal.dmg" +arches=() for binary in "${binaries[@]}"; do arch="$(lipo -archs "$binary" | sed 's/x86_64/amd64/')" make_app "$out/$arch/BarSQL.app" "$binary" + arches+=("$arch") +done + +if [ -n "$notarizing" ]; then + # One submission covers every app. The stapled ticket lets each open without asking Apple first. + submission="$root_dir/target/package/macos-notarize.zip" + ditto -c -k --keepParent "$out" "$submission" + notarize "$submission" + rm "$submission" + staple "$app" + for arch in "${arches[@]}"; do + staple "$out/$arch/BarSQL.app" + done +fi + +# The updater downloads the zip for its own architecture, so each zip carries that one alone. +rm -f "$dist"/BarSQL-darwin-*.zip "$dist/BarSQL-macos-universal.dmg" +for arch in "${arches[@]}"; do (cd "$out/$arch" && zip -r -y -X "$dist/BarSQL-darwin-$arch.zip" "BarSQL.app" >/dev/null) done @@ -72,5 +133,15 @@ for attempt in 1 2 3; do done rm -rf "$staging" -echo "BarSQL $version: $(lipo -archs "$app/Contents/MacOS/BarSQL")" +if [ -n "$identity" ]; then + codesign --force --timestamp --sign "$identity" "$dmg" + if [ -n "$notarizing" ]; then + notarize "$dmg" + staple "$dmg" + fi +fi + +# The certificate's name, whether BARSQL_MACOS_IDENTITY gave its name or its hash. +signer="$(codesign -dvv "$app" 2>&1 | awk '/^Authority=/ && !seen++ { sub(/^Authority=/, ""); print }')" +echo "BarSQL $version: $(lipo -archs "$app/Contents/MacOS/BarSQL"), signed ${signer:+by }${signer:-ad-hoc}${notarizing:+ and notarized}" ls -l "$dist"