Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ service_account_key.json
*.zip
release_work
*.asc
secluso-v*
secluso-v*

# Exclude local release staging outputs and imported signing material
/releases/release_assets/
*.p12
8 changes: 7 additions & 1 deletion releases/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
builds
builds

# Keep release helper scripts tracked
!sign_macos_release.sh
!verify_macos_release.sh
!sign_windows_release.sh
!verify_windows_release.sh
236 changes: 107 additions & 129 deletions releases/README.md

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions releases/digests.lock.env
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ RUST_DIGEST__X86_64_APPLE_DARWIN=3f6e6f8d8725a65a2db964bb828850f888d430c68784d66
RUST_DIGEST__AARCH64_APPLE_DARWIN=4c632e493dfa97f0fe014c3910d1690c149bba85ed8678d47d3563ec6f258ead

# Host-macOS toolchain pins for deterministic deploy bundles.
#
# Deploy builds are not just using cargo build
# Tauri packaging pulls in host-side tooling from several layers like Rust/Cargo, Node/pnpm, Tauri CLI, and Apple toolchain components such as clang, Xcode, etc.
# Any one of those moving underneath us can change the emitted bundle structure / metadata / linker output / etc even when code is identical.
#
# We therefore lock every host dependency that affects packaging and record those versions here
# That way a compare failure can tell the operator of a host-environment drift and avoid reproducibility concerns.
#
# TODO: Longer-term, the cleaner answer here is to run the macOS deploy path inside a pinned macOS VM image.
MACOS_HOST_RUSTC_VERSION=1.90.0
MACOS_HOST_CARGO_VERSION=1.90.0
MACOS_HOST_NODE_VERSION=22.9.0
MACOS_HOST_PNPM_VERSION=10.29.1
MACOS_HOST_TAURI_CLI_VERSION=2.10.0
MACOS_HOST_CLANG_VERSION=16.0.0
MACOS_HOST_XCODE_VERSION=16.2
MACOS_HOST_SDK_VERSION=15.2
MACOS_HOST_TAURI_CLI_VERSION=2.10.1
MACOS_HOST_CLANG_VERSION=21.0.0
MACOS_HOST_XCODE_VERSION=26.4.1
MACOS_HOST_SDK_VERSION=26.4
5 changes: 5 additions & 0 deletions releases/expected_macos_entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
90 changes: 90 additions & 0 deletions releases/lib/common.bash
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,96 @@ sha256_stdin() {
fi
}

normalized_macho_sha256_file() {
local file_path="$1"

# This is intentionally not a general-purpose Mach-O canonicalizer.
# It is a (narrow) comparison helper for release verification, specifically for cases where the executable payload should match but Apple signing/related post-processing mutate some bytes in some (predictable) places.
#
# Current normalization policy we use here:
# [1] zero LC_UUID because it is per-build identity metadata rather than payload
# [2] zero LC_CODE_SIGNATURE because signing rewrites that load command
# [3] zero selected __LINKEDIT size fields that move around with signing
# [4] remove only the EXACT LC_CODE_SIGNATURE blob, and ONLY when we can prove it occupies the tail of both __LINKEDIT and the file
perl -MDigest::SHA=sha256_hex -e '
use strict;
use warnings;

sub u32le {
return unpack("V", substr($_[0], $_[1], 4));
}

sub u64le {
return unpack("Q<", substr($_[0], $_[1], 8));
}

my $path = shift @ARGV;
open my $fh, "<", $path or die "open($path): $!";
binmode $fh;
local $/;
my $data = <$fh>;
my $file_len = length($data);

my $magic = u32le($data, 0);
die "unsupported Mach-O magic in $path\n" if $magic != 0xfeedfacf;

# Thin 64-bit little-endian Mach-O header. Load commands start at byte 32.
my $ncmds = u32le($data, 16);
my $offset = 32;
my ($linkedit_fileoff, $linkedit_filesize);
my ($sig_dataoff, $sig_datasize);

for (my $i = 0; $i < $ncmds; $i++) {
die "truncated Mach-O load commands in $path\n" if $offset + 8 > $file_len;
my $cmd = u32le($data, $offset);
my $cmdsize = u32le($data, $offset + 4);
die "invalid load command size in $path\n" if $cmdsize < 8 || $offset + $cmdsize > $file_len;

if ($cmd == 0x19) {
# LC_SEGMENT_64.
my $segname = substr($data, $offset + 8, 16);
$segname =~ s/\0.*$//s;
if ($segname eq "__LINKEDIT") {
die "multiple __LINKEDIT segments in $path\n" if defined $linkedit_fileoff;
$linkedit_fileoff = u64le($data, $offset + 40);
$linkedit_filesize = u64le($data, $offset + 48);
# Signing perturbs LINKEDIT sizing bookkeeping, so drop those fields from the comparison view while keeping the segment placement itself.
substr($data, $offset + 32, 8) = "\0" x 8;
substr($data, $offset + 48, 8) = "\0" x 8;
}
} elsif ($cmd == 0x1b) {
# LC_UUID is build-identity metadata rather than executable payload.
substr($data, $offset, $cmdsize) = "\0" x $cmdsize;
} elsif ($cmd == 0x1d) {
die "unexpected LC_CODE_SIGNATURE size in $path\n" if $cmdsize < 16;
die "multiple LC_CODE_SIGNATURE commands in $path\n" if defined $sig_dataoff;
$sig_dataoff = u32le($data, $offset + 8);
$sig_datasize = u32le($data, $offset + 12);
# Signing rewrites both the command metadata and the blob it points at.
substr($data, $offset, $cmdsize) = "\0" x $cmdsize;
}

$offset += $cmdsize;
}

die "missing __LINKEDIT segment in $path\n" if !defined $linkedit_fileoff;
die "missing LC_CODE_SIGNATURE in $path\n" if !defined $sig_dataoff;
die "empty LC_CODE_SIGNATURE in $path\n" if $sig_datasize == 0;
die "LC_CODE_SIGNATURE starts before __LINKEDIT in $path\n"
if $sig_dataoff < $linkedit_fileoff;
die "LC_CODE_SIGNATURE exceeds __LINKEDIT in $path\n"
if $sig_dataoff + $sig_datasize > $linkedit_fileoff + $linkedit_filesize;
die "LC_CODE_SIGNATURE is not the tail of __LINKEDIT in $path\n"
if $sig_dataoff + $sig_datasize != $linkedit_fileoff + $linkedit_filesize;
die "LC_CODE_SIGNATURE is not the tail of the file in $path\n"
if $sig_dataoff + $sig_datasize != $file_len;

substr($data, $sig_dataoff, $sig_datasize) = "";

print sha256_hex($data);
' "$file_path"
}

lookup_rust_digest() {
local triple="$1"
local key
Expand Down
31 changes: 23 additions & 8 deletions releases/lib/compare.bash
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
# For each overlapping artifact key we validate:
# 1) Build-input metadata (crate, version, lock digest, toolchain digest).
# 2) Manifest hash presence / correctness against on-disk files.
# 3) Hash equality between the two runs.
# 3) Raw-byte hash equality between the two runs.
#
# Step 2 is intentionally raw-byte strict.
# A run directory is only trustworthy if the manifest sha256 still matches the file that is actually on disk.
#
# Step 3 is intentionally raw-byte strict.
# Generic run-vs-run reproducibility checks must compare the exact bytes that were built
# Signed-vs-unsigned macOS equivalence is handled separately by verify_macos_release.sh.
#
# This layered approach makes failures actionable because it distinguishes input
# drift from output drift, instead of collapsing everything into some very generic
Expand Down Expand Up @@ -159,32 +166,40 @@ compare_runs() {
continue
fi

local h1 h2
h1="$(sha256_file "$p1")"
h2="$(sha256_file "$p2")"
# First prove that each run is internally self-consistent.
# We always compare the manifest's recorded raw sha256 against the real file bytes before any normalization logic is allowed to enter the picture.
local raw_h1 raw_h2
raw_h1="$(sha256_file "$p1")"
raw_h2="$(sha256_file "$p2")"

if [[ -z "$sha1" || -z "$sha2" ]]; then
echo "FAIL: manifest missing sha256 for $pkg | $tgt | $bin"
status=1
continue
fi

if [[ "$h1" != "$sha1" ]]; then
if [[ "$raw_h1" != "$sha1" ]]; then
echo "FAIL: run1 manifest sha256 does not match file for $pkg | $tgt | $bin"
echo " manifest: $sha1"
echo " file : $h1"
echo " file : $raw_h1"
status=1
continue
fi

if [[ "$h2" != "$sha2" ]]; then
if [[ "$raw_h2" != "$sha2" ]]; then
echo "FAIL: run2 manifest sha256 does not match file for $pkg | $tgt | $bin"
echo " manifest: $sha2"
echo " file : $h2"
echo " file : $raw_h2"
status=1
continue
fi

# Only after both run directories pass raw integrity checks do we compare cross-run output hashes.
# This stays raw-byte strict so compare mode proves exact reproducibility
local h1 h2
h1="$raw_h1"
h2="$raw_h2"

if [[ "$h1" != "$h2" ]]; then
echo "DIFF: binary hash mismatch for $pkg | $tgt | $bin"
echo " run1: $h1"
Expand Down
5 changes: 3 additions & 2 deletions releases/lib/deploy_helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ select_deploy_bundles_for_triple() {
# Prefer app bundles for reproducibility checks; dmg container metadata can
# vary between runs even when the app payload is identical.
*apple-darwin) echo "app dmg" ;;
*linux*) echo "appimage deb rpm" ;;
# TODO: RPM reproducibility canonicalization is currently brittle
*linux*) echo "appimage deb" ;;
*) echo "all" ;;
esac
}
Expand Down Expand Up @@ -102,7 +103,7 @@ deploy_bundle_targets_json_for_triple() {
if is_windows_triple "$triple"; then
echo '["nsis"]'
elif is_linux_triple "$triple"; then
echo '["appimage","deb","rpm"]'
echo '["appimage","deb"]'
else
echo '["dmg","app"]'
fi
Expand Down
26 changes: 14 additions & 12 deletions releases/lib/deploy_pipeline.bash
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,14 @@ record_macos_app_payload_artifacts() {
local app_name
app_name="$(basename "$app_dir")"

local exec_dir="$app_dir/Contents/MacOS"
[[ -d "$exec_dir" ]] || die "Missing macOS app executable directory: $exec_dir"
local contents_dir="$app_dir/Contents"
[[ -d "$contents_dir" ]] || die "Missing macOS app Contents directory: $contents_dir"

# Capture deterministic app payload files instead of dmg container bytes.
# Keep the app bundle intact enough to launch locally by preserving resources such as icon.icns.
# Exclude signing metadata from comparisons.
local copied_any=0
local info_plist="$app_dir/Contents/Info.plist"
local info_plist="$contents_dir/Info.plist"
if [[ -f "$info_plist" ]]; then
copied_any=1
local info_rel="app/${app_name}/Contents/Info.plist"
Expand Down Expand Up @@ -166,9 +168,16 @@ record_macos_app_payload_artifacts() {
"$deploy_version" \
"$deploy_lock_sha" \
"$digest"
done < <(find "$exec_dir" -type f | LC_ALL=C sort)
done < <(
find "$contents_dir" -type f \
! -path '*/Info.plist' \
! -path '*/_CodeSignature/*' \
! -name 'CodeResources' \
! -name '.DS_Store' \
| LC_ALL=C sort
)

[[ "$copied_any" -eq 1 ]] || die "No executable payload files found under $exec_dir"
[[ "$copied_any" -eq 1 ]] || die "No macOS app payload files found under $contents_dir"
}

run_host_deploy_bundle_for_triple() {
Expand All @@ -187,13 +196,6 @@ run_host_deploy_bundle_for_triple() {
local deterministic_rustflags="${13}"
local effective_rustflags="$deterministic_rustflags"

if is_apple_triple "$triple"; then
local no_uuid_flag="-C link-arg=-Wl,-no_uuid"
if [[ "$effective_rustflags" != *"$no_uuid_flag"* ]]; then
effective_rustflags="${effective_rustflags:+$effective_rustflags }$no_uuid_flag"
fi
fi

# Keep build-output path stable across run1/run2 so build-script-generated
# absolute paths do not introduce per-run entropy into the final binary.
rm -rf "$run_target_dir"
Expand Down
Loading
Loading