latest updates

This commit is contained in:
Mark Gregory
2026-09-08 20:06:58 +01:00
parent fb05cf71ed
commit 868604b848
53 changed files with 6442 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT="$ROOT/Rsync Studio.xcodeproj"
SCHEME="Rsync Studio"
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "release-preflight.sh must run on macOS with Xcode installed." >&2
exit 2
fi
for tool in xcodebuild plutil codesign spctl; do
command -v "$tool" >/dev/null || { echo "Missing required tool: $tool" >&2; exit 2; }
done
"$SCRIPT_DIR/verify-release-config.sh"
"$SCRIPT_DIR/security-check.sh"
plutil -lint "$ROOT/Rsync Studio/Resources/PrivacyInfo.xcprivacy"
BUILD_DIR="$ROOT/build/preflight"
rm -rf "$BUILD_DIR"
xcodebuild \
-project "$PROJECT" \
-scheme "$SCHEME" \
-configuration Release \
-destination 'platform=macOS' \
-derivedDataPath "$BUILD_DIR" \
CODE_SIGNING_ALLOWED=NO \
clean build
APP="$BUILD_DIR/Build/Products/Release/Rsync Studio.app"
test -d "$APP" || { echo "Release app was not produced at $APP" >&2; exit 1; }
echo "Unsigned Release build passed: $APP"
echo "Next: Archive with Developer ID, notarise, staple, then run the signed-app checks in Docs/RELEASE.md."
+206
View File
@@ -0,0 +1,206 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT="$ROOT/Rsync Studio.xcodeproj"
SCHEME="Rsync Studio"
CONFIGURATION="Release"
PRODUCT_NAME="Rsync Studio"
DISPLAY_NAME="Rsync Studio"
BUNDLE_ID="net.absdev.RsyncStudio"
NOTARY_PROFILE="${NOTARY_PROFILE:-Rsync Studio Notary}"
RELEASE_ROOT="${RELEASE_ROOT:-$ROOT/build/releases}"
SKIP_PREFLIGHT=0
SKIP_NOTARIZATION=0
usage() {
cat <<USAGE
Usage: $(basename "$0") [options]
Builds a Developer ID archive, exports the signed app, creates a drag-to-Applications
DMG, notarizes/staples it, verifies Gatekeeper, and writes SHA-256 checksums.
Nothing is uploaded to your website or update feed.
Options:
--notary-profile NAME Keychain profile created by notarytool (default: $NOTARY_PROFILE)
--output DIR Release output root (default: build/releases)
--skip-preflight Skip Scripts/release-preflight.sh
--skip-notarization Build/sign/package only; do not submit to Apple
-h, --help Show this help
Environment:
DEVELOPMENT_TEAM Optional Apple Developer Team ID. Recommended for CI.
NOTARY_PROFILE Alternative to --notary-profile.
RELEASE_ROOT Alternative to --output.
First-time notarization setup:
xcrun notarytool store-credentials "$NOTARY_PROFILE"
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--notary-profile) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 2; }; NOTARY_PROFILE="$2"; shift 2 ;;
--output) [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 2; }; RELEASE_ROOT="$2"; shift 2 ;;
--skip-preflight) SKIP_PREFLIGHT=1; shift ;;
--skip-notarization) SKIP_NOTARIZATION=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
[[ "$(uname -s)" == "Darwin" ]] || { echo "ERROR: release.sh must run on macOS." >&2; exit 2; }
for tool in xcodebuild xcrun hdiutil codesign spctl plutil shasum ditto; do
command -v "$tool" >/dev/null || { echo "ERROR: Missing required tool: $tool" >&2; exit 2; }
done
PBX="$PROJECT/project.pbxproj"
VERSION="$(awk -F' = ' '/MARKETING_VERSION = / {gsub(/;/,"",$2); print $2; exit}' "$PBX")"
BUILD="$(awk -F' = ' '/CURRENT_PROJECT_VERSION = / {gsub(/;/,"",$2); print $2; exit}' "$PBX")"
[[ -n "$VERSION" && -n "$BUILD" ]] || { echo "ERROR: Could not determine version/build from Xcode project." >&2; exit 1; }
RELEASE_DIR="$RELEASE_ROOT/$VERSION-$BUILD"
ARCHIVE="$RELEASE_DIR/$PRODUCT_NAME.xcarchive"
EXPORT_DIR="$RELEASE_DIR/export"
DMG_STAGING="$RELEASE_DIR/dmg-root"
DMG="$RELEASE_DIR/$PRODUCT_NAME-$VERSION.dmg"
CHECKSUM="$DMG.sha256"
METADATA="$RELEASE_DIR/release-info.txt"
EXPORT_OPTIONS="$RELEASE_DIR/ExportOptions.plist"
if [[ -e "$RELEASE_DIR" ]]; then
echo "ERROR: Release directory already exists: $RELEASE_DIR" >&2
echo "Refusing to overwrite an existing version/build artifact." >&2
exit 1
fi
mkdir -p "$RELEASE_DIR"
cleanup() {
rm -rf "$DMG_STAGING"
}
trap cleanup EXIT
if [[ "$SKIP_PREFLIGHT" -eq 0 ]]; then
"$SCRIPT_DIR/release-preflight.sh"
fi
cat > "$EXPORT_OPTIONS" <<PLIST
<?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>
<key>method</key>
<string>developer-id</string>
<key>signingStyle</key>
<string>automatic</string>
<key>stripSwiftSymbols</key>
<true/>
</dict>
</plist>
PLIST
plutil -lint "$EXPORT_OPTIONS" >/dev/null
XCODE_SIGN_ARGS=()
if [[ -n "${DEVELOPMENT_TEAM:-}" ]]; then
XCODE_SIGN_ARGS+=("DEVELOPMENT_TEAM=$DEVELOPMENT_TEAM")
fi
echo "== Archive $DISPLAY_NAME $VERSION ($BUILD) =="
xcodebuild \
-project "$PROJECT" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-destination 'generic/platform=macOS' \
-archivePath "$ARCHIVE" \
clean archive \
"${XCODE_SIGN_ARGS[@]}"
echo "== Export Developer ID app =="
xcodebuild \
-exportArchive \
-archivePath "$ARCHIVE" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$EXPORT_OPTIONS" \
-allowProvisioningUpdates
APP="$EXPORT_DIR/$PRODUCT_NAME.app"
[[ -d "$APP" ]] || { echo "ERROR: Exported app not found: $APP" >&2; exit 1; }
ACTUAL_BUNDLE_ID="$(plutil -extract CFBundleIdentifier raw -o - "$APP/Contents/Info.plist" 2>/dev/null || true)"
[[ "$ACTUAL_BUNDLE_ID" == "$BUNDLE_ID" ]] || { echo "ERROR: Exported bundle id is '$ACTUAL_BUNDLE_ID', expected '$BUNDLE_ID'." >&2; exit 1; }
"$SCRIPT_DIR/verify-signed-app.sh" "$APP"
if [[ "$SKIP_NOTARIZATION" -eq 0 ]]; then
echo "== Validate notarization credentials =="
xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null
echo "== Notarize exported app =="
APP_ZIP="$RELEASE_DIR/$PRODUCT_NAME-$VERSION-notary.zip"
ditto -c -k --keepParent "$APP" "$APP_ZIP"
xcrun notarytool submit "$APP_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
rm -f "$APP_ZIP"
"$SCRIPT_DIR/verify-signed-app.sh" "$APP"
fi
echo "== Create drag-to-Applications DMG =="
mkdir -p "$DMG_STAGING"
ditto "$APP" "$DMG_STAGING/$PRODUCT_NAME.app"
ln -s /Applications "$DMG_STAGING/Applications"
cat > "$DMG_STAGING/README.txt" <<TXT
$DISPLAY_NAME $VERSION
Install: drag $DISPLAY_NAME to Applications.
Then eject this disk image and launch $DISPLAY_NAME from Applications.
TXT
hdiutil create \
-volname "$DISPLAY_NAME $VERSION" \
-srcfolder "$DMG_STAGING" \
-ov \
-format UDZO \
-imagekey zlib-level=9 \
"$DMG" >/dev/null
hdiutil verify "$DMG" >/dev/null
if [[ "$SKIP_NOTARIZATION" -eq 0 ]]; then
echo "== Notarize final DMG =="
xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
echo "== Final Gatekeeper verification =="
"$SCRIPT_DIR/verify-signed-app.sh" "$APP"
spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG"
else
echo "WARNING: Notarization was skipped. This output is NOT ready for public distribution." >&2
fi
printf '%s %s\n' "$(shasum -a 256 "$DMG" | awk '{print $1}')" "$(basename "$DMG")" > "$CHECKSUM"
DMG_SIZE="$(stat -f '%z' "$DMG")"
DMG_SHA256="$(awk '{print $1}' "$CHECKSUM")"
cat > "$METADATA" <<INFO
Product: $DISPLAY_NAME
Bundle ID: $BUNDLE_ID
Version: $VERSION
Build: $BUILD
Artifact: $(basename "$DMG")
Bytes: $DMG_SIZE
SHA-256: $DMG_SHA256
Notarized: $([[ "$SKIP_NOTARIZATION" -eq 0 ]] && echo yes || echo no)
Notary profile: $NOTARY_PROFILE
Generated (UTC): $(date -u '+%Y-%m-%dT%H:%M:%SZ')
INFO
rm -f "$EXPORT_OPTIONS"
echo
echo "Release artifact created successfully:"
echo " $DMG"
echo " $CHECKSUM"
echo " $METADATA"
echo
echo "Publishing is intentionally NOT automated. Test the DMG on a clean Mac before upload."
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT="$ROOT/Rsync Studio.xcodeproj"
echo "== Rsync Studio project security checks =="
grep -q 'ENABLE_HARDENED_RUNTIME = YES' "$PROJECT/project.pbxproj"
grep -q 'ENABLE_APP_SANDBOX = NO' "$PROJECT/project.pbxproj"
grep -q 'INFOPLIST_KEY_CFBundleIconName = AppIcon' "$PROJECT/project.pbxproj"
test -f "$ROOT/Rsync Studio/Resources/PrivacyInfo.xcprivacy"
if grep -R --line-number -E 'StrictHostKeyChecking[ =]+no|UserKnownHostsFile[ =]+/dev/null|sshpass' "$ROOT/Rsync Studio" --include='*.swift'; then
echo "Unsafe SSH configuration detected." >&2
exit 1
fi
grep -q 'BatchMode=yes' "$ROOT/Rsync Studio/Services/RsyncCommandBuilder.swift"
grep -q 'allowUnattendedDelete' "$ROOT/Rsync Studio/Models/SyncJob.swift"
grep -q 'RsyncSecurityValidator' "$ROOT/Rsync Studio/Services/RsyncCommandBuilder.swift"
grep -q 'isSafeRemotePath' "$ROOT/Rsync Studio/Services/RsyncCommandBuilder.swift"
grep -q 'hasBalancedShellSyntax' "$ROOT/Rsync Studio/Services/RsyncCommandBuilder.swift"
grep -q -- '--files-from' "$ROOT/Rsync Studio/Services/RsyncCommandBuilder.swift"
grep -q -- '--log-file' "$ROOT/Rsync Studio/Services/RsyncCommandBuilder.swift"
echo "Static policy checks passed."
echo ""
echo "After creating a signed Release app, also run:"
echo " codesign --verify --deep --strict --verbose=2 /path/to/Rsync Studio.app"
echo " spctl --assess --type execute --verbose=4 /path/to/Rsync Studio.app"
echo " xcrun stapler validate /path/to/Rsync Studio.app"
+51
View File
@@ -0,0 +1,51 @@
#!/bin/bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PBX="$ROOT/Rsync Studio.xcodeproj/project.pbxproj"
EXPECTED_BUNDLE_ID="net.absdev.RsyncStudio"
EXPECTED_VERSION="1.0.0"
EXPECTED_BUILD="1"
EXPECTED_MIN_MACOS="14.0"
fail() { echo "ERROR: $*" >&2; exit 1; }
pass() { echo "OK: $*"; }
[[ -f "$PBX" ]] || fail "Missing Xcode project file"
[[ $(grep -c "PRODUCT_BUNDLE_IDENTIFIER = $EXPECTED_BUNDLE_ID;" "$PBX") -ge 2 ]] || fail "Bundle identifier must remain $EXPECTED_BUNDLE_ID in all app configurations"
pass "Bundle identifier is $EXPECTED_BUNDLE_ID"
[[ $(grep -c "MARKETING_VERSION = $EXPECTED_VERSION;" "$PBX") -ge 2 ]] || fail "Marketing version must be $EXPECTED_VERSION"
pass "Marketing version is $EXPECTED_VERSION"
[[ $(grep -c "CURRENT_PROJECT_VERSION = $EXPECTED_BUILD;" "$PBX") -ge 2 ]] || fail "Build number must be $EXPECTED_BUILD"
pass "Build number is $EXPECTED_BUILD"
[[ $(grep -c 'ENABLE_HARDENED_RUNTIME = YES;' "$PBX") -ge 2 ]] || fail "Hardened Runtime must be enabled"
pass "Hardened Runtime is enabled"
[[ $(grep -c 'ENABLE_APP_SANDBOX = NO;' "$PBX") -ge 2 ]] || fail "App Sandbox baseline changed unexpectedly"
pass "App Sandbox remains intentionally disabled for direct distribution"
[[ $(grep -c "MACOSX_DEPLOYMENT_TARGET = $EXPECTED_MIN_MACOS;" "$PBX") -ge 2 ]] || fail "macOS deployment target must remain $EXPECTED_MIN_MACOS"
pass "Minimum macOS is $EXPECTED_MIN_MACOS"
[[ $(grep -c 'INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";' "$PBX") -ge 2 ]] || fail "Application category is missing"
pass "Application category is Utilities"
if grep -q 'CODE_SIGN_IDENTITY = "Apple Development"' "$PBX"; then
fail "A project-level Apple Development identity is pinned; Release distribution must use Developer ID Application"
fi
pass "No development certificate is pinned into the project"
if grep -q 'com.apple.security.get-task-allow' "$ROOT/Rsync Studio"/*.entitlements 2>/dev/null; then
fail "get-task-allow must never ship in a commercial Release build"
fi
pass "No get-task-allow entitlement is present in source entitlements"
cat <<'MSG'
Release identity baseline passed.
Before the first commercial archive, select your Apple Developer Team in Xcode and ensure the archived app is signed with a "Developer ID Application" certificate. The exact certificate identity is intentionally not committed to source control.
MSG
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
set -euo pipefail
APP="${1:-}"
[[ -n "$APP" ]] || { echo "Usage: $0 /path/to/Rsync Studio.app" >&2; exit 2; }
[[ -d "$APP" ]] || { echo "ERROR: App not found: $APP" >&2; exit 2; }
for tool in codesign spctl; do
command -v "$tool" >/dev/null || { echo "ERROR: Missing required tool: $tool" >&2; exit 2; }
done
echo "== Verifying code signature =="
codesign --verify --deep --strict --verbose=2 "$APP"
echo "== Inspecting signing authority =="
SIGNING_INFO="$(codesign -dvvv "$APP" 2>&1)"
printf '%s\n' "$SIGNING_INFO"
grep -q 'Authority=Developer ID Application:' <<<"$SIGNING_INFO" || {
echo "ERROR: App is not signed with Developer ID Application." >&2
exit 1
}
echo "== Checking Hardened Runtime =="
grep -Eq '^CodeDirectory .* flags=.*runtime' <<<"$SIGNING_INFO" || {
echo "ERROR: Hardened Runtime flag was not found in the signed app." >&2
exit 1
}
echo "== Checking release entitlements =="
ENTITLEMENTS="$(codesign -d --entitlements :- "$APP" 2>/dev/null || true)"
if grep -q 'com.apple.security.get-task-allow' <<<"$ENTITLEMENTS"; then
echo "ERROR: Release app contains com.apple.security.get-task-allow." >&2
exit 1
fi
if grep -q '<true/>' <<<"$(printf '%s\n' "$ENTITLEMENTS" | sed -n '/com.apple.security.app-sandbox/,+1p')"; then
echo "ERROR: App Sandbox is unexpectedly enabled in the direct-distribution build." >&2
exit 1
fi
echo "== Gatekeeper assessment =="
spctl --assess --type execute --verbose=4 "$APP"
echo "Signed Developer ID app verification passed: $APP"