Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 6 additions & 0 deletions docs/repository-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ job trusts its runner with the selected environment's secrets. PR builds have
no signing credentials and retain their existing branch-scoped caches. No
persistent vendor disks or Docker builders are configured by these workflows.

Research Linux packaging builds AppImage and DEB through Tauri, then creates
the final RPM once from the normalized DEB payload. Its staging executable is
marked as RPM so Tauri selects the RPM updater entry and installer. Build and
download verifiers check the executable identity in all three package formats;
updater metadata, signing and publication keep their existing flow.

Research's Unix compiler-cache statistics use `scripts/ci/sccache-stats.sh`,
which shares the build's cache configuration. In Actions the sccache socket
lives under `RUNNER_TEMP`, so separate Nix shells reach the same server despite
Expand Down
3 changes: 3 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@
atk
bubblewrap
cairo
cpio
gdk-pixbuf
glib
glib-networking
Expand Down Expand Up @@ -822,6 +823,8 @@
src = ./.;
} ''
cd "$src"
python3 scripts/ci/test_tauri_bundle_type.py
python3 scripts/ci/test_linux_rpm_packaging.py
bash ./scripts/ci/test-release-gates.sh
touch "$out"
'';
Expand Down
120 changes: 68 additions & 52 deletions scripts/ci/_common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3686,11 +3686,12 @@ verify_linux_deb_package_executable_metadata() {
tmp="$(mktemp -d)"
status=0
(
cd "${tmp}"
ar x "${deb}"
mkdir data
tar -xzf data.tar.gz -C data
verify_linux_package_executable_metadata "${tmp}/data/usr/bin/maple"
cd "${tmp}" &&
ar x "${deb}" &&
mkdir data &&
tar -xzf data.tar.gz -C data &&
verify_linux_package_executable_metadata "${tmp}/data/usr/bin/maple" &&
"$(python3_runner)" "${SCRIPT_DIR}/tauri_bundle_type.py" --expect deb "${tmp}/data/usr/bin/maple"
) || status=$?
rm -rf "${tmp}"
return "${status}"
Expand All @@ -3703,16 +3704,32 @@ verify_linux_appimage_executable_metadata() {
tmp="$(mktemp -d)"
status=0
(
extract_appimage_tool "${appimage}" "${tmp}/AppDir"
verify_linux_package_executable_metadata "${tmp}/AppDir/usr/bin/maple"
verify_linux_appdir_runtime_closure "${tmp}/AppDir"
verify_linux_appdir_portable_runtime_paths "${tmp}/AppDir"
extract_appimage_tool "${appimage}" "${tmp}/AppDir" &&
verify_linux_package_executable_metadata "${tmp}/AppDir/usr/bin/maple" &&
"$(python3_runner)" "${SCRIPT_DIR}/tauri_bundle_type.py" --expect appimage "${tmp}/AppDir/usr/bin/maple" &&
verify_linux_appdir_runtime_closure "${tmp}/AppDir" &&
verify_linux_appdir_portable_runtime_paths "${tmp}/AppDir" &&
verify_linux_appdir_webkit_runtime "${tmp}/AppDir"
) || status=$?
rm -rf "${tmp}"
return "${status}"
}

verify_linux_rpm_package_executable_metadata() {
local rpm="$1"
local tmp status

tmp="$(mktemp -d)"
status=0
(
rpm2cpio "${rpm}" | cpio -i --quiet --to-stdout './usr/bin/maple' > "${tmp}/maple" &&
verify_linux_package_executable_metadata "${tmp}/maple" &&
"$(python3_runner)" "${SCRIPT_DIR}/tauri_bundle_type.py" --expect rpm "${tmp}/maple"
) || status=$?
rm -rf "${tmp}"
return "${status}"
}

verify_linux_desktop_package_metadata() {
local artifact

Expand All @@ -3724,6 +3741,9 @@ verify_linux_desktop_package_metadata() {
*.deb)
verify_linux_deb_package_executable_metadata "${artifact}"
;;
*.rpm)
verify_linux_rpm_package_executable_metadata "${artifact}"
;;
esac
done
}
Expand Down Expand Up @@ -3753,17 +3773,19 @@ normalize_deb_package() {
extract_deb_payload() {
local deb="$1"
local payload="$2"
local tmp
local tmp status
tmp="$(mktemp -d)"
status=0

(
cd "${tmp}"
ar x "${deb}"
mkdir -p "${payload}"
cd "${tmp}" &&
ar x "${deb}" &&
mkdir -p "${payload}" &&
tar -xzf data.tar.gz -C "${payload}"
)
) || status=$?

rm -rf "${tmp}"
return "${status}"
}

touch_tree_to_source_date_epoch() {
Expand All @@ -3786,14 +3808,14 @@ rpm_arch() {
esac
}

rebuild_rpm_package_from_payload() {
build_rpm_package_from_payload() {
local rpm="$1"
local payload="$2"
local topdir spec built_rpm
local version release arch rpm_build_shell

command -v rpmbuild >/dev/null 2>&1 || {
echo "rpmbuild is required to normalize RPM packages. Run through the flake CI shell." >&2
echo "rpmbuild is required to build RPM packages. Run through the flake CI shell." >&2
return 1
}
rpm_build_shell="$(command -v bash)"
Expand Down Expand Up @@ -3848,7 +3870,10 @@ EOF
--define "_buildhost (none)" \
--define "use_source_date_epoch_as_buildtime 1" \
--define "_build_name_fmt %{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm" \
--define "_binary_payload w9.gzdio"
--define "_binary_payload w9.gzdio" || {
rm -rf "${topdir}"
return 1
}

built_rpm="$(find "${topdir}/RPMS" -type f -name '*.rpm' | LC_ALL=C sort | head -n 1)"
if [ -z "${built_rpm}" ]; then
Expand All @@ -3857,7 +3882,10 @@ EOF
return 1
fi

mv "${built_rpm}" "${rpm}"
mv "${built_rpm}" "${rpm}" || {
rm -rf "${topdir}"
return 1
}
rm -rf "${topdir}"
}

Expand All @@ -3866,35 +3894,33 @@ normalize_linux_desktop_packages() {
return 0
fi

local deb rpm payload
local deb rpm payload status
local -a debs=()
local -a rpms=()

while IFS= read -r -d '' deb; do
debs+=("${deb}")
done < <(find "${TAURI_DIR}/target/release/bundle/deb" -type f -name '*.deb' -print0 2>/dev/null | LC_ALL=C sort -z)

while IFS= read -r -d '' rpm; do
rpms+=("${rpm}")
done < <(find "${TAURI_DIR}/target/release/bundle/rpm" -type f -name '*.rpm' -print0 2>/dev/null | LC_ALL=C sort -z)

for deb in "${debs[@]}"; do
normalize_deb_package "${deb}"
done

if [ "${#rpms[@]}" -gt 0 ]; then
if [ "${#debs[@]}" -eq 0 ]; then
echo "Cannot normalize RPM packages without a Debian payload source." >&2
return 1
fi

payload="$(mktemp -d)"
extract_deb_payload "${debs[0]}" "${payload}"
for rpm in "${rpms[@]}"; do
rebuild_rpm_package_from_payload "${rpm}" "${payload}"
done
rm -rf "${payload}"
if [ "${#debs[@]}" -ne 1 ]; then
echo "Linux packaging requires exactly one Debian payload source." >&2
return 1
fi

normalize_deb_package "${debs[0]}"
rpm="${TAURI_DIR}/target/release/bundle/rpm/$(jq -r --arg arch "$(rpm_arch)" \
'"Maple-\(.version)-\(.bundle.linux.rpm.release // "1").\($arch).rpm"' "${TAURI_DIR}/tauri.conf.json")"
mkdir -p "$(dirname "${rpm}")"
payload="$(mktemp -d)"
status=0
(
extract_deb_payload "${debs[0]}" "${payload}" &&
# Tauri selects both the update artifact and installer from this marker.
# Change only the RPM staging copy; the normalized DEB keeps its identity.
"$(python3_runner)" "${SCRIPT_DIR}/tauri_bundle_type.py" --expect deb --set rpm "${payload}/usr/bin/maple" &&
build_rpm_package_from_payload "${rpm}" "${payload}"
) || status=$?
rm -rf "${payload}"
return "${status}"
}

linux_tauri_pr_config() {
Expand All @@ -3910,7 +3936,7 @@ linux_tauri_pr_config() {
bundle: {
createUpdaterArtifacts: false,
useLocalToolsDir: true,
targets: ["appimage", "deb", "rpm"],
targets: ["appimage", "deb"],
linux: {
appimage: {
bundleMediaFramework: true,
Expand All @@ -3922,11 +3948,6 @@ linux_tauri_pr_config() {
files: {
"/usr/lib/maple/libonnxruntime.so": $ort
}
},
rpm: {
files: {
"/usr/lib/maple/libonnxruntime.so": $ort
}
}
}
}
Expand All @@ -3952,7 +3973,7 @@ linux_tauri_release_config() {
bundle: {
createUpdaterArtifacts: true,
useLocalToolsDir: true,
targets: ["appimage", "deb", "rpm"],
targets: ["appimage", "deb"],
linux: {
appimage: {
bundleMediaFramework: true,
Expand All @@ -3964,11 +3985,6 @@ linux_tauri_release_config() {
files: {
"/usr/lib/maple/libonnxruntime.so": $ort
}
},
rpm: {
files: {
"/usr/lib/maple/libonnxruntime.so": $ort
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/release-verification-guide.sh
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ That command verifies portable hashes and skips host-specific checks that cannot

\`\`\`bash
# Linux desktop installers: verifies final hashes, Tauri updater signatures,
# AppImage executable metadata, and .deb executable metadata.
# AppImage/DEB/RPM executable metadata and package-specific updater identity.
nix develop --no-update-lock-file .#desktop-linux -c ./scripts/ci/verify-release-artifacts.sh artifacts linux

# macOS desktop installers: run on macOS with Xcode selected.
Expand Down
56 changes: 56 additions & 0 deletions scripts/ci/tauri_bundle_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
"""Verify Tauri's package marker; correct it when repacking a DEB payload as RPM."""

import argparse
from pathlib import Path
import sys


# Tauri's bundler patches this fixed-length token in the packaged executable.
PREFIX = b"__TAURI_BUNDLE_TYPE_VAR_"
SUFFIXES = {"deb": b"DEB", "rpm": b"RPM", "appimage": b"APP"}


def verify_bundle_type(path, expected, replacement=None):
if replacement is not None and (expected, replacement) != ("deb", "rpm"):
raise ValueError("only the DEB-to-RPM transition is supported")
if path.is_symlink() or not path.is_file():
raise ValueError("expected a regular executable file")

data = path.read_bytes()
if data.count(PREFIX) != 1:
raise ValueError("expected exactly one Tauri bundle marker")
offset = data.index(PREFIX) + len(PREFIX)
suffix = data[offset:offset + 3]
if len(suffix) != 3:
raise ValueError("truncated Tauri bundle marker")
if suffix not in SUFFIXES.values():
raise ValueError("unrecognized Tauri bundle marker")
if suffix != SUFFIXES[expected]:
raise ValueError(f"expected {expected} bundle marker, found {suffix.decode().lower()}")

if replacement is not None:
# Patch only the three-byte suffix, retaining all other bytes and mode.
with path.open("r+b") as stream:
stream.seek(offset)
stream.write(SUFFIXES[replacement])


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--expect", required=True, choices=SUFFIXES)
parser.add_argument("--set", choices=["rpm"], dest="replacement")
parser.add_argument("file", type=Path)
args = parser.parse_args()
try:
verify_bundle_type(args.file, args.expect, args.replacement)
except (OSError, ValueError) as error:
print(f"Tauri bundle type check failed: {error}", file=sys.stderr)
return 1
kind = args.replacement or args.expect
print(f"verified-tauri-bundle-type {kind} {args.file}")
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading