@@ -29,6 +29,7 @@ import { CopyResourcesTask } from '../../../cli/lib/tasks/copy-resources-task.js
2929import { ProcessJsTask } from '../../../cli/lib/tasks/process-js-task.js' ;
3030import { Color } from '../../../common/lib/color.js' ;
3131import { ProcessCSSTask } from '../../../cli/lib/tasks/process-css-task.js' ;
32+ import { injectSPMPackage } from '../lib/ios/spm.js' ;
3233import { exec , spawn } from 'node:child_process' ;
3334import ti from 'node-titanium-sdk' ;
3435import util from 'node:util' ;
@@ -45,6 +46,7 @@ const { version } = appc;
4546const require = createRequire ( import . meta. url ) ;
4647const platformsRegExp = new RegExp ( '^(' + ti . allPlatformNames . join ( '|' ) + ')$' ) ; // eslint-disable-line security/detect-non-literal-regexp
4748const pemCertRegExp = / ( ^ - - - - - B E G I N C E R T I F I C A T E - - - - - ) | ( - - - - - E N D C E R T I F I C A T E - - - - - .* $ ) | \n / g;
49+ const SPM_LOG_PREFIX = '[SPM]' ;
4850
4951class 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 ( / \. g i t $ / , '' ) ;
2620+ const segments = withoutGit . split ( '/' ) ;
2621+ const candidate = segments [ segments . length - 1 ] || withoutGit ;
2622+ const sanitized = candidate . replace ( / [ ^ A - Z a - z 0 - 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