Skip to content

Commit 38ac2ba

Browse files
committed
rules-swift: one package's Swift sources compile into its images, with a generated header and the Swift runtime on the link (#647 E2)
1 parent ad7a3dd commit 38ac2ba

13 files changed

Lines changed: 511 additions & 3 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,18 @@ jobs:
20462046
working-directory: tests/metal-consumer
20472047
run: MCPP="$MCPP" ./check-metal.sh
20482048

2049+
# rules-swift (mcpp#647 E2): a C++ program calling a Swift function, which
2050+
# calls back into C through the bridging header, printing a line from each
2051+
# side; the generated header declares the exported function; the fixture
2052+
# links for the iOS simulator when the runner has that SDK. On a runner
2053+
# without swiftc the script asserts the rule's refusal and annotates the
2054+
# run instead.
2055+
- name: rules-swift compiles a package's Swift sources, or records that this runner has no swiftc
2056+
if: ${{ !cancelled() && runner.os == 'macOS' }}
2057+
timeout-minutes: 20
2058+
working-directory: tests/swift-consumer
2059+
run: MCPP="$MCPP" ./check-swift.sh
2060+
20492061
# THE iOS ROW, FOR REAL (#622 B1's other half). `tests/ios-app-consumer`
20502062
# is the same fixture the Linux `consumers` job checks at the plan
20512063
# level (see that fixture's own header); here it is built, packed and

mcpp.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "plugins"
33
namespace = "mcpp"
4-
version = "0.11.1"
4+
version = "0.12.0"
55
description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature"
66
license = "Apache-2.0"
77
authors = ["mcpp-community"]
@@ -94,6 +94,16 @@ implies = ["surface"]
9494
rule_module = "mcpp.rules.metal"
9595
device_extensions = [".metal"]
9696

97+
# `.swift` sources become one object per package through `xcrun swiftc`, with a
98+
# generated header for the package's C and C++ sources and the Swift runtime's
99+
# search directories on the link. No payload is declared, for the reason
100+
# `rules-metal` gives: the compiler ships with Xcode and the Command Line Tools.
101+
[features.rules-swift]
102+
sources = ["rules/swift.cppm"]
103+
implies = ["surface"]
104+
rule_module = "mcpp.rules.swift"
105+
device_extensions = [".swift"]
106+
97107
[features.rules-hip]
98108
sources = ["rules/hip.cppm"]
99109
implies = ["surface"]

rules/swift.cppm

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
// mcpp.rules.swift -- how one package's Swift sources become one object file
2+
// the package's images link, stated once.
3+
//
4+
// THE DIVISION OF LABOUR IS `mcpp.rules.metal`'S. The ENGINE owns the graph:
5+
// `.swift` is a device extension this feature declares, so a `.swift` file the
6+
// project names in `[build] sources` reaches this rule through
7+
// `mcpp::device_sources()` instead of being refused, and every command below is
8+
// an action with declared inputs and outputs. The RULE owns the spelling: which
9+
// SDK, which compiler, which flags, and which runtime the link needs
10+
// (mcpp-community/mcpp#647 E2).
11+
//
12+
// ONE MODULE, ONE OBJECT. Swift compiles a module, not a file: a declaration in
13+
// one source is visible in every other source of the same module without an
14+
// import. The package's sources are therefore compiled together, in one
15+
// whole-module invocation (`-wmo`), into one object named after the package,
16+
// through an action whose role is `object`, so the object joins the link of every
17+
// image the package produces.
18+
//
19+
// A HEADER FOR THE C AND C++ SIDE. `swiftc -emit-objc-header-path` writes the
20+
// declarations Swift exports to C and Objective-C (`@_cdecl` functions and
21+
// `@objc` classes) as a header. It is produced by a second action whose role is
22+
// `source`, so every compile edge of this package waits for it, and its directory
23+
// is added with `mcpp::include_dir`, so the package's C++ sources include it as
24+
// `#include "<module>-Swift.h"`. The header action type-checks and emits no
25+
// object, which keeps the object action free of an output the link must not
26+
// receive.
27+
//
28+
// C FROM SWIFT. `options::bridging_header` names a C header whose declarations
29+
// Swift sees without an import (`-import-objc-header`). It is an input of both
30+
// actions, so editing it recompiles the module.
31+
//
32+
// THE RUNTIME. A Swift object refers to the Swift runtime and records the
33+
// libraries it needs as linker options inside the object; the link finds them
34+
// through two search directories, the toolchain's `usr/lib/swift/<platform>` and
35+
// the SDK's `usr/lib/swift`, which reach the link through `mcpp::link_flag`. The
36+
// runtime itself is part of the operating system from macOS 10.14.4 and iOS 12.2,
37+
// so the program's run path names `/usr/lib/swift`.
38+
//
39+
// THE TOOLCHAIN IS LOCATED, NOT INSTALLED. `swiftc` ships with Xcode and with the
40+
// Command Line Tools, neither of which is redistributable, so no payload is
41+
// declared. Before planning anything the rule asks `xcrun --sdk <sdk>
42+
// --show-sdk-path` and `xcrun --sdk <sdk> --find swiftc`, and refuses naming the
43+
// command that answered nothing. The commands run through `xcrun --sdk <sdk>`, so
44+
// the compiler targets the SDK of the row.
45+
//
46+
// ROWS. macOS and the iOS rows (the simulator and a device). Every other row is
47+
// refused by name: Swift on Linux and Windows is a different toolchain and a
48+
// different runtime, neither of which this rule knows how to link.
49+
//
50+
// WHAT THIS RULE DOES NOT DO, stated so that its absence is not mistaken for a
51+
// defect:
52+
// - A Swift `import` of ANOTHER package's Swift module. That needs the other
53+
// package's `.swiftmodule` directory on this package's search path, and a
54+
// package's compile-interface directories are private to it by design
55+
// (`mcpp::include_dir` colours the declaring package's own translation units
56+
// only). It waits for an engine channel that publishes such a directory to
57+
// dependents.
58+
// - A C++ translation unit of another package including this package's
59+
// generated header, for the same reason.
60+
// - Swift Package Manager dependencies. A SwiftPM package is not a package in
61+
// the mcpp graph.
62+
63+
module;
64+
#include <cstdio>
65+
66+
export module mcpp.rules.swift;
67+
68+
import std;
69+
import mcpp;
70+
import mcpp.plugins;
71+
72+
// Nothing here uses `std::println`, for the reason `rules/metal.cppm` gives.
73+
74+
export namespace mcpp::rules::swift {
75+
76+
struct options {
77+
// The Swift module name. Empty means the package name, with every character
78+
// that is not a letter, a digit or `_` replaced by `_` (a module name is an
79+
// identifier; `swift-consumer` is not one).
80+
std::string module_name;
81+
// A C header Swift sees without an import (`-import-objc-header`). Relative
82+
// paths resolve against the package root. Empty passes none.
83+
std::string bridging_header;
84+
// `-D` for Swift's conditional compilation (`#if NAME`).
85+
std::vector<std::string> defines;
86+
// Further arguments for both `swiftc` invocations, after the rule's own.
87+
std::vector<std::string> flags;
88+
// `false` compiles without the header action and adds no include directory,
89+
// for a package whose C++ side declares the Swift functions itself.
90+
bool emit_header = true;
91+
// The SDK `xcrun` compiles against. Empty derives it from the target row:
92+
// `macosx`, `iphoneos` or `iphonesimulator`.
93+
std::string sdk;
94+
// The deployment target in the `-target` triple. Empty takes the row's
95+
// minimum platform version from the engine, and a default when the engine
96+
// states none (14.0 on macOS, 17.0 on iOS).
97+
std::string min_os_version;
98+
std::string out_dir = std::string(mcpp::out_dir());
99+
};
100+
101+
// ─── What the engine said ──────────────────────────────────────────────────
102+
103+
// The SDK a target row compiles against, or empty on a row Swift does not
104+
// serve.
105+
inline std::string sdk_for(const options& opt) {
106+
if (!opt.sdk.empty()) return opt.sdk;
107+
const std::string os = mcpp::target_os();
108+
if (os == "macos") return "macosx";
109+
if (os == "ios")
110+
return std::string(mcpp::target_env()) == "sim" ? "iphonesimulator" : "iphoneos";
111+
return {};
112+
}
113+
114+
// The `-target` triple swiftc takes: Apple's architecture spelling, the
115+
// platform, the deployment version, and `-simulator` on the simulator row.
116+
inline std::string swift_target_for(const options& opt, const std::string& sdk) {
117+
std::string arch = mcpp::target_arch();
118+
if (arch == "aarch64" || arch.empty()) arch = "arm64";
119+
std::string version = opt.min_os_version;
120+
if (version.empty()) version = mcpp::min_platform_version();
121+
if (sdk == "macosx") {
122+
if (version.empty()) version = "14.0";
123+
return arch + "-apple-macosx" + version;
124+
}
125+
if (version.empty()) version = "17.0";
126+
return arch + "-apple-ios" + version + (sdk == "iphonesimulator" ? "-simulator" : "");
127+
}
128+
129+
// The platform directory name under a toolchain's `usr/lib/swift/`.
130+
inline std::string platform_dir_for(const std::string& sdk) {
131+
return sdk; // `macosx`, `iphoneos`, `iphonesimulator`: the same spelling
132+
}
133+
134+
inline std::string module_name_for(const options& opt) {
135+
std::string name = opt.module_name;
136+
if (name.empty()) {
137+
const char* pkg = mcpp::package_name();
138+
name = (pkg && *pkg) ? pkg : "swift_module";
139+
}
140+
// Indices rather than iterators: a string iterator's operators are
141+
// `always_inline` in libstdc++, and GCC 16 does not carry their bodies
142+
// through this module's interface to the build program that inlines this
143+
// function ("inlining failed ... function body not available", measured).
144+
for (std::size_t i = 0; i < name.size(); ++i) {
145+
const unsigned char c = static_cast<unsigned char>(name[i]);
146+
if (!std::isalnum(c) && name[i] != '_') name[i] = '_';
147+
}
148+
if (!name.empty() && std::isdigit(static_cast<unsigned char>(name[0])))
149+
name.insert(0, 1, '_');
150+
return name;
151+
}
152+
153+
// `mcpp::device_sources()` is the package's whole device set, one path per
154+
// line, and a rule takes the extensions it claims (see `rules/spirv.cppm`).
155+
inline std::vector<std::string> device_swift_sources() {
156+
std::vector<std::string> out;
157+
const std::string all = mcpp::device_sources();
158+
std::size_t i = 0;
159+
while (i <= all.size()) {
160+
auto nl = all.find('\n', i);
161+
std::string one = all.substr(i, nl == std::string::npos ? std::string::npos : nl - i);
162+
i = nl == std::string::npos ? all.size() + 1 : nl + 1;
163+
while (!one.empty() && (one.back() == ' ' || one.back() == '\r')) one.pop_back();
164+
if (one.size() > 6 && one.substr(one.size() - 6) == ".swift") out.push_back(one);
165+
}
166+
return out;
167+
}
168+
169+
// ─── The toolchain ─────────────────────────────────────────────────────────
170+
171+
// The first line a command prints, or empty. `popen` is POSIX; this module is
172+
// compiled on every host (`tests/all-rules-compile`), so Windows is spelled.
173+
inline std::string first_line_of(const std::string& cmd) {
174+
#if defined(_WIN32)
175+
FILE* p = ::_popen(cmd.c_str(), "r");
176+
#else
177+
FILE* p = ::popen(cmd.c_str(), "r");
178+
#endif
179+
if (!p) return {};
180+
char buf[1024];
181+
std::string line;
182+
if (std::fgets(buf, sizeof buf, p)) line = buf;
183+
#if defined(_WIN32)
184+
::_pclose(p);
185+
#else
186+
::pclose(p);
187+
#endif
188+
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) line.pop_back();
189+
return line;
190+
}
191+
192+
inline std::string sdk_path(const std::string& sdk) {
193+
#if defined(_WIN32)
194+
(void)sdk;
195+
return {};
196+
#else
197+
const std::string path = first_line_of(
198+
"/usr/bin/xcrun --sdk " + sdk + " --show-sdk-path 2>/dev/null");
199+
std::error_code ec;
200+
return !path.empty() && std::filesystem::is_directory(path, ec) ? path : std::string();
201+
#endif
202+
}
203+
204+
inline std::string find_swiftc(const std::string& sdk) {
205+
#if defined(_WIN32)
206+
(void)sdk;
207+
return {};
208+
#else
209+
const std::string path = first_line_of(
210+
"/usr/bin/xcrun --sdk " + sdk + " --find swiftc 2>/dev/null");
211+
std::error_code ec;
212+
return !path.empty() && std::filesystem::is_regular_file(path, ec) ? path : std::string();
213+
#endif
214+
}
215+
216+
// ─── The rule ──────────────────────────────────────────────────────────────
217+
218+
// Compiles `sources` (absolute, or relative to the package root) as one Swift
219+
// module. A build that names no `.swift` file has nothing to do, which is not a
220+
// mistake: a project names the Swift sources on the rows that compile them.
221+
inline bool compile(std::span<const std::string> sources, options opt = {}) {
222+
if (sources.empty()) return true;
223+
224+
const std::string sdk = sdk_for(opt);
225+
if (sdk.empty()) {
226+
std::cerr << std::format(
227+
"mcpp.rules.swift: {} Swift source(s) reached this rule on a '{}' "
228+
"target; this rule compiles Swift for macOS and iOS only. Condition "
229+
"the sources on the row, e.g. [target.'cfg(any(os = \"macos\", os = \"ios\"))'.build] "
230+
"sources = [\"swift/*.swift\"].",
231+
sources.size(), std::string(mcpp::target_os())) << '\n';
232+
mcpp::warning("mcpp.rules.swift: Swift sources on a row this rule does not serve");
233+
return false;
234+
}
235+
const std::string sdkRoot = sdk_path(sdk);
236+
if (sdkRoot.empty()) {
237+
std::cerr << std::format(
238+
"mcpp.rules.swift: the {0} SDK was not found: `xcrun --sdk {0} "
239+
"--show-sdk-path` answered nothing. The macOS SDK ships with Xcode "
240+
"and with the Command Line Tools, the iOS SDKs with Xcode alone.",
241+
sdk) << '\n';
242+
return false;
243+
}
244+
const std::string swiftc = find_swiftc(sdk);
245+
if (swiftc.empty()) {
246+
std::cerr << std::format(
247+
"mcpp.rules.swift: no Swift compiler was found for the {0} SDK: "
248+
"`xcrun --sdk {0} --find swiftc` answered nothing. It ships with "
249+
"Xcode and with the Command Line Tools.", sdk) << '\n';
250+
return false;
251+
}
252+
// `<toolchain>/usr/bin/swiftc`: the toolchain root is three components up.
253+
const auto toolchainRoot = std::filesystem::path(swiftc).parent_path().parent_path().parent_path();
254+
255+
const std::string root = mcpp::manifest_dir();
256+
const auto resolve = [&](const std::string& p) {
257+
return std::filesystem::path(p).is_absolute() ? p : root + "/" + p;
258+
};
259+
std::vector<std::string> absSources;
260+
for (auto const& s : sources) absSources.push_back(resolve(s));
261+
std::string bridging;
262+
if (!opt.bridging_header.empty()) {
263+
bridging = resolve(opt.bridging_header);
264+
std::error_code ec;
265+
if (!std::filesystem::is_regular_file(bridging, ec)) {
266+
std::cerr << std::format(
267+
"mcpp.rules.swift: `options::bridging_header` ({}) was not found", bridging) << '\n';
268+
return false;
269+
}
270+
}
271+
272+
const std::string module = module_name_for(opt);
273+
const std::string target = swift_target_for(opt, sdk);
274+
const auto gen = std::filesystem::path(opt.out_dir) / "swift" / module;
275+
std::error_code ec;
276+
std::filesystem::create_directories(gen, ec);
277+
278+
// The arguments both invocations share.
279+
const auto common = [&](mcpp::action& a) {
280+
a.arg("/usr/bin/xcrun").arg("--sdk").arg(sdk.c_str()).arg("swiftc");
281+
a.arg("-target").arg(target.c_str());
282+
a.arg("-module-name").arg(module.c_str());
283+
a.arg("-parse-as-library");
284+
for (auto const& d : opt.defines) a.arg(("-D" + d).c_str());
285+
if (!bridging.empty()) {
286+
a.arg("-import-objc-header").arg(bridging.c_str());
287+
a.input(bridging.c_str());
288+
}
289+
for (auto const& f : opt.flags) a.arg(f.c_str());
290+
for (auto const& s : absSources) { a.arg(s.c_str()); a.input(s.c_str()); }
291+
};
292+
293+
if (opt.emit_header) {
294+
const std::string header = (gen / (module + "-Swift.h")).string();
295+
const std::string id = "swift-header:" + module;
296+
const std::string desc = "SWIFT HEADER " + module;
297+
mcpp::action h;
298+
h.id = id.c_str();
299+
h.role = "source"; // a header: ordered before compilation
300+
h.description = desc.c_str();
301+
common(h);
302+
h.arg("-typecheck").arg("-emit-objc-header-path").arg(header.c_str());
303+
h.output(header.c_str());
304+
h.submit();
305+
mcpp::include_dir(gen.string().c_str());
306+
}
307+
308+
const std::string object = (gen / (module + ".o")).string();
309+
{
310+
const std::string id = "swift-object:" + module;
311+
const std::string desc = "SWIFTC " + module;
312+
mcpp::action o;
313+
o.id = id.c_str();
314+
o.role = "object"; // joins the link of every image of the package
315+
o.description = desc.c_str();
316+
common(o);
317+
o.arg("-wmo").arg("-emit-object").arg("-o").arg(object.c_str());
318+
o.output(object.c_str());
319+
o.submit();
320+
}
321+
322+
const std::string platform = platform_dir_for(sdk);
323+
mcpp::link_flag(("-L" + (toolchainRoot / "usr" / "lib" / "swift" / platform).string()).c_str());
324+
mcpp::link_flag(("-L" + (std::filesystem::path(sdkRoot) / "usr" / "lib" / "swift").string()).c_str());
325+
mcpp::link_flag("-Wl,-rpath,/usr/lib/swift");
326+
327+
mcpp::fact("mcpp.plugins", std::string(mcpp::plugins::version).c_str());
328+
return true;
329+
}
330+
331+
// The `.swift` files the project named in `[build] sources`, as one module.
332+
inline bool compile(options opt = {}) {
333+
const auto list = device_swift_sources();
334+
return compile(std::span<const std::string>(list), std::move(opt));
335+
}
336+
337+
} // namespace mcpp::rules::swift

src/plugins.cppm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export namespace mcpp::plugins {
4949
//
5050
// One package, one version: the number lives in mcpp.toml, and the CI step
5151
// `the collection states its own version` compares the two.
52-
inline constexpr std::string_view version = "0.11.1";
52+
inline constexpr std::string_view version = "0.12.0";
5353

5454
} // namespace mcpp::plugins
5555

0 commit comments

Comments
 (0)