new scripts
This commit is contained in:
@@ -3,3 +3,5 @@ build/
|
|||||||
DerivedData/
|
DerivedData/
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
/releases/
|
||||||
|
/.build/
|
||||||
|
|||||||
Regular → Executable
+57
-14
@@ -1,22 +1,65 @@
|
|||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd /Volumes/Developer/Applications/DbSyn
|
# Resolve script location under Bash or Zsh.
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
# Remove previous build
|
cd "$PROJECT_ROOT"
|
||||||
rm -rf ./build
|
|
||||||
|
# Find the Xcode project.
|
||||||
|
PROJECT="$(find "$PROJECT_ROOT" -maxdepth 2 -name '*.xcodeproj' -type d -print -quit)"
|
||||||
|
|
||||||
|
if [[ -z "$PROJECT" ]]; then
|
||||||
|
echo "ERROR: No Xcode project found in $PROJECT_ROOT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Project: $PROJECT"
|
||||||
|
|
||||||
|
# Detect the application scheme.
|
||||||
|
SCHEME="$(xcodebuild -list -json -project "$PROJECT" | python3 -c '
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
schemes = data.get("project", {}).get("schemes", [])
|
||||||
|
|
||||||
|
if not schemes:
|
||||||
|
sys.exit("ERROR: No Xcode schemes found")
|
||||||
|
|
||||||
|
print(schemes[0])
|
||||||
|
')"
|
||||||
|
|
||||||
|
if [[ -z "$SCHEME" ]]; then
|
||||||
|
echo "ERROR: Could not detect Xcode scheme"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Scheme: $SCHEME"
|
||||||
|
|
||||||
|
DERIVED_DATA="$PROJECT_ROOT/.build"
|
||||||
|
|
||||||
|
echo "Building Rsync Studio..."
|
||||||
|
|
||||||
# Clean and build DBSyn
|
|
||||||
xcodebuild \
|
xcodebuild \
|
||||||
-project DBSyn.xcodeproj \
|
-project "$PROJECT" \
|
||||||
-scheme DBSyn \
|
-scheme "$SCHEME" \
|
||||||
-configuration Debug \
|
-configuration Debug \
|
||||||
-destination 'platform=macOS,arch=arm64' \
|
-derivedDataPath "$DERIVED_DATA" \
|
||||||
-derivedDataPath ./build \
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
clean build
|
build
|
||||||
|
|
||||||
# Stop any running instance
|
APP="$(find "$DERIVED_DATA/Build/Products/Debug" -maxdepth 1 -name '*.app' -type d -print -quit)"
|
||||||
pkill -x DBSyn 2>/dev/null || true
|
|
||||||
|
|
||||||
# Launch the newly built application
|
if [[ -z "$APP" ]]; then
|
||||||
open ./build/Build/Products/Debug/DBSyn.app
|
echo "ERROR: Built application not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Build successful: $APP"
|
||||||
|
|
||||||
|
if [[ "${LAUNCH_AFTER_BUILD:-1}" == "1" ]]; then
|
||||||
|
echo "Launching application..."
|
||||||
|
open "$APP"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env swift
|
|
||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
let fm = FileManager.default
|
|
||||||
let home = fm.homeDirectoryForCurrentUser
|
|
||||||
let directory = home.appendingPathComponent("Library/Application Support/Rsync Studio Licensing", isDirectory: true)
|
|
||||||
let privateURL = directory.appendingPathComponent("license-private-key.txt")
|
|
||||||
let publicURL = directory.appendingPathComponent("license-public-key.txt")
|
|
||||||
|
|
||||||
try fm.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
||||||
if fm.fileExists(atPath: privateURL.path) || fm.fileExists(atPath: publicURL.path) {
|
|
||||||
fputs("Refusing to overwrite an existing licensing keypair at \(directory.path)\n", stderr)
|
|
||||||
exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
let privateKey = Curve25519.Signing.PrivateKey()
|
|
||||||
let privateText = privateKey.rawRepresentation.base64EncodedString()
|
|
||||||
let publicText = privateKey.publicKey.rawRepresentation.base64EncodedString()
|
|
||||||
try privateText.write(to: privateURL, atomically: true, encoding: .utf8)
|
|
||||||
try publicText.write(to: publicURL, atomically: true, encoding: .utf8)
|
|
||||||
try fm.setAttributes([.posixPermissions: 0o600], ofItemAtPath: privateURL.path)
|
|
||||||
try fm.setAttributes([.posixPermissions: 0o644], ofItemAtPath: publicURL.path)
|
|
||||||
|
|
||||||
print("Created Rsync Studio Ed25519 licensing keypair.")
|
|
||||||
print("PRIVATE (KEEP SECRET): \(privateURL.path)")
|
|
||||||
print("PUBLIC: \(publicURL.path)")
|
|
||||||
print("\nPublic key for RsyncStudioLicensePublicKey:\n\(publicText)")
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env swift
|
|
||||||
import Foundation
|
|
||||||
import CryptoKit
|
|
||||||
|
|
||||||
struct Payload: Codable {
|
|
||||||
let licenseID: String
|
|
||||||
let customerName: String?
|
|
||||||
let customerEmail: String?
|
|
||||||
let edition: String
|
|
||||||
let installationID: String
|
|
||||||
let issuedAt: Date
|
|
||||||
let expiresAt: Date?
|
|
||||||
let maxMajorVersion: Int?
|
|
||||||
}
|
|
||||||
|
|
||||||
func base64URL(_ data: Data) -> String {
|
|
||||||
data.base64EncodedString()
|
|
||||||
.replacingOccurrences(of: "+", with: "-")
|
|
||||||
.replacingOccurrences(of: "/", with: "_")
|
|
||||||
.replacingOccurrences(of: "=", with: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
let args = CommandLine.arguments
|
|
||||||
func value(_ flag: String) -> String? {
|
|
||||||
guard let index = args.firstIndex(of: flag), index + 1 < args.count else { return nil }
|
|
||||||
return args[index + 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let installationID = value("--installation-id"), UUID(uuidString: installationID) != nil else {
|
|
||||||
fputs("Usage: swift Scripts/license-sign.swift --installation-id UUID [--name NAME] [--email EMAIL] [--edition Professional] [--license-id ID] [--max-major 1] [--expires YYYY-MM-DD]\n", stderr)
|
|
||||||
exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
let fm = FileManager.default
|
|
||||||
let defaultPrivate = fm.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support/Rsync Studio Licensing/license-private-key.txt")
|
|
||||||
let privateURL = value("--private-key").map { URL(fileURLWithPath: $0) } ?? defaultPrivate
|
|
||||||
let privateText = try String(contentsOf: privateURL, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
guard let privateData = Data(base64Encoded: privateText) else { throw NSError(domain: "RsyncStudioLicensing", code: 1, userInfo: [NSLocalizedDescriptionKey: "Private key is not Base64."]) }
|
|
||||||
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateData)
|
|
||||||
|
|
||||||
var expiry: Date?
|
|
||||||
if let text = value("--expires") {
|
|
||||||
let formatter = DateFormatter()
|
|
||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
||||||
formatter.dateFormat = "yyyy-MM-dd"
|
|
||||||
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
|
||||||
guard let date = formatter.date(from: text) else {
|
|
||||||
fputs("Invalid --expires date; use YYYY-MM-DD.\n", stderr)
|
|
||||||
exit(2)
|
|
||||||
}
|
|
||||||
expiry = date
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload = Payload(
|
|
||||||
licenseID: value("--license-id") ?? UUID().uuidString.lowercased(),
|
|
||||||
customerName: value("--name"),
|
|
||||||
customerEmail: value("--email"),
|
|
||||||
edition: value("--edition") ?? "Professional",
|
|
||||||
installationID: installationID.lowercased(),
|
|
||||||
issuedAt: Date(),
|
|
||||||
expiresAt: expiry,
|
|
||||||
maxMajorVersion: value("--max-major").flatMap(Int.init) ?? 1
|
|
||||||
)
|
|
||||||
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
encoder.outputFormatting = [.sortedKeys]
|
|
||||||
encoder.dateEncodingStrategy = .iso8601
|
|
||||||
let data = try encoder.encode(payload)
|
|
||||||
let signature = try privateKey.signature(for: data)
|
|
||||||
let token = "RS3.\(base64URL(data)).\(base64URL(signature))"
|
|
||||||
print(token)
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
if ! grep -Eq 'INFOPLIST_KEY_RsyncStudioLicensePublicKey = "[A-Za-z0-9+/_=-]{40,}";' "$PROJECT/project.pbxproj"; then
|
|
||||||
echo "RsyncStudioLicensePublicKey is not configured. Generate the licensing keypair and embed the PUBLIC key before a commercial release." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
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."
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
#!/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."
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
|
|
||||||
grep -q 'final class LicensingManager' "$ROOT/Rsync Studio/Services/LicensingManager.swift"
|
|
||||||
grep -q 'trialLengthDays = 30' "$ROOT/Rsync Studio/Services/LicensingManager.swift"
|
|
||||||
grep -q 'Curve25519.Signing.PublicKey' "$ROOT/Rsync Studio/Services/LicensingManager.swift"
|
|
||||||
grep -q 'RsyncStudioLicensePublicKey' "$PROJECT/project.pbxproj"
|
|
||||||
if find "$ROOT" -type f \( -iname '*private*key*' -o -iname '*.p8' \) ! -path '*/build/*' | grep -q .; then
|
|
||||||
echo "A private signing key appears to be present in the project tree." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
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"
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
SERVER="$ROOT/rsync-studio-licensing"
|
|
||||||
[[ -d "$SERVER" ]] || { echo 'Licensing Server directory is missing.' >&2; exit 1; }
|
|
||||||
if command -v php >/dev/null 2>&1; then
|
|
||||||
while IFS= read -r -d '' file; do php -l "$file" >/dev/null; done < <(find "$SERVER" -name '*.php' -type f -print0)
|
|
||||||
echo 'OK: all Stage 4 PHP files pass php -l'
|
|
||||||
else
|
|
||||||
echo 'WARN: PHP is unavailable; skipped php -l checks.'
|
|
||||||
fi
|
|
||||||
bash -n "$SERVER/install-overlay.sh"
|
|
||||||
! grep -RniE --exclude='*.md' --exclude='*.example' '(PRIVATE[_ -]?KEY|PADDLE_WEBHOOK_SECRET|LEMON_SQUEEZY_WEBHOOK_SECRET)[[:space:]]*=[[:space:]]*[A-Za-z0-9+/=_-]{20,}' "$SERVER" >/dev/null || { echo 'Possible committed secret detected.' >&2; exit 1; }
|
|
||||||
grep -q "sodium_crypto_sign_detached" "$SERVER/app/Services/Licensing/LicenseSigner.php"
|
|
||||||
grep -q "hash_equals" "$SERVER/app/Services/Payments/PaddleWebhookVerifier.php"
|
|
||||||
grep -q "hash_equals" "$SERVER/app/Services/Payments/LemonSqueezyWebhookVerifier.php"
|
|
||||||
grep -q "lockForUpdate" "$SERVER/app/Http/Controllers/Api/LicenseActivationController.php"
|
|
||||||
grep -q "license_delivery_outbox" "$SERVER/database/migrations/2026_09_08_200000_create_rsync_studio_licensing_tables.php"
|
|
||||||
echo 'Stage 4 licensing-server static checks passed.'
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
LS="$ROOT/rsync-studio-licensing"
|
|
||||||
find "$LS/app" "$LS/config" "$LS/database" "$LS/routes" -name '*.php' -print0 | while IFS= read -r -d '' f; do php -l "$f" >/dev/null; done
|
|
||||||
grep -q "implements MustVerifyEmail" "$LS/app/Models/User.php"
|
|
||||||
grep -q "AdminOnly::class" "$LS/routes/web.stage5.php"
|
|
||||||
grep -q "middleware(\['auth','verified'\])" "$LS/routes/web.stage5.php"
|
|
||||||
grep -q "api: __DIR__.'/../routes/api.php'" "$LS/bootstrap/app.php"
|
|
||||||
grep -q "RsyncStudioLicensingServiceProvider::class" "$LS/bootstrap/providers.php"
|
|
||||||
grep -q "create_rsync_studio_auth_tables" <(find "$LS/database/migrations" -maxdepth 1 -type f -printf '%f\n')
|
|
||||||
echo 'Stage 5 integrated licensing portal static verification passed.'
|
|
||||||
Regular → Executable
+16
-13
@@ -1,11 +1,11 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# DBSyn direct distribution: Developer ID signed, notarized app and DMG.
|
# Rsync Studio direct distribution: Developer ID signed, notarized app and DMG.
|
||||||
# macOS/Xcode required. Install as scripts/release-dbsyn-direct.sh.
|
# macOS/Xcode required. Install as Scripts/release.sh.
|
||||||
APP_NAME=DBSyn
|
APP_NAME="Rsync Studio"
|
||||||
SCHEME=DBSyn
|
SCHEME="Rsync Studio"
|
||||||
PROJECT=DBSyn.xcodeproj
|
PROJECT="Rsync Studio.xcodeproj"
|
||||||
TEAM_ID="${TEAM_ID:-39R6U3437D}"
|
TEAM_ID="${TEAM_ID:-39R6U3437D}"
|
||||||
SIGNING_IDENTITY="${SIGNING_IDENTITY:-B99D353C88996866C49E4389B375645F76870A3F}"
|
SIGNING_IDENTITY="${SIGNING_IDENTITY:-B99D353C88996866C49E4389B375645F76870A3F}"
|
||||||
NOTARY_PROFILE="${NOTARY_PROFILE:-orion-notary}"
|
NOTARY_PROFILE="${NOTARY_PROFILE:-orion-notary}"
|
||||||
@@ -49,7 +49,7 @@ cat > "$EXPORT_OPTIONS" <<PLIST
|
|||||||
</dict></plist>
|
</dict></plist>
|
||||||
PLIST
|
PLIST
|
||||||
|
|
||||||
echo '==> Archiving DBSyn with hardened runtime...'
|
echo '==> Archiving Rsync Studio with hardened runtime...'
|
||||||
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \
|
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \
|
||||||
-destination 'generic/platform=macOS' -archivePath "$ARCHIVE_PATH" \
|
-destination 'generic/platform=macOS' -archivePath "$ARCHIVE_PATH" \
|
||||||
DEVELOPMENT_TEAM="$TEAM_ID" CODE_SIGN_STYLE=Automatic \
|
DEVELOPMENT_TEAM="$TEAM_ID" CODE_SIGN_STYLE=Automatic \
|
||||||
@@ -64,20 +64,23 @@ xcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" \
|
|||||||
# Xcode's generated Info.plist can omit CFBundleIconFile even when the
|
# Xcode's generated Info.plist can omit CFBundleIconFile even when the
|
||||||
# build setting specifies it. Set it on the exported bundle BEFORE the
|
# build setting specifies it. Set it on the exported bundle BEFORE the
|
||||||
# final Developer ID signature, so the signed app includes the icon key.
|
# final Developer ID signature, so the signed app includes the icon key.
|
||||||
ICON_PATH="$APP_PATH/Contents/Resources/DBSyn.icns"
|
ICON_PATH="$APP_PATH/Contents/Resources/AppIcon.icns"
|
||||||
|
if [[ ! -s "$ICON_PATH" ]]; then
|
||||||
|
ICON_PATH="$(find "$APP_PATH/Contents/Resources" -maxdepth 1 -name "*.icns" -type f -print -quit)"
|
||||||
|
fi
|
||||||
[[ -s "$ICON_PATH" ]] || { echo "ERROR: Missing bundled icon $ICON_PATH" >&2; exit 1; }
|
[[ -s "$ICON_PATH" ]] || { echo "ERROR: Missing bundled icon $ICON_PATH" >&2; exit 1; }
|
||||||
PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
PLIST_PATH="$APP_PATH/Contents/Info.plist"
|
||||||
ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" 2>/dev/null || true)"
|
ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" 2>/dev/null || true)"
|
||||||
if [[ "$ICON_KEY" != DBSyn && "$ICON_KEY" != DBSyn.icns ]]; then
|
if [[ "$ICON_KEY" != "$(basename "$ICON_PATH" .icns)" && "$ICON_KEY" != "$(basename "$ICON_PATH")" ]]; then
|
||||||
echo "==> Setting CFBundleIconFile in exported app (before signing)..."
|
echo "==> Setting CFBundleIconFile in exported app (before signing)..."
|
||||||
if /usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" >/dev/null 2>&1; then
|
if /usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" >/dev/null 2>&1; then
|
||||||
/usr/libexec/PlistBuddy -c 'Set :CFBundleIconFile DBSyn.icns' "$PLIST_PATH"
|
/usr/libexec/PlistBuddy -c 'Set :CFBundleIconFile $(basename "$ICON_PATH")' "$PLIST_PATH"
|
||||||
else
|
else
|
||||||
/usr/libexec/PlistBuddy -c 'Add :CFBundleIconFile string DBSyn.icns' "$PLIST_PATH"
|
/usr/libexec/PlistBuddy -c 'Add :CFBundleIconFile string $(basename "$ICON_PATH")' "$PLIST_PATH"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH")"
|
ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH")"
|
||||||
[[ "$ICON_KEY" == DBSyn || "$ICON_KEY" == DBSyn.icns ]] || {
|
[[ "$ICON_KEY" == "$(basename "$ICON_PATH" .icns)" || "$ICON_KEY" == "$(basename "$ICON_PATH")" ]] || {
|
||||||
echo "ERROR: CFBundleIconFile still incorrect: '$ICON_KEY'" >&2; exit 1;
|
echo "ERROR: CFBundleIconFile still incorrect: '$ICON_KEY'" >&2; exit 1;
|
||||||
}
|
}
|
||||||
echo "==> Application icon verified: $ICON_KEY"
|
echo "==> Application icon verified: $ICON_KEY"
|
||||||
@@ -116,7 +119,7 @@ MOUNTED=1
|
|||||||
ditto "$DMG_STAGE/$APP_NAME.app" "$MOUNT_POINT/$APP_NAME.app"
|
ditto "$DMG_STAGE/$APP_NAME.app" "$MOUNT_POINT/$APP_NAME.app"
|
||||||
ln -s /Applications "$MOUNT_POINT/Applications"
|
ln -s /Applications "$MOUNT_POINT/Applications"
|
||||||
cp "$ICON_PATH" "$MOUNT_POINT/.VolumeIcon.icns"
|
cp "$ICON_PATH" "$MOUNT_POINT/.VolumeIcon.icns"
|
||||||
xcrun SetFile -a C "$MOUNT_POINT"
|
if xcrun --find SetFile >/dev/null 2>&1; then xcrun SetFile -a C "$MOUNT_POINT"; else echo "WARNING: SetFile unavailable; skipping custom volume icon flag"; fi
|
||||||
hdiutil detach "$MOUNT_POINT" -quiet
|
hdiutil detach "$MOUNT_POINT" -quiet
|
||||||
MOUNTED=0
|
MOUNTED=0
|
||||||
hdiutil convert "$RW_DMG" -format UDZO -o "$TMP_DMG" -ov
|
hdiutil convert "$RW_DMG" -format UDZO -o "$TMP_DMG" -ov
|
||||||
@@ -154,4 +157,4 @@ ditto "$TMP_DMG" "$DMG_PATH"
|
|||||||
echo '==> Release ready:'
|
echo '==> Release ready:'
|
||||||
ls -lh "$DMG_PATH"
|
ls -lh "$DMG_PATH"
|
||||||
shasum -a 256 "$DMG_PATH"
|
shasum -a 256 "$DMG_PATH"
|
||||||
printf '\n========== DBSyn RELEASE COMPLETE ==========\n%s\n' "$DMG_PATH"
|
printf '\n========== RSYNC STUDIO RELEASE COMPLETE ==========\n%s\n' "$DMG_PATH"
|
||||||
|
|||||||
Reference in New Issue
Block a user