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
64 changes: 64 additions & 0 deletions .github/release/Package.swift.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// swift-tools-version: 5.9

// TEMPLATE for the machine-generated release manifest -- the Package.swift that
// exists only at a tagged release commit ("B" in docs/ai/design/release-process.md).
// The _release-target-swift workflow interpolates the three MX_RELEASE
// placeholders (version, url, checksum) and the release-build prep job commits
// the result as B; commit R then restores the source manifest, so every tip of
// main keeps the source-only Package.swift.
//
// KEEP IN SYNC: mxSourceTarget below must stay byte-for-byte identical to the
// target in the repository's Package.swift, so that a release built from source
// (non-Apple hosts, or MX_SOURCE_BUILD=1) is the same build a sibling checkout
// gets. When Package.swift's target changes, change this copy too.

import PackageDescription

// Release: __MX_RELEASE_VERSION__
//
// SwiftPM evaluates this manifest on the consumer's machine, so the selection
// below runs there:
// - Apple hosts get the published prebuilt XCFramework by default -- no
// environment setup. Setting MX_SOURCE_BUILD=1 opts a Mac into compiling
// the tagged release from source instead.
// - Non-Apple hosts always compile from source; an XCFramework binaryTarget
// cannot resolve there.

let mxSourceTarget: Target = .target(
name: "Mx",
path: "src",
exclude: [
"private/cpul",
"private/mxtest",
"private/mx/examples",
],
publicHeadersPath: "include",
cxxSettings: [
.headerSearchPath("private"),
.headerSearchPath("private/pugixml"),
]
)

let mxTarget: Target
#if os(macOS)
if Context.environment["MX_SOURCE_BUILD"] != nil {
mxTarget = mxSourceTarget
} else {
mxTarget = .binaryTarget(
name: "Mx",
url: "__MX_RELEASE_URL__",
checksum: "__MX_RELEASE_CHECKSUM__"
)
}
#else
mxTarget = mxSourceTarget
#endif

let package = Package(
name: "mx",
products: [
.library(name: "Mx", targets: ["Mx"]),
],
targets: [mxTarget],
cxxLanguageStandard: .cxx20
)
175 changes: 175 additions & 0 deletions .github/workflows/_release-target-swift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# The Swift release target (docs/ai/design/release-process.md §8-9): builds the
# Mx XCFramework from commit A and emits the two artifacts the release system
# consumes -- the publishable archive and the metadata the prep job commits as B.
# Called only from release-build.yml (workflow_call); never triggered directly.
name: _release-target-swift

on:
workflow_call:
inputs:
sha:
description: Full 40-hex SHA of the commit to build (commit A).
required: true
type: string
version:
description: The MAJOR.MINOR.PATCH being released (no leading v).
required: true
type: string

jobs:
xcframework:
runs-on: macos-latest
permissions:
contents: read
env:
ARCHIVE: Mx.xcframework.zip
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.sha }}

- name: Toolchain versions
run: |
sw_vers
xcodebuild -version
cmake --version | head -n 1
swift --version

# Three static-library slices via CMake's built-in Apple cross-compilation
# support (CMAKE_SYSTEM_NAME=iOS plus per-slice sysroot/archs); CMake drives
# clang directly -- no .xcodeproj, no third-party toolchain file. Each slice
# builds only the `mx` target and its dependencies (mx_core, pugixml), then
# libtool merges the three .a files so each XCFramework slice carries a
# single library. Deployment floors: macOS 11.0 (first arm64 macOS),
# iOS 13.0 -- raise deliberately, never by accident.
- name: Build slices (macOS universal, iOS device, iOS simulator)
run: |
set -euxo pipefail
build_slice() {
slice="$1"; shift
cmake -S . -B "build/release-$slice" -DCMAKE_BUILD_TYPE=Release "$@"
cmake --build "build/release-$slice" --target mx --parallel 4
libtool -static -o "build/release-$slice/libMxAll.a" \
"build/release-$slice/libmx.a" \
"build/release-$slice/libmx_core.a" \
"build/release-$slice/libpugixml.a"
}
build_slice macos \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0
build_slice ios \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=iphoneos \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY
build_slice iossim \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_SYSROOT=iphonesimulator \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY

# xcodebuild -create-xcframework is the one Apple-tool call: nothing else
# writes the bundle format correctly. Headers are the public mx::api
# surface (src/include), shipped per slice; no module map -- consumers are
# C++/ObjC++ and include mx/api/... via the header search path.
- name: Assemble Mx.xcframework
run: |
set -euxo pipefail
mkdir -p stage/headers out/dist
cp -R src/include/. stage/headers/
xcodebuild -create-xcframework \
-library build/release-macos/libMxAll.a -headers stage/headers \
-library build/release-ios/libMxAll.a -headers stage/headers \
-library build/release-iossim/libMxAll.a -headers stage/headers \
-output out/Mx.xcframework
# ditto --keepParent puts the .xcframework bundle at the zip root, the
# layout SwiftPM's binaryTarget expects.
ditto -c -k --keepParent out/Mx.xcframework "out/dist/$ARCHIVE"

- name: Checksum, release Package.swift, contract fragment
env:
VERSION: ${{ inputs.version }}
SHA: ${{ inputs.sha }}
run: |
set -euxo pipefail
# For a zip, swift package compute-checksum IS the file's SHA-256;
# assert that equivalence here so release-publish can re-verify with
# plain sha256sum (design §5, check 6).
CHECKSUM=$(swift package compute-checksum "out/dist/$ARCHIVE")
SHA256=$(shasum -a 256 "out/dist/$ARCHIVE" | cut -d' ' -f1)
if [ "$CHECKSUM" != "$SHA256" ]; then
echo "::error::swift package compute-checksum ($CHECKSUM) != sha256 ($SHA256)"
exit 1
fi
printf '%s %s\n' "$CHECKSUM" "$ARCHIVE" > "out/dist/$ARCHIVE.sha256"

# The release-mode Package.swift: the template at A, with the asset URL
# (a pure function of the tag name, which is why version is a build
# input) and the computed checksum interpolated.
URL="https://github.com/$GITHUB_REPOSITORY/releases/download/v$VERSION/$ARCHIVE"
TEMPLATE=.github/release/Package.swift.template
for placeholder in __MX_RELEASE_URL__ __MX_RELEASE_CHECKSUM__ __MX_RELEASE_VERSION__; do
if ! grep -q "$placeholder" "$TEMPLATE"; then
echo "::error::$TEMPLATE lost placeholder $placeholder"
exit 1
fi
done
mkdir -p out/metadata/tree
sed -e "s|__MX_RELEASE_URL__|$URL|g" \
-e "s|__MX_RELEASE_CHECKSUM__|$CHECKSUM|g" \
-e "s|__MX_RELEASE_VERSION__|$VERSION|g" \
"$TEMPLATE" > out/metadata/tree/Package.swift
if grep -q '__MX_RELEASE' out/metadata/tree/Package.swift; then
echo '::error::placeholders survived interpolation'
exit 1
fi

# Evaluate the generated manifest exactly as a consumer's SwiftPM will
# (dump-package loads the manifest; it fetches and builds nothing).
mkdir -p "$RUNNER_TEMP/manifest-check"
cp out/metadata/tree/Package.swift "$RUNNER_TEMP/manifest-check/"
(cd "$RUNNER_TEMP/manifest-check" && swift package dump-package > /dev/null)

# The target's contract fragment (design §9): identity, artifact,
# checksum, platforms, and the paths this target may occupy in B. A
# copy ships in both artifacts.
jq -n \
--arg version "$VERSION" \
--arg base_sha "$SHA" \
--arg artifact "mx-release-swift-$SHA" \
--arg archive "$ARCHIVE" \
--arg checksum "$CHECKSUM" \
'{target: "swift",
version: $version,
base_sha: $base_sha,
artifact: $artifact,
archive: $archive,
checksum: $checksum,
platforms: ["macos-arm64", "macos-x86_64", "ios-arm64",
"ios-simulator-arm64", "ios-simulator-x86_64"],
allowed_paths: ["Package.swift"]}' \
> out/metadata/fragment.json
cp out/metadata/fragment.json out/dist/fragment.json

# The publishable artifact (design §6): archive + .sha256 + fragment.
# retention-days is the documented 90-day contract: the tag must be pushed
# while this artifact still exists.
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: mx-release-swift-${{ inputs.sha }}
path: out/dist/
retention-days: 90
if-no-files-found: error

# The prep-job channel (design §9, outputs b and c): the exact tree files
# for commit B plus the contract fragment.
- name: Upload metadata artifact
uses: actions/upload-artifact@v4
with:
name: mx-release-swift-metadata-${{ inputs.sha }}
path: out/metadata/
retention-days: 90
if-no-files-found: error
3 changes: 2 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,8 @@ jobs:

# Proves mx compiles as a Swift package under AppleClang/SPM on every PR --
# the path komp takes when it depends on a sibling mx via `.package(path:)`.
# Runs no test suites; the binary xcframework release arm is follow-up work.
# Runs no test suites; the binary xcframework ships from release-build.yml
# (docs/ai/design/release-process.md), not from CI.
#
# SwiftPM has no compiler-launcher hook, so incremental builds come from caching
# its .build tree: llbuild tracks inputs by content signature, so a restored
Expand Down
Loading
Loading