|
| 1 | +// Builds the SwiftPM dependency graph that the vendored ZcashLightClientKit |
| 2 | +// source links against — grpc-swift, SwiftNIO, SwiftProtobuf, SQLite.swift and |
| 3 | +// their C shims — into ONE static library per platform, plus the Swift |
| 4 | +// `.swiftmodule`s and C `module.modulemap`s the in-pod SDK source needs to |
| 5 | +// `import` them. |
| 6 | +// |
| 7 | +// Why this exists: as of the modern SDK, grpc-swift (1.24+) ships SwiftPM-only |
| 8 | +// with no podspec, so it can no longer be a CocoaPods `dependency`. Instead of |
| 9 | +// forcing the whole host app onto dynamic frameworks (the only way to consume |
| 10 | +// the SDK via `spm_dependency`), we pre-build just these leaf dependencies into |
| 11 | +// a static binary. The host app stays on static frameworks; the SDK *source* |
| 12 | +// keeps compiling in-pod exactly as before (see copySwift in updateSources.ts). |
| 13 | +// |
| 14 | +// Output (all under ios/vendored/, gitignored, shipped in the npm tarball): |
| 15 | +// libZcashDeps-ios-arm64.a - merged static lib, device |
| 16 | +// libZcashDeps-ios-arm64-simulator.a - merged static lib, simulator |
| 17 | +// modules/<Module>.swiftmodule - Swift dep modules (device + sim arches) |
| 18 | +// cmodules/<Module>/ - C dep modules (headers + module.modulemap) |
| 19 | +// |
| 20 | +// Per-platform .a (rather than one xcframework) so the podspec can -force_load |
| 21 | +// the right slice via an `[sdk=...]` condition — force_load preserves the Swift |
| 22 | +// type metadata / protocol conformances the SDK source pulls from these deps. |
| 23 | + |
| 24 | +import { execFileSync } from 'child_process' |
| 25 | +import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'fs' |
| 26 | +import { join } from 'path' |
| 27 | + |
| 28 | +// This repo's @types/node predates fs.cpSync/rmSync, and the sibling scripts |
| 29 | +// already shell out for file ops, so do the same here. |
| 30 | +function rm(path: string): void { |
| 31 | + execFileSync('rm', ['-rf', path]) |
| 32 | +} |
| 33 | +function cp(src: string, dest: string): void { |
| 34 | + execFileSync('cp', ['-R', src, dest]) |
| 35 | +} |
| 36 | + |
| 37 | +const root = join(__dirname, '..') |
| 38 | +const tmp = join(root, 'tmp') |
| 39 | +const sdkClone = join(tmp, 'ZCashLightClientKit') |
| 40 | +const wrapper = join(tmp, 'deps-wrapper') |
| 41 | +const vendored = join(root, 'ios/vendored') |
| 42 | + |
| 43 | +// Targets that must NOT go into the deps binary: |
| 44 | +// - ZcashLightClientKit: compiled in-pod from source, not vendored as a binary |
| 45 | +// - ZcashDepsWrapper: our throwaway entry-point target |
| 46 | +// - CSQLite/SQLite C shim: the host app already provides sqlite3 (Edge vendors |
| 47 | +// its own), so bundling it here double-defines every sqlite3_* symbol |
| 48 | +const EXCLUDED_TARGETS = [ |
| 49 | + 'ZcashLightClientKit', |
| 50 | + 'ZcashDepsWrapper', |
| 51 | + 'CSQLite', |
| 52 | + 'SQLite3' // some SQLite.swift versions name the C shim differently |
| 53 | +] |
| 54 | + |
| 55 | +const PLATFORMS = [ |
| 56 | + { destination: 'generic/platform=iOS', dir: 'ios-arm64', arch: 'arm64' }, |
| 57 | + { |
| 58 | + destination: 'generic/platform=iOS Simulator', |
| 59 | + dir: 'ios-arm64-simulator', |
| 60 | + arch: 'arm64' |
| 61 | + } |
| 62 | +] |
| 63 | + |
| 64 | +export function buildVendoredDeps(): void { |
| 65 | + console.log('Building vendored SwiftPM dependency binary...') |
| 66 | + rm(vendored) |
| 67 | + mkdirSync(join(vendored, 'modules'), { recursive: true }) |
| 68 | + mkdirSync(join(vendored, 'cmodules'), { recursive: true }) |
| 69 | + |
| 70 | + writeWrapperPackage() |
| 71 | + |
| 72 | + for (const platform of PLATFORMS) { |
| 73 | + console.log(` Compiling deps for ${platform.dir}...`) |
| 74 | + const dd = join(tmp, `deps-dd-${platform.dir}`) |
| 75 | + loud(wrapper, [ |
| 76 | + 'xcodebuild', |
| 77 | + '-scheme', |
| 78 | + 'ZcashDepsWrapper', |
| 79 | + '-destination', |
| 80 | + platform.destination, |
| 81 | + '-derivedDataPath', |
| 82 | + dd, |
| 83 | + 'BUILD_LIBRARY_FOR_DISTRIBUTION=NO', |
| 84 | + 'ONLY_ACTIVE_ARCH=YES', |
| 85 | + 'SKIP_INSTALL=NO', |
| 86 | + 'build' |
| 87 | + ]) |
| 88 | + |
| 89 | + mergeDeps(dd, platform.arch, platform.dir) |
| 90 | + harvestModules(dd, platform.arch) |
| 91 | + } |
| 92 | + console.log('Vendored deps built.') |
| 93 | +} |
| 94 | + |
| 95 | +// A throwaway SwiftPM package that depends on the SDK so SwiftPM resolves the |
| 96 | +// SDK's EXACT dependency versions; we then harvest those compiled deps. |
| 97 | +function writeWrapperPackage(): void { |
| 98 | + rm(wrapper) |
| 99 | + mkdirSync(join(wrapper, 'Sources/ZcashDepsWrapper'), { recursive: true }) |
| 100 | + writeFileSync( |
| 101 | + join(wrapper, 'Package.swift'), |
| 102 | + `// swift-tools-version:5.9 |
| 103 | +import PackageDescription |
| 104 | +let package = Package( |
| 105 | + name: "ZcashDepsWrapper", |
| 106 | + platforms: [.iOS(.v16)], |
| 107 | + products: [.library(name: "ZcashDepsWrapper", type: .static, targets: ["ZcashDepsWrapper"])], |
| 108 | + dependencies: [.package(path: ${JSON.stringify(sdkClone)})], |
| 109 | + targets: [.target(name: "ZcashDepsWrapper", dependencies: [ |
| 110 | + .product(name: "ZcashLightClientKit", package: "ZCashLightClientKit") |
| 111 | + ])] |
| 112 | +) |
| 113 | +` |
| 114 | + ) |
| 115 | + writeFileSync( |
| 116 | + join(wrapper, 'Sources/ZcashDepsWrapper/Empty.swift'), |
| 117 | + '@_exported import ZcashLightClientKit\n' |
| 118 | + ) |
| 119 | +} |
| 120 | + |
| 121 | +// Merge every dependency target's compiled objects into one static lib, keeping |
| 122 | +// each target's objects grouped so cross-target basename collisions don't drop |
| 123 | +// symbols. Excludes the SDK, the wrapper, the sqlite C shim, and tests. |
| 124 | +function mergeDeps(dd: string, arch: string, dir: string): void { |
| 125 | + const objectsRoot = join(dd, 'Build/Intermediates.noindex') |
| 126 | + const objects = findObjects(objectsRoot, arch).filter(path => { |
| 127 | + if (/IntegrationTests|Benchmarks|Tests\.build|Example/.test(path)) { |
| 128 | + return false |
| 129 | + } |
| 130 | + return !EXCLUDED_TARGETS.some(target => path.includes(`/${target}.build/`)) |
| 131 | + }) |
| 132 | + if (objects.length === 0) { |
| 133 | + throw new Error(`No dependency objects found under ${objectsRoot}`) |
| 134 | + } |
| 135 | + const listFile = join(tmp, `deps-objects-${dir}.txt`) |
| 136 | + writeFileSync(listFile, objects.join('\n')) |
| 137 | + const out = join(vendored, `libZcashDeps-${dir}.a`) |
| 138 | + rm(out) |
| 139 | + loud(tmp, ['libtool', '-static', '-o', out, '-filelist', listFile]) |
| 140 | +} |
| 141 | + |
| 142 | +function findObjects(base: string, arch: string): string[] { |
| 143 | + const out: string[] = [] |
| 144 | + const walk = (dir: string): void => { |
| 145 | + for (const entry of readdirSync(dir, { withFileTypes: true })) { |
| 146 | + const full = join(dir, entry.name) |
| 147 | + if (entry.isDirectory()) walk(full) |
| 148 | + else if ( |
| 149 | + entry.name.endsWith('.o') && |
| 150 | + dir.endsWith(`Objects-normal/${arch}`) |
| 151 | + ) { |
| 152 | + out.push(full) |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + walk(base) |
| 157 | + return out |
| 158 | +} |
| 159 | + |
| 160 | +// Copy the dep Swift `.swiftmodule`s (merging device + simulator arch slices) |
| 161 | +// and the C modules (headers + module.modulemap) the SDK source imports. |
| 162 | +function harvestModules(dd: string, arch: string): void { |
| 163 | + const products = findProductsDir(dd) |
| 164 | + // Swift modules: copy each <Module>.swiftmodule, unioning arch slices across |
| 165 | + // the per-platform builds (each build contributes its own arch's slice). |
| 166 | + for (const entry of readdirSync(products)) { |
| 167 | + if (!entry.endsWith('.swiftmodule')) continue |
| 168 | + const name = entry.replace('.swiftmodule', '') |
| 169 | + if (EXCLUDED_TARGETS.includes(name)) continue |
| 170 | + const src = join(products, entry) |
| 171 | + const dest = join(vendored, 'modules', entry) |
| 172 | + if (statSync(src).isDirectory()) { |
| 173 | + // Union the per-platform arch slices into one .swiftmodule bundle. |
| 174 | + mkdirSync(dest, { recursive: true }) |
| 175 | + cp(`${src}/.`, dest) |
| 176 | + } else if (!existsSync(dest)) { |
| 177 | + cp(src, dest) |
| 178 | + } |
| 179 | + } |
| 180 | + // C modules: each compiled C target maps to a checkout include/ dir with a |
| 181 | + // module.modulemap (synthesize a simple umbrella one if SwiftPM generated it). |
| 182 | + const checkouts = join(dd, 'SourcePackages/checkouts') |
| 183 | + if (!existsSync(checkouts)) return |
| 184 | + for (const obj of readdirSync(products)) { |
| 185 | + if (!obj.endsWith('.o')) continue |
| 186 | + const mod = obj.replace('.o', '') |
| 187 | + if (existsSync(join(vendored, 'cmodules', mod))) continue |
| 188 | + const inc = findInclude(checkouts, mod) |
| 189 | + if (inc == null) continue |
| 190 | + const dest = join(vendored, 'cmodules', mod) |
| 191 | + cp(inc, dest) |
| 192 | + if (!existsSync(join(dest, 'module.modulemap'))) { |
| 193 | + writeFileSync( |
| 194 | + join(dest, 'module.modulemap'), |
| 195 | + `module ${mod} {\n umbrella "."\n export *\n}\n` |
| 196 | + ) |
| 197 | + } |
| 198 | + } |
| 199 | +} |
| 200 | + |
| 201 | +function findProductsDir(dd: string): string { |
| 202 | + const base = join(dd, 'Build/Products') |
| 203 | + const entry = readdirSync(base).find(name => name.startsWith('Debug-')) |
| 204 | + if (entry == null) throw new Error(`No Products dir under ${base}`) |
| 205 | + return join(base, entry) |
| 206 | +} |
| 207 | + |
| 208 | +function findInclude(checkouts: string, mod: string): string | undefined { |
| 209 | + for (const pkg of readdirSync(checkouts)) { |
| 210 | + const inc = join(checkouts, pkg, 'Sources', mod, 'include') |
| 211 | + if (existsSync(inc) && readdirSync(inc).some(f => f.endsWith('.h'))) { |
| 212 | + return inc |
| 213 | + } |
| 214 | + } |
| 215 | + return undefined |
| 216 | +} |
| 217 | + |
| 218 | +function loud(cwd: string, argv: string[]): void { |
| 219 | + execFileSync(argv[0], argv.slice(1), { |
| 220 | + cwd, |
| 221 | + stdio: 'inherit', |
| 222 | + encoding: 'utf8' |
| 223 | + }) |
| 224 | +} |
0 commit comments