From cd75a82f96098b8e28896afc89bbf74c3cd41af8 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Tue, 21 Mar 2023 08:08:46 +0100 Subject: [PATCH 01/12] feat(ios): support adding SPM packages --- packages/project/src/definitions.ts | 9 ++ packages/project/src/ios/project.ts | 15 ++- packages/project/src/ios/spm.ts | 138 ++++++++++++++++++++++ packages/project/test/project.ios.test.ts | 14 +++ 4 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 packages/project/src/ios/spm.ts diff --git a/packages/project/src/definitions.ts b/packages/project/src/definitions.ts index 20a14999..36fe1238 100644 --- a/packages/project/src/definitions.ts +++ b/packages/project/src/definitions.ts @@ -7,6 +7,8 @@ export interface IosPbxProject { [key: string]: any; } +export type IosPbxArrayValue = { value: string; comment: string }; + export interface IosEntitlements { [key: string]: any; } @@ -48,6 +50,13 @@ export type IosBuildName = 'Debug' | 'Release' | string; export type IosTargetName = string; export type IosProjectName = string; +export interface IosSPMPackageDefinition { + name: string; + libs: string[]; + repositoryURL: string; + version: string; +} + /** * Android definitions */ diff --git a/packages/project/src/ios/project.ts b/packages/project/src/ios/project.ts index 68e3280d..51e98846 100644 --- a/packages/project/src/ios/project.ts +++ b/packages/project/src/ios/project.ts @@ -5,13 +5,14 @@ import { copy, pathExists, readdir, writeFile } from '@ionic/utils-fs'; import { parsePbxProject, pbxReadString, pbxSerializeString } from "../util/pbx"; import { MobileProject } from "../project"; -import { IosPbxProject, IosEntitlements, IosFramework, IosBuildName, IosTarget, IosTargetName, IosTargetBuildConfiguration, IosFrameworkOpts } from '../definitions'; +import { IosPbxProject, IosEntitlements, IosFramework, IosBuildName, IosTarget, IosTargetName, IosTargetBuildConfiguration, IosFrameworkOpts, IosSPMPackageDefinition } from '../definitions'; import { VFSRef, VFSFile } from '../vfs'; import { XmlFile } from '../xml'; import { PlistFile } from '../plist'; import { PlatformProject } from '../platform-project'; import { Logger } from '../logger'; import { assertParentDirs } from '../util/fs'; +import { addSPMPackageToProject } from './spm'; const defaultEntitlementsPlist = ` @@ -362,6 +363,18 @@ export class IosProject extends PlatformProject { return this.pbxProject?.pbxFrameworksBuildPhaseObj(target.id)?.files?.map((f: any) => f.comment.split(' ')[0]); } + /** + * Add a SPM framework for the given target. + * If the `targetName` is null the main app target is used. + */ + addSPMPackage(targetName: IosTargetName | null, packageDef: IosSPMPackageDefinition, opts: IosFrameworkOpts = {}) { + targetName = this.assertTargetName(targetName || null); + const target = this.getTarget(targetName); + if (this.pbxProject) { + addSPMPackageToProject(this.pbxProject, target!.id, packageDef); + } + } + /** * Get the path to the entitlements file for the given target and build. * If the `targetName` is null the main app target is used. If the `buildName` is null the first diff --git a/packages/project/src/ios/spm.ts b/packages/project/src/ios/spm.ts new file mode 100644 index 00000000..40025776 --- /dev/null +++ b/packages/project/src/ios/spm.ts @@ -0,0 +1,138 @@ +import { + IosPbxArrayValue, + IosPbxProject, + IosSPMPackageDefinition, +} from '../definitions'; + +export function addSPMPackageToProject( + project: IosPbxProject, + targetId: string, + pkg: IosSPMPackageDefinition, +) { + const helper = new SPMHelper(project); + const target = project.pbxNativeTargetSection()[targetId]; + const firstProject = project.getFirstProject().firstProject; + const packageReferences: IosPbxArrayValue[] = (firstProject[ + 'packageReferences' + ] ??= []); + const packageProductReferences: IosPbxArrayValue[] = (target[ + 'packageProductDependencies' + ] ??= []); + const frameworkBuildPhaseObj = project.pbxFrameworksBuildPhaseObj(targetId); + const frameworkBuildPhaseFiles: IosPbxArrayValue[] = (frameworkBuildPhaseObj[ + 'files' + ] ??= []); + + const packageReferenceComment = `XCRemoteSwiftPackageReference "${pkg.name}"`; + + const { uuid: spmPackageReferenceUUID, comment: spmPackageReferenceComment } = + helper.addOrUpdateEntry( + 'XCRemoteSwiftPackageReference', + packageReferenceComment, + { + isa: 'XCRemoteSwiftPackageReference', + repositoryURL: JSON.stringify(pkg.repositoryURL), + requirement: { + // todo: support different ranges? + kind: 'upToNextMajorVersion', + minimumVersion: pkg.version, + }, + }, + ); + + helper.addOrUpdateArrayEntry(packageReferences, spmPackageReferenceUUID, { + value: spmPackageReferenceUUID, + comment: packageReferenceComment, + }); + + for (const lib of pkg.libs) { + const { uuid: spmProductDependencyUUID } = helper.addOrUpdateEntry( + 'XCSwiftPackageProductDependency', + lib, + { + isa: 'XCSwiftPackageProductDependency', + package: spmPackageReferenceUUID, + package_comment: spmPackageReferenceComment, + productName: lib, + }, + ); + + const libComment = lib + ' in Frameworks'; + + const { uuid: spmBuildFileUuid } = helper.addOrUpdateEntry( + 'PBXBuildFile', + libComment, + { + isa: 'PBXBuildFile', + productRef: spmProductDependencyUUID, + productRef_comment: lib, + }, + ); + + helper.addOrUpdateArrayEntry( + packageProductReferences, + spmProductDependencyUUID, + { + value: spmProductDependencyUUID, + comment: lib, + }, + ); + + helper.addOrUpdateArrayEntry(frameworkBuildPhaseFiles, spmBuildFileUuid, { + value: spmBuildFileUuid, + comment: libComment, + }); + } +} + +class SPMHelper { + constructor(private pbxProject: IosPbxProject) {} + + addOrUpdateArrayEntry( + array: IosPbxArrayValue[], + lookupValue: string, + value: IosPbxArrayValue, + ) { + const existing = array.find(entry => entry.value === lookupValue); + + if (existing) { + Object.assign(existing, value); + return; + } + + array.push(value); + } + + addOrUpdateEntry(section: string, entryComment: string, entry: any) { + const pbxSection = this.getOrCreateSection(section); + const entryUuid = this.getExistingOrGenerateUUID(section, entryComment); + + const entryCommentKey = `${entryUuid}_comment`; + pbxSection[entryCommentKey] = entryComment; + pbxSection[entryUuid] = entry; + + return { + uuid: entryUuid, + comment: entryComment, + }; + } + + getExistingOrGenerateUUID(section: string, comment: string) { + const existingUUID = Object.keys( + this.pbxProject.hash.project.objects[section], + ) + .find(key => { + if (key.endsWith('_comment')) { + return this.pbxProject.hash.project.objects[section][key] === comment; + } + return false; + }) + ?.replace('_comment', ''); + + return existingUUID ?? this.pbxProject.generateUuid(); + } + + getOrCreateSection(section: string) { + return (this.pbxProject.hash.project.objects[section] ??= new Object()); + } +} diff --git a/packages/project/test/project.ios.test.ts b/packages/project/test/project.ios.test.ts index e558c32c..6c6721b9 100644 --- a/packages/project/test/project.ios.test.ts +++ b/packages/project/test/project.ios.test.ts @@ -163,6 +163,20 @@ describe('project - ios standard', () => { expect(fwks.every(f => (frameworks?.indexOf(f) ?? -1) >= 0)).toBe(true); }); + it.only('should add spm packages', async () => { + const fwks = [{ + name: 'swift-numerics', + libs: ['Numerics'], + repositoryURL: "https://github.com/apple/swift-numerics.git", + version: '1.0.0' + }]; + fwks.forEach(f => project.ios?.addSPMPackage('App', f)); + const frameworks = project.ios?.getFrameworks('App'); + expect(fwks.every(f => { + return f.libs.every(l => (frameworks?.indexOf(l) ?? -1) >= 0) + })).toBe(true); + }); + it('should add frameworks to non-app targets', async () => { const fwks = ['WebKit.framework', 'QuartzCore.framework']; fwks.forEach(f => project.ios?.addFramework('My App Clip', f)); From 47e4e81a53390518fdd3c67e1e349a4415117614 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Tue, 21 Mar 2023 08:54:46 +0100 Subject: [PATCH 02/12] test(ios): cover SPM related actions --- packages/project/test/project.ios.test.ts | 93 ++++++++++++++++++++--- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/packages/project/test/project.ios.test.ts b/packages/project/test/project.ios.test.ts index 6c6721b9..15099abc 100644 --- a/packages/project/test/project.ios.test.ts +++ b/packages/project/test/project.ios.test.ts @@ -1,7 +1,7 @@ import tempy from 'tempy'; import { join } from 'path'; import { copy, pathExists, readFile, rm } from '@ionic/utils-fs'; -import { MobileProject, StringsFile, XCConfigFile, XmlFile } from '../src'; +import { IosPbxArrayValue, MobileProject, StringsFile, XCConfigFile, XmlFile } from '../src'; import { MobileProjectConfig } from '../src/config'; import { PlistFile } from '../src/plist'; @@ -163,18 +163,87 @@ describe('project - ios standard', () => { expect(fwks.every(f => (frameworks?.indexOf(f) ?? -1) >= 0)).toBe(true); }); - it.only('should add spm packages', async () => { - const fwks = [{ - name: 'swift-numerics', - libs: ['Numerics'], - repositoryURL: "https://github.com/apple/swift-numerics.git", - version: '1.0.0' - }]; - fwks.forEach(f => project.ios?.addSPMPackage('App', f)); + it('should add spm packages', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + // Make sure there are no SPM packages to start + expect(sections.XCRemoteSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'swift-numerics', + libs: ['RealModule', 'ComplexModule'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0', + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure the frameworks are added const frameworks = project.ios?.getFrameworks('App'); - expect(fwks.every(f => { - return f.libs.every(l => (frameworks?.indexOf(l) ?? -1) >= 0) - })).toBe(true); + expect( + pkgs.every(p => { + return p.libs.every(l => (frameworks?.indexOf(l) ?? -1) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return Object.values(sections.XCRemoteSwiftPackageReference) + .filter(s => typeof s !== 'string') + .some((v: any) => v.repositoryURL.indexOf(p.repositoryURL) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.XCSwiftPackageProductDependency) + .filter(s => typeof s !== 'string') + .some((v: any) => v.productName.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // ensure that the package is added to the project's packageReferences + expect( + pkgs.every(p => { + return pbx! + .getFirstProject() + .firstProject.packageReferences.some( + (v: IosPbxArrayValue) => v.comment.indexOf(p.name) >= 0, + ); + }), + ).toBe(true); + + // ensure that the package is added to the correct target's packageProductDependencies + const targetId = project.ios?.getTarget('App')?.id; + const targetSection = pbx!.pbxNativeTargetSection()[targetId!]; + expect( + pkgs.every(p => { + return p.libs.every(l => { + return targetSection.packageProductDependencies.some( + (v: IosPbxArrayValue) => v.comment.indexOf(l) >= 0, + ); + }); + }), + ).toBe(true); + + // ensure that the package is added to the BuildFiles + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.PBXBuildFile) + .filter(s => typeof s === 'string') + .some((v: any) => v.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // Make sure the SPM packages were added + expect(sections.XCRemoteSwiftPackageReference).toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).toBeDefined(); }); it('should add frameworks to non-app targets', async () => { From e91075f4cb75190b50ac6254d6452d862d07c328 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Tue, 21 Mar 2023 09:15:14 +0100 Subject: [PATCH 03/12] test: make sure packages are not duplicated when adding them twice --- packages/project/test/project.ios.test.ts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/project/test/project.ios.test.ts b/packages/project/test/project.ios.test.ts index 15099abc..a7276537 100644 --- a/packages/project/test/project.ios.test.ts +++ b/packages/project/test/project.ios.test.ts @@ -246,6 +246,35 @@ describe('project - ios standard', () => { expect(sections.XCSwiftPackageProductDependency).toBeDefined(); }); + it('should add spm packages only once', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + + // Make sure there are no SPM packages to start + expect(sections.XCRemoteSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'swift-numerics', + libs: ['Numerics'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0', + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + expect(Object.values(sections.XCRemoteSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + + // add the same package again + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure that the package isn't added again + expect(Object.values(sections.XCRemoteSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + }); + it('should add frameworks to non-app targets', async () => { const fwks = ['WebKit.framework', 'QuartzCore.framework']; fwks.forEach(f => project.ios?.addFramework('My App Clip', f)); From 7f413a42d623525942245b8c2f5a6e9e39fb8062 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Tue, 21 Mar 2023 09:22:30 +0100 Subject: [PATCH 04/12] refactor: use template string to match rest of the code --- packages/project/src/ios/spm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/project/src/ios/spm.ts b/packages/project/src/ios/spm.ts index 40025776..344ec268 100644 --- a/packages/project/src/ios/spm.ts +++ b/packages/project/src/ios/spm.ts @@ -57,7 +57,7 @@ export function addSPMPackageToProject( }, ); - const libComment = lib + ' in Frameworks'; + const libComment = `${lib} in Frameworks`; const { uuid: spmBuildFileUuid } = helper.addOrUpdateEntry( 'PBXBuildFile', From b1631708710c86a0be4680dd2e4f495ba2ecff65 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Tue, 21 Mar 2023 09:36:17 +0100 Subject: [PATCH 05/12] chore: add changeset --- .changeset/warm-emus-collect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/warm-emus-collect.md diff --git a/.changeset/warm-emus-collect.md b/.changeset/warm-emus-collect.md new file mode 100644 index 00000000..6bf8989d --- /dev/null +++ b/.changeset/warm-emus-collect.md @@ -0,0 +1,5 @@ +--- +'@trapezedev/project': minor +--- + +Add support for iOS SPM (SwiftPackageManager) packages From 1483daef1feb49f87ba32e65ec8bbb0826ec81f9 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Wed, 19 Jul 2023 17:24:10 +0200 Subject: [PATCH 06/12] feat(spm): support adding local SPM packages --- packages/project/src/definitions.ts | 10 +- packages/project/src/ios/project.ts | 2 +- packages/project/src/ios/spm.ts | 42 +++++--- packages/project/test/project.ios.test.ts | 114 +++++++++++++++++++++- 4 files changed, 153 insertions(+), 15 deletions(-) diff --git a/packages/project/src/definitions.ts b/packages/project/src/definitions.ts index 36fe1238..2f56f19c 100644 --- a/packages/project/src/definitions.ts +++ b/packages/project/src/definitions.ts @@ -50,13 +50,21 @@ export type IosBuildName = 'Debug' | 'Release' | string; export type IosTargetName = string; export type IosProjectName = string; -export interface IosSPMPackageDefinition { +export interface IosRemoteSPMPackageDefinition { name: string; libs: string[]; repositoryURL: string; version: string; } +export interface IosLocalSPMPackageDefinition { + name: string; + libs: string[]; + path: string; +} + +export type IosSPMPackageDefinition = IosRemoteSPMPackageDefinition | IosLocalSPMPackageDefinition; + /** * Android definitions */ diff --git a/packages/project/src/ios/project.ts b/packages/project/src/ios/project.ts index 51e98846..d3cfd4c6 100644 --- a/packages/project/src/ios/project.ts +++ b/packages/project/src/ios/project.ts @@ -371,7 +371,7 @@ export class IosProject extends PlatformProject { targetName = this.assertTargetName(targetName || null); const target = this.getTarget(targetName); if (this.pbxProject) { - addSPMPackageToProject(this.pbxProject, target!.id, packageDef); + addSPMPackageToProject(this.pbxProject, target!.id, packageDef, this.project.projectRoot); } } diff --git a/packages/project/src/ios/spm.ts b/packages/project/src/ios/spm.ts index 344ec268..0f6148c7 100644 --- a/packages/project/src/ios/spm.ts +++ b/packages/project/src/ios/spm.ts @@ -3,11 +3,13 @@ import { IosPbxProject, IosSPMPackageDefinition, } from '../definitions'; +import path from 'path'; export function addSPMPackageToProject( project: IosPbxProject, targetId: string, pkg: IosSPMPackageDefinition, + projectRoot: string, ) { const helper = new SPMHelper(project); const target = project.pbxNativeTargetSection()[targetId]; @@ -23,21 +25,39 @@ export function addSPMPackageToProject( 'files' ] ??= []); - const packageReferenceComment = `XCRemoteSwiftPackageReference "${pkg.name}"`; + let packageReferenceComment: string; + let packageReferenceSection: string; + let packageReferenceSectionContent: Record; + + if ('path' in pkg) { + // local package + const relativePath = path.relative(projectRoot, path.resolve(projectRoot, pkg.path)); + packageReferenceComment = `XCLocalSwiftPackageReference "${relativePath}"`; + packageReferenceSection = 'XCLocalSwiftPackageReference'; + packageReferenceSectionContent = { + isa: packageReferenceSection, + relativePath: JSON.stringify(relativePath), + }; + } else { + // remote package + packageReferenceComment = `XCRemoteSwiftPackageReference "${pkg.name}"`; + packageReferenceSection = 'XCRemoteSwiftPackageReference'; + packageReferenceSectionContent = { + isa: packageReferenceSection, + repositoryURL: JSON.stringify(pkg.repositoryURL), + requirement: { + // todo: support different ranges? + kind: 'upToNextMajorVersion', + minimumVersion: pkg.version, + }, + }; + } const { uuid: spmPackageReferenceUUID, comment: spmPackageReferenceComment } = helper.addOrUpdateEntry( - 'XCRemoteSwiftPackageReference', + packageReferenceSection, packageReferenceComment, - { - isa: 'XCRemoteSwiftPackageReference', - repositoryURL: JSON.stringify(pkg.repositoryURL), - requirement: { - // todo: support different ranges? - kind: 'upToNextMajorVersion', - minimumVersion: pkg.version, - }, - }, + packageReferenceSectionContent, ); helper.addOrUpdateArrayEntry(packageReferences, spmPackageReferenceUUID, { diff --git a/packages/project/test/project.ios.test.ts b/packages/project/test/project.ios.test.ts index a7276537..2f1f7695 100644 --- a/packages/project/test/project.ios.test.ts +++ b/packages/project/test/project.ios.test.ts @@ -163,7 +163,7 @@ describe('project - ios standard', () => { expect(fwks.every(f => (frameworks?.indexOf(f) ?? -1) >= 0)).toBe(true); }); - it('should add spm packages', async () => { + it('should add remote spm packages', async () => { const pbx = project.ios?.getPbxProject(); const sections = pbx!.hash.project.objects; // Make sure there are no SPM packages to start @@ -246,7 +246,7 @@ describe('project - ios standard', () => { expect(sections.XCSwiftPackageProductDependency).toBeDefined(); }); - it('should add spm packages only once', async () => { + it('should add remote spm packages only once', async () => { const pbx = project.ios?.getPbxProject(); const sections = pbx!.hash.project.objects; @@ -275,6 +275,116 @@ describe('project - ios standard', () => { expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); }); + it('should add local spm packages', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + // Make sure there are no SPM packages to start + expect(sections.XCLocalSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'local-swift-numerics', + libs: ['LocalRealModule', 'LocalComplexModule'], + path: 'path/to/package', + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure the frameworks are added + const frameworks = project.ios?.getFrameworks('App'); + expect( + pkgs.every(p => { + return p.libs.every(l => (frameworks?.indexOf(l) ?? -1) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return Object.values(sections.XCLocalSwiftPackageReference) + .filter(s => typeof s !== 'string') + .some((v: any) => v.relativePath.indexOf(p.path) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.XCSwiftPackageProductDependency) + .filter(s => typeof s !== 'string') + .some((v: any) => v.productName.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // ensure that the package is added to the project's packageReferences + expect( + pkgs.every(p => { + return pbx! + .getFirstProject() + .firstProject.packageReferences.some( + (v: IosPbxArrayValue) => v.comment.indexOf(p.path) >= 0, + ); + }), + ).toBe(true); + + // ensure that the package is added to the correct target's packageProductDependencies + const targetId = project.ios?.getTarget('App')?.id; + const targetSection = pbx!.pbxNativeTargetSection()[targetId!]; + expect( + pkgs.every(p => { + return p.libs.every(l => { + return targetSection.packageProductDependencies.some( + (v: IosPbxArrayValue) => v.comment.indexOf(l) >= 0, + ); + }); + }), + ).toBe(true); + + // ensure that the package is added to the BuildFiles + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.PBXBuildFile) + .filter(s => typeof s === 'string') + .some((v: any) => v.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // Make sure the SPM packages were added + expect(sections.XCLocalSwiftPackageReference).toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).toBeDefined(); + }); + + it('should add local spm packages only once', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + + // Make sure there are no SPM packages to start + expect(sections.XCLocalSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'local-swift-numerics', + libs: ['Numerics'], + path: 'path/to/package' + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + expect(Object.values(sections.XCLocalSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + + // add the same package again + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure that the package isn't added again + expect(Object.values(sections.XCLocalSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + }); + it('should add frameworks to non-app targets', async () => { const fwks = ['WebKit.framework', 'QuartzCore.framework']; fwks.forEach(f => project.ios?.addFramework('My App Clip', f)); From abd2960a09cd679d953693a92f7b194a8a71407d Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Thu, 20 Jul 2023 18:44:28 +0200 Subject: [PATCH 07/12] feat(configure): add spmPackages op --- packages/configure/src/definitions.ts | 6 ++- packages/configure/src/op.ts | 2 + .../src/operations/ios/spmPackages.ts | 12 +++++ .../test/ops/ios.spmPackages.test.ts | 48 +++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/configure/src/operations/ios/spmPackages.ts create mode 100644 packages/configure/test/ops/ios.spmPackages.test.ts diff --git a/packages/configure/src/definitions.ts b/packages/configure/src/definitions.ts index 6f0b1f1d..51e1c472 100644 --- a/packages/configure/src/definitions.ts +++ b/packages/configure/src/definitions.ts @@ -1,4 +1,4 @@ -import { AndroidGradleInjectType } from '@trapezedev/project'; +import { AndroidGradleInjectType, IosSPMPackageDefinition } from '@trapezedev/project'; export type OperationMeta = string[]; export interface Operation { @@ -144,3 +144,7 @@ export type IosXCConfigOperationValue = { file?: string; set?: any; } + +export interface IosSpmPackagesOperation { + value: IosSPMPackageDefinition[]; +} diff --git a/packages/configure/src/op.ts b/packages/configure/src/op.ts index 14652cf1..12c90168 100644 --- a/packages/configure/src/op.ts +++ b/packages/configure/src/op.ts @@ -213,6 +213,8 @@ function createOpDisplayText(op: Partial) { return (Array.isArray(op.value) ? op.value : op.value.entries).map((v: any) => Object.keys(v)).join(', '); case 'ios.frameworks': return op.value.join(', '); + case 'ios.spmPackages': + return (Array.isArray(op.value) ? op.value : op.value.entries).map((v: any) => `${v.name} (${v.libs.join(', ')})`).join(', '); case 'ios.plist': return `${op.value.entries.length} modifications`; case 'ios.xml': diff --git a/packages/configure/src/operations/ios/spmPackages.ts b/packages/configure/src/operations/ios/spmPackages.ts new file mode 100644 index 00000000..386e7ac3 --- /dev/null +++ b/packages/configure/src/operations/ios/spmPackages.ts @@ -0,0 +1,12 @@ +import { Context } from '../../ctx'; +import { Operation, OperationMeta } from '../../definitions'; + +export default async function execute(ctx: Context, op: Operation) { + for (let spmPackage of op.value) { + ctx.project.ios?.addSPMPackage(op.iosTarget, spmPackage); + } +} + +export const OPS: OperationMeta = [ + 'ios.spmPackages' +] \ No newline at end of file diff --git a/packages/configure/test/ops/ios.spmPackages.test.ts b/packages/configure/test/ops/ios.spmPackages.test.ts new file mode 100644 index 00000000..c6edc7f1 --- /dev/null +++ b/packages/configure/test/ops/ios.spmPackages.test.ts @@ -0,0 +1,48 @@ +import { copy } from '@ionic/utils-fs'; +import { XCConfigFile } from '@trapezedev/project/src'; +import { join } from 'path'; +import tempy from 'tempy'; + +import { Context, loadContext } from '../../src/ctx'; +import { IosSpmPackagesOperation, Operation } from '../../src/definitions'; +import Op from '../../src/operations/ios/spmPackages'; + +describe('op: ios.spmPackages', () => { + let dir: string; + let ctx: Context; + + beforeEach(async () => { + dir = tempy.directory(); + + await copy('../common/test/fixtures/ios-and-android', dir); + + ctx = await loadContext(dir); + ctx.args.quiet = true; + }); + + it('should set ios.spmPackages', async () => { + const op: IosSpmPackagesOperation = { + value: [ + { + name: 'swift-numerics', + libs: ['Numberics'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0' + }, + { + name: 'local-swift-numerics', + libs: ['Numberics'], + path: '../path/to/local-swift-numerics' + }, + ], + }; + + await Op(ctx, op as Operation); + + const pbxProject = ctx.project.ios?.getPbxProject() + const pbxProjectText = pbxProject?.writeSync() + + expect(pbxProjectText).toContain('https://github.com/apple/swift-numerics.git') + expect(pbxProjectText).toContain('../path/to/local-swift-numerics') + }); +}); From c847d1d9187a1bbd1713615014eebeab021836d5 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Thu, 20 Jul 2023 18:49:14 +0200 Subject: [PATCH 08/12] chore: update changeset to bump the configure package --- .changeset/warm-emus-collect.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/warm-emus-collect.md b/.changeset/warm-emus-collect.md index 6bf8989d..7aeaf3d9 100644 --- a/.changeset/warm-emus-collect.md +++ b/.changeset/warm-emus-collect.md @@ -1,5 +1,6 @@ --- '@trapezedev/project': minor +'@trapezedev/configure': minor --- Add support for iOS SPM (SwiftPackageManager) packages From af942eba94ea8904addfa33b1e8af2b245196732 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Thu, 20 Jul 2023 18:50:50 +0200 Subject: [PATCH 09/12] refactor: spmPackages should always be an array --- packages/configure/src/op.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/configure/src/op.ts b/packages/configure/src/op.ts index 12c90168..01f2aeea 100644 --- a/packages/configure/src/op.ts +++ b/packages/configure/src/op.ts @@ -214,7 +214,7 @@ function createOpDisplayText(op: Partial) { case 'ios.frameworks': return op.value.join(', '); case 'ios.spmPackages': - return (Array.isArray(op.value) ? op.value : op.value.entries).map((v: any) => `${v.name} (${v.libs.join(', ')})`).join(', '); + return op.value.map((v: any) => `${v.name} (${v.libs.join(', ')})`).join(', '); case 'ios.plist': return `${op.value.entries.length} modifications`; case 'ios.xml': From 6b0913f4c0d795fbe08c52f3c3ddf526bcad259d Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Thu, 20 Jul 2023 18:55:23 +0200 Subject: [PATCH 10/12] docs: document `spmPackages` op --- packages/website/docs/Operations/ios.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/website/docs/Operations/ios.md b/packages/website/docs/Operations/ios.md index 49698946..6deff2bc 100644 --- a/packages/website/docs/Operations/ios.md +++ b/packages/website/docs/Operations/ios.md @@ -288,4 +288,26 @@ platforms: - file: App/Config.xcconfig set: "PRODUCT_NAME": "$(NAME)" +``` + +### `spmPackages` + + +*Since: 7.1.0* + +Add iOS SPM (Swift Package Manager) dependencies to a project. + +```yaml +platforms: + ios: + targets: + App: + spmPackages: + - name: "swift-numerics" + libs: [ "Numerics" ] + repositoryURL: "https://github.com/apple/swift-numerics.git" + version: "1.0.0" + - name: "local-swift-numerics" + libs: [ "ComplexModule", "RealModule" ] + path: "../path/to/local-swift-numerics" ``` \ No newline at end of file From 932de0512caf33c3baa5851bd336493c229a72e2 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Thu, 20 Jul 2023 18:57:53 +0200 Subject: [PATCH 11/12] docs: document addSPMPackage api --- packages/website/docs/project-api.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/website/docs/project-api.md b/packages/website/docs/project-api.md index febf857c..7c1d38f9 100644 --- a/packages/website/docs/project-api.md +++ b/packages/website/docs/project-api.md @@ -148,6 +148,27 @@ project.ios?.addFramework(targetName, 'Custom.framework', { project.ios?.getFrameworks(targetName); ``` +#### SPM Packages + +SPM (Swift Package Manager) packages can be added + +```typescript +// remote SPM packages +await project.ios?.addSPMPackage(targetName, { + name: 'swift-numerics', + libs: ['Numerics'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0' +}) + +// local SPM packages +await project.ios?.addSPMPackage(targetName, { + name: 'local-swift-numerics', + libs: ['LocalNumerics'], + path: '../path/to/local-swift-numerics', +}) +``` + #### Entitlements Entitlements can be managed: From d1993e122b289d0a52b61e990bdab2778c529423 Mon Sep 17 00:00:00 2001 From: Igor Randjelovic Date: Thu, 20 Jul 2023 19:00:43 +0200 Subject: [PATCH 12/12] docs: use same example as the configure OP docs --- packages/website/docs/project-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/website/docs/project-api.md b/packages/website/docs/project-api.md index 7c1d38f9..025ebe02 100644 --- a/packages/website/docs/project-api.md +++ b/packages/website/docs/project-api.md @@ -164,7 +164,7 @@ await project.ios?.addSPMPackage(targetName, { // local SPM packages await project.ios?.addSPMPackage(targetName, { name: 'local-swift-numerics', - libs: ['LocalNumerics'], + libs: ['ComplexModule', 'RealModule'], path: '../path/to/local-swift-numerics', }) ```