|
| 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 |
0 commit comments