From bddcfd5c13c7ec046bf9b62627c3d8f829039508 Mon Sep 17 00:00:00 2001 From: hiroaki222 Date: Fri, 14 Aug 2026 09:20:40 +0900 Subject: [PATCH 1/4] chore: initial commit From 8d4afde701b339053b59055473e0536aa2779e87 Mon Sep 17 00:00:00 2001 From: hiroaki222 Date: Fri, 14 Aug 2026 09:22:39 +0900 Subject: [PATCH 2/4] ci(release): refuse a macOS bundle that is not sealed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The linker ad-hoc signs the executable on its own, which is not the same as sealing the bundle around it. Without a _CodeSignature covering the sidecars, macOS calls the app damaged and offers only the Trash — and nothing here opens the app, so that stays invisible until somebody downloads it. The sidecars are found rather than looked for at a fixed path: where Tauri puts an externalBin is Tauri's business, and a check that hard-codes it fails on a bundle that is fine. Tried against a bundle built for the purpose. It passes a sealed one, and fails each of: a sidecar outside the bundle, a sidecar replaced after signing, an unsigned executable added after signing, and a sidecar whose own signature has been stripped. --- .github/workflows/release.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6bc4b11..bfad159 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -328,6 +328,39 @@ jobs: releaseId: ${{ needs.draft.outputs.id }} args: --target ${{ matrix.target }} --config src-tauri/tauri.release.conf.json + # Ad-hoc signing the executable is not the same as sealing the bundle + # around it. Without a `_CodeSignature` covering the sidecars, macOS + # calls the app damaged and offers only the Trash — and that is invisible + # here, where the app is never opened, until somebody downloads it. + # + # The sidecars are found rather than looked for at a fixed path: where + # Tauri puts an externalBin inside the bundle is Tauri's business, and a + # check that hard-codes it fails on a bundle that is fine. + - name: Check the bundle is sealed around its sidecars + if: runner.os == 'macOS' + shell: bash + run: | + set -euo pipefail + + app="$(find target -maxdepth 5 -name 'Transcrate.app' | head -1)" + if [ -z "$app" ]; then + echo "no Transcrate.app was built" >&2 + exit 1 + fi + + for tool in ffmpeg ffprobe; do + found="$(find "$app" -type f -name "$tool" | head -1)" + if [ -z "$found" ]; then + echo "$tool is not inside the bundle" >&2 + exit 1 + fi + echo "$tool: ${found#"$app"/}" + done + + # --deep reaches the nested executables, --strict refuses a seal that + # does not match what is actually in the bundle. + codesign --verify --deep --strict --verbose=2 "$app" + # Kept whether or not there is a release to attach them to. The size of # the download is the reason ffmpeg is built here rather than fetched, and # a manual run is where that gets weighed. From 729499e7cf04a30fe74b278a7dacb18c03d64528 Mon Sep 17 00:00:00 2001 From: hiroaki222 Date: Fri, 14 Aug 2026 09:28:05 +0900 Subject: [PATCH 3/4] fix(ci): make the seal check find the right bundle, and only it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two ways the first version could fail a build that was fine. It searched all of target, taking whichever Transcrate.app find reached first — so a stray one anywhere under there decided what got checked. It searches the folder the upload step already reads. It piped find into head, and under pipefail head leaving early sends find SIGPIPE and fails the step. That needs more output than a pipe buffer holds, which this search would not produce; the reason it would not is a count nobody is watching. `-print -quit` stops at the first match without a pipe. Both were found by running the step rather than by reading it. The SIGPIPE case reproduces at two thousand matches, exit 141 on a bundle that is sealed. Tried again against a bundle built for the purpose: it passes a sealed one, ignores a same-named folder elsewhere in target, and fails each of a missing app, a sidecar outside the bundle, a sidecar replaced after signing, an unsigned executable added after signing, and a sidecar whose own signature has been stripped. --- .github/workflows/release.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bfad159..606b97c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -342,14 +342,23 @@ jobs: run: | set -euo pipefail - app="$(find target -maxdepth 5 -name 'Transcrate.app' | head -1)" + # Searched under the folder the upload step already reads, not all + # of target: a stray Transcrate.app anywhere else is whichever one + # find reaches first, and the wrong bundle would be the one checked. + # + # `-print -quit` rather than piping into `head`, because under + # pipefail head leaving early sends find SIGPIPE and the step fails + # on a bundle that is fine. `|| true` so an absent folder is reported + # by the line below rather than by find. + bundle="target/${{ matrix.target }}/release/bundle" + app="$(find "$bundle" -maxdepth 3 -name 'Transcrate.app' -print -quit 2>/dev/null || true)" if [ -z "$app" ]; then echo "no Transcrate.app was built" >&2 exit 1 fi for tool in ffmpeg ffprobe; do - found="$(find "$app" -type f -name "$tool" | head -1)" + found="$(find "$app" -type f -name "$tool" -print -quit 2>/dev/null || true)" if [ -z "$found" ]; then echo "$tool is not inside the bundle" >&2 exit 1 From eccc416bbb29239815ab6caffd3b396bfd5e59d2 Mon Sep 17 00:00:00 2001 From: hiroaki222 Date: Fri, 14 Aug 2026 09:32:28 +0900 Subject: [PATCH 4/4] fix(ci): require the sidecars where the app actually looks for them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The check accepted ffmpeg anywhere inside the bundle. The app does not: it resolves a tool against the directory holding the running executable and otherwise falls back to a bare name on the PATH, silently, on a machine that has no ffmpeg. A copy sealed under Contents/Resources passed — signed, verified, and never opened by the app that shipped with it. The reasoning behind the looser check was that where Tauri puts an externalBin is Tauri's business. It is not: tools::locate states where the tool has to be, so that is the assumption worth holding the build to. Contents/MacOS is that directory by the bundle format rather than by Tauri, so naming it is not a guess. When it fires it says where the file was found instead, which is the question anybody reading the failure will have. Tried against a bundle built for the purpose, under /bin/bash 3.2 as the runner has. It passes a correct bundle, and fails a sidecar sealed under Resources, a missing app, a sidecar outside the bundle, one replaced after signing, an unsigned executable added after signing, and one whose own signature has been stripped. --- .github/workflows/release.yml | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 606b97c..11754f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -333,9 +333,12 @@ jobs: # calls the app damaged and offers only the Trash — and that is invisible # here, where the app is never opened, until somebody downloads it. # - # The sidecars are found rather than looked for at a fixed path: where - # Tauri puts an externalBin inside the bundle is Tauri's business, and a - # check that hard-codes it fails on a bundle that is fine. + # The sidecars are required beside the executable rather than anywhere + # inside the bundle, because that is the only place the app looks: + # tools::locate resolves them against the directory holding the running + # binary and otherwise falls back to a bare name on the PATH. A copy + # sealed somewhere else would pass a laxer check and ship an app that + # tells its user ffmpeg is missing. - name: Check the bundle is sealed around its sidecars if: runner.os == 'macOS' shell: bash @@ -357,13 +360,19 @@ jobs: exit 1 fi + # Contents/MacOS is where the bundle format puts the executable, so + # it is what current_exe().parent() returns and what the app joins a + # tool name onto. for tool in ffmpeg ffprobe; do - found="$(find "$app" -type f -name "$tool" -print -quit 2>/dev/null || true)" - if [ -z "$found" ]; then - echo "$tool is not inside the bundle" >&2 + if [ ! -f "$app/Contents/MacOS/$tool" ]; then + echo "$tool is not beside the executable in the bundle" >&2 + + stray="$(find "$app" -type f -name "$tool" -print -quit 2>/dev/null || true)" + if [ -n "$stray" ]; then + echo " it is at ${stray#"$app"/}, where the app will not look" >&2 + fi exit 1 fi - echo "$tool: ${found#"$app"/}" done # --deep reaches the nested executables, --strict refuses a seal that