Skip to content

Commit a6dee74

Browse files
hansemannnm1gajanvennemanncb1kenobinarbs
authored
feat(ios): support swift package manager (SPM) module dependencies (#14327)
* chore: add 13.0.0 changelog * fix(android): fix transparent TextField backgroundColor (#14245) * fix(android): fix transparent TextField backgroundColor * fix null * fix null * fix null * null == transparent * fix: properly clean build folder (#14264) * fix: properly handle containment of tab group (#14261) * fix(ios): keep TableView search results on enter (#14265) * Revert "fix: properly handle containment of tab group (#14261)" (#14266) This reverts commit 5875cb0. * fix(ios): improve safe area detection and trigger relayout (#14267) * fix(ios): include top safe area inset in navigation window (#14268) * fix(ios): improve safe area layout lifecycle (#14269) * fix(ios): improve safe area layout lifecycle * chore: use BOOL instead of bool * Update iphone/TitaniumKit/TitaniumKit/Sources/API/TiViewProxy.m --------- Co-authored-by: Hans Knöchel <[email protected]> * chore: update changelog * chore: update ioslib to 5.1.0 to support Xcode 26 * chore: update changelog * [backport] Drop Node 16 and 18, remove engines.node (#14272) * remove node engines, let the CLI own it * Add back venderDependencies * fix(ios): correct safe area for standalone windows (#14273) * fix(ios): use proxy viewCount in ScrollableView to prevent empty display (#14275) * chore: remove interactive dismiss mode (#14274) * chore(ios): update xcode/ios/watchos compatibility versions * Revert "feat(ios): support iOS 26+ source views for non-iPad devices (#14258)" (#14276) This reverts commit ce3752b. * chore: update module versions (#14270) * chore: add 13.0.0.GA changelog * chore: update 13.0.0.GA changelog * chore(release): bump version * fix(ios): fix ButtonConfiguration API from throwing an error on device * fix: patch ActivityKit so it works with catalyst (#14279) (#14280) * fix: ensure eventStoreChanged notification is not over-registered (#14282) * feat(ios): support swift package manager (SPM) module dependencies * chore: update iOS workflow (#14304) * fix(ios/cli): fix linting error in module build * chore: move spm util to ES-based module --------- Co-authored-by: Michael Gangolf <[email protected]> Co-authored-by: Jan Vennemann <[email protected]> Co-authored-by: Chris Barber <[email protected]> Co-authored-by: narbs <[email protected]>
1 parent 089cabc commit a6dee74

File tree

6 files changed

+798
-2
lines changed

6 files changed

+798
-2
lines changed

iphone/cli/commands/_build.js

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { CopyResourcesTask } from '../../../cli/lib/tasks/copy-resources-task.js
2929
import { ProcessJsTask } from '../../../cli/lib/tasks/process-js-task.js';
3030
import { Color } from '../../../common/lib/color.js';
3131
import { ProcessCSSTask } from '../../../cli/lib/tasks/process-css-task.js';
32+
import { injectSPMPackage } from '../lib/ios/spm.js';
3233
import { exec, spawn } from 'node:child_process';
3334
import ti from 'node-titanium-sdk';
3435
import util from 'node:util';
@@ -45,6 +46,7 @@ const { version } = appc;
4546
const require = createRequire(import.meta.url);
4647
const platformsRegExp = new RegExp('^(' + ti.allPlatformNames.join('|') + ')$'); // eslint-disable-line security/detect-non-literal-regexp
4748
const pemCertRegExp = /(^-----BEGIN CERTIFICATE-----)|(-----END CERTIFICATE-----.*$)|\n/g;
49+
const SPM_LOG_PREFIX = '[SPM]';
4850

4951
class iOSBuilder extends Builder {
5052
constructor() {
@@ -111,6 +113,8 @@ class iOSBuilder extends Builder {
111113

112114
// object of all used Titanium symbols, used to determine preprocessor statements, e.g. USE_TI_UIWINDOW
113115
this.tiSymbols = {};
116+
this.moduleSpmDependencies = [];
117+
this.hostSpmPackages = [];
114118

115119
// when true, uses the new build system (Xcode 9+)
116120
this.useNewBuildSystem = true;
@@ -2407,6 +2411,7 @@ class iOSBuilder extends Builder {
24072411
}, this);
24082412

24092413
this.modulesNativeHash = this.hash(nativeHashes.length ? nativeHashes.sort().join(',') : '');
2414+
this.collectModuleSpmDependencies();
24102415

24112416
next();
24122417
}.bind(this));
@@ -2427,6 +2432,220 @@ class iOSBuilder extends Builder {
24272432
}).join('');
24282433
}
24292434

2435+
collectModuleSpmDependencies() {
2436+
this.moduleSpmDependencies = [];
2437+
this.hostSpmPackages = [];
2438+
2439+
const packagesByKey = new Map();
2440+
const repoTracker = new Map();
2441+
2442+
(this.modules || []).forEach(module => {
2443+
const metadataPath = path.join(module.modulePath, 'metadata.json');
2444+
let metadata = null;
2445+
2446+
if (fs.existsSync(metadataPath)) {
2447+
try {
2448+
metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
2449+
} catch (err) {
2450+
this.logger.warn(`${SPM_LOG_PREFIX} Unable to parse ${path.relative(this.projectDir, metadataPath)} for module ${module.id}: ${err.message}`);
2451+
return;
2452+
}
2453+
}
2454+
2455+
const spmInfo = metadata && metadata.spm;
2456+
if (!spmInfo || !Array.isArray(spmInfo.dependencies) || !spmInfo.dependencies.length) {
2457+
if (this.moduleHasLegacySpmHook(module)) {
2458+
this.logger.warn(`${SPM_LOG_PREFIX} Module ${module.id} ships the legacy ti.spm hook. Add an spm.json file and rebuild the module to avoid duplicate Swift packages.`);
2459+
} else {
2460+
this.logger.debug(`${SPM_LOG_PREFIX} Module ${module.id} has no Swift package metadata`);
2461+
}
2462+
return;
2463+
}
2464+
2465+
const normalizedDeps = spmInfo.dependencies
2466+
.map(dep => this.normalizeModuleSpmDependency(module, dep))
2467+
.filter(Boolean);
2468+
2469+
if (!normalizedDeps.length) {
2470+
this.logger.debug(`${SPM_LOG_PREFIX} Module ${module.id} declared Swift packages, but none were valid after normalization`);
2471+
return;
2472+
}
2473+
2474+
this.logger.debug(`${SPM_LOG_PREFIX} Module ${module.id} contributes ${normalizedDeps.length} Swift package(s)`);
2475+
2476+
this.moduleSpmDependencies.push({ module, dependencies: normalizedDeps });
2477+
2478+
normalizedDeps.forEach(dep => {
2479+
const hostProducts = dep.products.filter(product => product.linkage === 'host');
2480+
if (!hostProducts.length) {
2481+
this.logger.debug(`${SPM_LOG_PREFIX} Module ${module.id} embeds Swift package ${dep.repositoryURL} directly; nothing to add to the app`);
2482+
return;
2483+
}
2484+
2485+
const packageKey = [
2486+
dep.repositoryURL,
2487+
dep.requirementKind,
2488+
dep.requirementMinimumVersion
2489+
].join('#');
2490+
2491+
let pkg = packagesByKey.get(packageKey);
2492+
if (!pkg) {
2493+
pkg = {
2494+
remotePackageReference: dep.remotePackageReference,
2495+
repositoryURL: dep.repositoryURL,
2496+
requirementKind: dep.requirementKind,
2497+
requirementMinimumVersion: dep.requirementMinimumVersion,
2498+
products: new Map()
2499+
};
2500+
packagesByKey.set(packageKey, pkg);
2501+
}
2502+
2503+
hostProducts.forEach(product => {
2504+
if (!pkg.products.has(product.productName)) {
2505+
pkg.products.set(product.productName, {
2506+
productName: product.productName,
2507+
frameworkName: product.frameworkName
2508+
});
2509+
}
2510+
});
2511+
2512+
this.logger.debug(`${SPM_LOG_PREFIX} Module ${module.id} requests host-level product(s) ${hostProducts.map(p => p.productName).join(', ')} from ${dep.repositoryURL}`);
2513+
2514+
this.trackSpmVersionRequirement(dep, module, repoTracker);
2515+
});
2516+
});
2517+
2518+
this.hostSpmPackages = Array.from(packagesByKey.values()).map(pkg => ({
2519+
remotePackageReference: pkg.remotePackageReference,
2520+
repositoryURL: pkg.repositoryURL,
2521+
requirementKind: pkg.requirementKind,
2522+
requirementMinimumVersion: pkg.requirementMinimumVersion,
2523+
products: Array.from(pkg.products.values())
2524+
}));
2525+
2526+
if (this.hostSpmPackages.length) {
2527+
this.logger.info(`${SPM_LOG_PREFIX} Will add ${this.hostSpmPackages.length} Swift package(s) to the app project`);
2528+
this.hostSpmPackages.forEach(pkg => {
2529+
this.logger.debug(`${SPM_LOG_PREFIX} Package ${pkg.repositoryURL} (${pkg.requirementKind} ${pkg.requirementMinimumVersion}) products: ${pkg.products.map(p => p.productName).join(', ')}`);
2530+
});
2531+
} else {
2532+
this.logger.debug(`${SPM_LOG_PREFIX} No host-level Swift packages needed for this build`);
2533+
}
2534+
}
2535+
2536+
trackSpmVersionRequirement(dep, module, tracker) {
2537+
const repositoryURL = dep.repositoryURL;
2538+
const existing = tracker.get(repositoryURL);
2539+
2540+
if (!existing) {
2541+
tracker.set(repositoryURL, {
2542+
requirementKind: dep.requirementKind,
2543+
requirementMinimumVersion: dep.requirementMinimumVersion,
2544+
modules: new Set([ module.id ])
2545+
});
2546+
return;
2547+
}
2548+
2549+
const matches = existing.requirementKind === dep.requirementKind
2550+
&& existing.requirementMinimumVersion === dep.requirementMinimumVersion;
2551+
2552+
existing.modules.add(module.id);
2553+
2554+
if (!matches) {
2555+
this.logger.warn(`${SPM_LOG_PREFIX} Swift package ${repositoryURL} is requested with conflicting versions by modules: ${Array.from(existing.modules).join(', ')}. Using ${existing.requirementKind} ${existing.requirementMinimumVersion}.`);
2556+
}
2557+
}
2558+
2559+
normalizeModuleSpmDependency(module, dep) {
2560+
if (!dep || typeof dep !== 'object') {
2561+
return null;
2562+
}
2563+
2564+
const repositoryURL = dep.repositoryURL || dep.repositoryUrl;
2565+
if (!repositoryURL) {
2566+
this.logger.warn(`${SPM_LOG_PREFIX} Module ${module.id} declares a Swift package without repositoryURL. Skip.`);
2567+
return null;
2568+
}
2569+
2570+
const requirementKind = dep.requirementKind || (dep.requirement && dep.requirement.kind) || 'upToNextMajorVersion';
2571+
const requirementMinimumVersion = dep.requirementMinimumVersion || (dep.requirement && (dep.requirement.minimumVersion || dep.requirement.minVersion)) || '1.0.0';
2572+
const dependencyLinkage = this.normalizeModuleSpmLinkage(dep.linkage);
2573+
2574+
const products = Array.isArray(dep.products)
2575+
? dep.products.map(product => this.normalizeModuleSpmProduct(product, dependencyLinkage)).filter(Boolean)
2576+
: [];
2577+
2578+
if (!products.length) {
2579+
this.logger.warn(`${SPM_LOG_PREFIX} Module ${module.id} declares Swift package ${repositoryURL} but no valid products. Skip.`);
2580+
return null;
2581+
}
2582+
2583+
return {
2584+
remotePackageReference: dep.remotePackageReference || dep.reference || this.generateSpmReferenceFromRepo(repositoryURL),
2585+
repositoryURL,
2586+
requirementKind,
2587+
requirementMinimumVersion,
2588+
linkage: dependencyLinkage,
2589+
products
2590+
};
2591+
}
2592+
2593+
normalizeModuleSpmProduct(product, dependencyLinkage) {
2594+
if (!product || typeof product !== 'object') {
2595+
return null;
2596+
}
2597+
2598+
const productName = product.productName || product.name;
2599+
if (!productName) {
2600+
return null;
2601+
}
2602+
2603+
return {
2604+
productName,
2605+
frameworkName: product.frameworkName || productName,
2606+
linkage: this.normalizeModuleSpmLinkage(product.linkage || dependencyLinkage)
2607+
};
2608+
}
2609+
2610+
normalizeModuleSpmLinkage(linkage) {
2611+
return linkage && typeof linkage === 'string' && linkage.toLowerCase() === 'host' ? 'host' : 'embedded';
2612+
}
2613+
2614+
generateSpmReferenceFromRepo(repositoryURL) {
2615+
if (!repositoryURL || typeof repositoryURL !== 'string') {
2616+
return 'TiSPMPackage';
2617+
}
2618+
2619+
const withoutGit = repositoryURL.replace(/\.git$/, '');
2620+
const segments = withoutGit.split('/');
2621+
const candidate = segments[segments.length - 1] || withoutGit;
2622+
const sanitized = candidate.replace(/[^A-Za-z0-9_-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
2623+
return sanitized || 'TiSPMPackage';
2624+
}
2625+
2626+
moduleHasLegacySpmHook(module) {
2627+
const hookPath = path.join(module.modulePath, 'hooks', 'ti.spm.js');
2628+
return fs.existsSync(hookPath);
2629+
}
2630+
2631+
applySwiftPackageDependencies(xcodeProject) {
2632+
if (!this.hostSpmPackages || !this.hostSpmPackages.length) {
2633+
this.logger.debug(`${SPM_LOG_PREFIX} No Swift packages to inject into the Xcode project`);
2634+
return;
2635+
}
2636+
2637+
this.logger.debug(`${SPM_LOG_PREFIX} Injecting ${this.hostSpmPackages.length} Swift package(s) declared by modules`);
2638+
2639+
const xobjs = xcodeProject.hash.project.objects;
2640+
2641+
this.hostSpmPackages.forEach(pkg => {
2642+
this.logger.debug(`${SPM_LOG_PREFIX} Injecting ${pkg.repositoryURL} (${pkg.requirementKind} ${pkg.requirementMinimumVersion}) with products ${pkg.products.map(p => p.productName).join(', ')}`);
2643+
injectSPMPackage(xobjs, pkg, {
2644+
generateUUID: () => this.generateXcodeUuid(xcodeProject)
2645+
});
2646+
});
2647+
}
2648+
24302649
/**
24312650
* Performs the build operations.
24322651
*
@@ -4122,6 +4341,8 @@ class iOSBuilder extends Builder {
41224341
comment: 'tiverify.xcframework'
41234342
});
41244343

4344+
this.applySwiftPackageDependencies(xcodeProject);
4345+
41254346
// run the xcode project hook
41264347
const hook = this.cli.createHook('build.ios.xcodeproject', this, function (xcodeProject, done) {
41274348
const contents = xcodeProject.writeSync(),

0 commit comments

Comments
 (0)