Skip to content

Package system-library targets as header-only frameworks#279

Merged
Ckitakishi merged 11 commits into
giginet:mainfrom
Ckitakishi:package-system-library-targets
Jul 17, 2026
Merged

Package system-library targets as header-only frameworks#279
Ckitakishi merged 11 commits into
giginet:mainfrom
Ckitakishi:package-system-library-targets

Conversation

@Ckitakishi

@Ckitakishi Ckitakishi commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

SwiftPM's .systemLibrary targets carry a module map and optional headers and have nothing to compile:

Sources/SysShim/
├── module.modulemap
└── shim.h              // #include <core/core.h>

SwiftPM exposes such modules to importers through injected search paths, which only resolve while the package sources are on disk. Prebuilt-framework consumers only get -F, so today the producer dies with fatalError("Unexpected target type"), and even if it skipped the target, import SysShim in consumers would fail with missing required module 'SysShim'.

This PR packages system-library targets as header-only frameworks, so the module resolves through the framework search paths consumers already have:

SysShim.xcframework/ios-arm64/SysShim.framework/
├── SysShim                    // stub binary; XCFramework creation requires one
├── Headers/shim.h             // #include <CoreLib/core/core.h>, rewritten by #278
├── Modules/module.modulemap   // framework module SysShim { ... }
└── Info.plist

Behavior

  • pkgConfig declarations log a warning instead of failing: shim-style modules still resolve through the SDK and autolinking.
  • Linking dynamic frameworks no longer injects -framework for dependencies that produce no linkable framework: system modules resolve through the SDK, and non-producible targets build no product. Generated module maps' link framework sections follow the same rule.
  • Targets that cannot produce a framework (executables, macros, plugins, tests) are pruned from the build-product graph together with dependencies reachable only through them. Such graphs previously crashed with fatalError("Unexpected target type").
  • Resolved-packages caches written before system-library support are discarded on restore.
  • Discovery is through importer edges; a system-library target reachable only as a product member stays out of scope, unchanged from current behavior.

Testing

  • Unit tests cover the resolution shape, module map conversion below leading comments, cache staleness, the snapshot round-trip, and the generated Info.plist.
  • An end-to-end test compiles a consumer importing the system-library module against the produced frameworks using only -F: the missing required module failure this PR removes.
  • Unit tests cover the graph pruning and the linkable-dependency walk; an end-to-end test builds a dynamic framework whose library depends on an executable target. Custom module map contents are covered for system-library targets.

Ckitakishi and others added 3 commits July 14, 2026 11:08
…ule maps

The ^module pattern matched only at string start, so a custom module map
opening with comment lines was shipped with its plain module declaration
unconverted. Inside a framework bundle, a non-framework module resolves
header paths relative to Modules/ instead of Headers/, so such modules
were unresolvable for consumers. Anchoring per line converts declarations
on any line start; nested submodules are indented and stay untouched.

The conversion also moves from NSRegularExpression to a Swift Regex
literal, resolving the TODO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
System-library targets fell into the library branch of ModuleTypeResolver
and came out as a clang module with a nonexistent include directory and no
headers. Resolve them as .system instead: the module directory is the
include root (SwiftPM requires module.modulemap at the directory root and
resolves its header paths relative to it), with the headers enumerated
beneath it. Module map existence is validated at packaging time, where a
missing file can be reported with a proper error.

isFrameworkProducible records which target kinds an XCFramework can be
produced for; the producer side adopts it separately.

Resolved-packages caches written before this modeling carry the wrong
module type for system modules, and packaging from them would produce
empty frameworks, so such caches are discarded and re-resolved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
System-library targets reach the producer through dependency edges, where
they crashed on a fatalError, and skipping them leaves consumers with a
missing required module for every import of such a target. They have
nothing to compile: SwiftPM exposes them to importers through header
search paths, which consumers of prebuilt XCFrameworks do not get.

The build-product graph now keeps every producible kind and drops the
rest gracefully. SystemLibraryPackager assembles a header-only framework
per SDK (headers, the shipped module map converted to a framework module,
a generated Info.plist, and a per-architecture stub binary XCFramework
creation requires to identify the slice platform) and combines them with
the existing createXCFramework path, so import resolves through the
framework search consumers already have. Framework caching works
unchanged since cache keys carry no source hashes.

Includes are rewritten against the union of all importers' dependency
closures: a system-library target declares no dependencies of its own,
and its headers compile in each importer's context. A target declaring
pkgConfig is packaged with a warning; its headers can still resolve
against the SDK and link directives survive in the module map, while
pkg-config flags cannot be carried into a prebuilt framework.

Dynamic builds also stop injecting -framework link flags for system
modules: they build no product, so the linker had nothing to find.

An end-to-end test builds a Swift library importing a system-library
module whose header includes another module's header, and compiles a
consumer against the produced XCFrameworks using only -F.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ckitakishi
Ckitakishi force-pushed the package-system-library-targets branch from a44bd90 to 581ee9a Compare July 14, 2026 07:55
@Ckitakishi
Ckitakishi marked this pull request as ready for review July 14, 2026 08:22
@Ckitakishi
Ckitakishi requested a review from S-Shimotori July 14, 2026 08:23
Comment on lines +160 to +161
/// The dependency closure of every module that transitively depends on this one: exactly
/// the compile contexts SwiftPM would have compiled these headers in.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// The dependency closure of every module that transitively depends on this one: exactly
/// the compile contexts SwiftPM would have compiled these headers in.
// The dependency closure of every module that transitively depends on this one: exactly
// the compile contexts SwiftPM would have compiled these headers in.

This comment is not docc comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 960a9df.

Comment on lines +121 to +122
/// Caches written before system-library support carry a wrong module type for system
/// modules; packaging from them would produce empty frameworks.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Suggested change
/// Caches written before system-library support carry a wrong module type for system
/// modules; packaging from them would produce empty frameworks.
// Caches written before system-library support carry a wrong module type for system
// modules; packaging from them would produce empty frameworks.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 960a9df, reworded to lead with what it detects.

id: \.target.name,
childIDs: { $0.target.dependencies.flatMap(\.moduleNames) }
)
return graph.filter { $0.target.underlying.type.isFrameworkProducible }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this line is not covered by test

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in bf2a171. Writing it surfaced a semantics issue: a137657 prunes the whole subtree instead of reconnecting, and 661df21 aligns link references with it.

Comment on lines +72 to +73
// Sorted because the enumeration order is unspecified: keeps the resolution
// result, and thus the serialized snapshot, deterministic.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a test to check this case?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in c1d3358: a nested header asserts recursive discovery in path-sorted order.

throw Error.unexpectedModuleType(targetName: target.name)
}
guard fileSystem.exists(moduleMapPath) else {
throw Error.moduleMapNotFound(targetName: target.name, expectedPath: moduleMapPath)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we don't have a test for this line

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered in 2c1530c, together with the unexpectedModuleType path.

let objectPath = workingDirectory.appending(component: "stub_\(architecture).o")
try await executor.execute([
"/usr/bin/xcrun",
"--sdk", sdk.settingValue,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with mac catalyst, but I'm worried whether this command can handle mac catalyst or not

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch: maccatalyst is not an SDK name, so this failed under Catalyst. Fixed in 2c1530c: the stub compiles against the macOS SDK with a macabi target triple, covered by packagesMacCatalystSlice.

return replaced
// Line-anchored so declarations below leading comment lines convert too; nested
// submodules are indented and stay untouched.
let moduleDeclaration = /^module/.anchorsMatchLineEndings()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit) I prefer /^module\b/, \s or something

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c752938.

Ckitakishi and others added 8 commits July 15, 2026 16:00
maccatalyst names an xcodebuild destination, not an SDK: xcrun cannot
locate it. The macabi target triple marks the slice platform instead.
Tests cover the produced Catalyst slice and the packager error paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An executable reachable through a target dependency edge is dropped,
and its parent is reconnected to the surviving library dependency.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stale-cache note now leads with what it detects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The filter used to reconnect parents to a dropped node's children, so
dependencies serving only executables or macros were packaged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The linkable walk mirrors the build-product pruning; without it, dynamic
linking injected -framework flags for modules pruned from the products.
System-library modules are packaged but resolve through the SDK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same precedence as the compiled path: custom contents replace the
shipped module map wholesale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Ckitakishi
Ckitakishi merged commit a453f56 into giginet:main Jul 17, 2026
5 checks passed
@Ckitakishi
Ckitakishi deleted the package-system-library-targets branch July 17, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants