diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index dc123cff04..fcad46b946 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -12,6 +12,7 @@ $injector.requirePublic("projectService", "./services/project-service"); $injector.require("androidProjectService", "./services/android-project-service"); $injector.require("androidPluginBuildService", "./services/android-plugin-build-service"); $injector.require("iOSEntitlementsService", "./services/ios-entitlements-service"); +$injector.require("iOSExtensionsService", "./services/ios-extensions-service"); $injector.require("iOSProjectService", "./services/ios-project-service"); $injector.require("iOSProvisionService", "./services/ios-provision-service"); $injector.require("xcconfigService", "./services/xcconfig-service"); diff --git a/lib/constants.ts b/lib/constants.ts index 3be5679121..ed6b281de0 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -47,6 +47,7 @@ export const HASHES_FILE_NAME = ".nshashes"; export const TNS_NATIVE_SOURCE_GROUP_NAME = "TNSNativeSource"; export const NATIVE_SOURCE_FOLDER = "src"; export const APPLICATION_RESPONSE_TIMEOUT_SECONDS = 60; +export const NATIVE_EXTENSION_FOLDER = "extensions"; export class PackageVersion { static NEXT = "next"; diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index d4ec9135a8..bc8aa2a854 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -566,6 +566,25 @@ interface ICocoaPodsPlatformManager { replacePlatformRow(podfileContent: string, podfilePath: string): { replacedContent: string, podfilePlatformData: IPodfilePlatformData }; } +/** + * Describes a service used to add and remove iOS extension + */ +interface IIOSExtensionsService { + addExtensionsFromPath(options: IAddExtensionsFromPathOptions): Promise; + removeExtensions(options: IRemoveExtensionsOptions): void; +} + +interface IAddExtensionsFromPathOptions{ + extensionsFolderPath: string; + projectData: IProjectData; + platformData: IPlatformData; + pbxProjPath: string; +} + +interface IRemoveExtensionsOptions { + pbxProjPath: string +} + interface IRubyFunction { functionName: string; functionParameters?: string; diff --git a/lib/definitions/xcode.d.ts b/lib/definitions/xcode.d.ts index 6b28c4382e..065fbe2067 100644 --- a/lib/definitions/xcode.d.ts +++ b/lib/definitions/xcode.d.ts @@ -13,7 +13,7 @@ declare module "nativescript-dev-xcode" { parse(callback: () => void): void; parseSync(): void; - writeSync(): string; + writeSync(options: any): string; addFramework(filepath: string, options?: Options): void; removeFramework(filePath: string, options?: Options): void; @@ -27,5 +27,35 @@ declare module "nativescript-dev-xcode" { updateBuildProperty(key: string, value: any): void; pbxXCBuildConfigurationSection(): any; + + addTarget(targetName: string, targetType: string, targetPath?: string): target; + addBuildPhase(filePathsArray: string[], + buildPhaseType: string, + comment: string, + target?: string, + optionsOrFolderType?: Object|string, + subfolderPath?: string + ): any; + addToBuildSettings(buildSetting: string, value: any, targetUuid?: string): void; + addPbxGroup( + filePathsArray: string[], + name: string, + path: string, + sourceTree: string, + opt: {filesRelativeToProject?: boolean, target?: string, uuid?: string, isMain?: boolean } + ): group; + addBuildProperty(prop: string, value: any, build_name?: string, productName?: string): void; + addToHeaderSearchPaths(file: string|Object, productName?: string): void; + removeTargetsByProductType(targetType: string): void + } + + class target { + uuid: string; + pbxNativeTarget: {productName: string} + } + + class group { + uuid: string; + pbxGroup: Object; } } \ No newline at end of file diff --git a/lib/node/xcode.ts b/lib/node/xcode.ts index e1595f9e23..b2d193c56f 100644 --- a/lib/node/xcode.ts +++ b/lib/node/xcode.ts @@ -3,7 +3,8 @@ import * as xcode from "nativescript-dev-xcode"; declare global { type IXcode = typeof xcode; export namespace IXcode { - export type project = typeof xcode.project; + export type target = xcode.target; + export type project = xcode.project; export interface Options extends xcode.Options {} // tslint:disable-line } } diff --git a/lib/services/ios-extensions-service.ts b/lib/services/ios-extensions-service.ts new file mode 100644 index 0000000000..67e850b2d8 --- /dev/null +++ b/lib/services/ios-extensions-service.ts @@ -0,0 +1,92 @@ +import * as path from "path"; + +export class IOSExtensionsService implements IIOSExtensionsService { + constructor(private $fs: IFileSystem, + private $pbxprojDomXcode: IPbxprojDomXcode, + private $xcode: IXcode) { + } + + public async addExtensionsFromPath({extensionsFolderPath, projectData, platformData, pbxProjPath}: IAddExtensionsFromPathOptions): Promise { + const targetUuids: string[] = []; + if (!this.$fs.exists(extensionsFolderPath)) { + return; + } + const project = new this.$xcode.project(pbxProjPath); + project.parseSync(); + this.$fs.readDirectory(extensionsFolderPath) + .filter(fileName => { + const filePath = path.join(extensionsFolderPath, fileName); + const stats = this.$fs.getFsStats(filePath); + + return stats.isDirectory() && !fileName.startsWith("."); + }) + .forEach(extensionFolder => { + const targetUuid = this.addExtensionToProject(extensionsFolderPath, extensionFolder, project, projectData, platformData); + targetUuids.push(targetUuid); + }); + + this.$fs.writeFile(pbxProjPath, project.writeSync({omitEmptyValues: true})); + this.prepareExtensionSigning(targetUuids, projectData, pbxProjPath); + } + + private addExtensionToProject(extensionsFolderPath: string, extensionFolder: string, project: IXcode.project, projectData: IProjectData, platformData: IPlatformData): string { + const extensionPath = path.join(extensionsFolderPath, extensionFolder); + const extensionRelativePath = path.relative(platformData.projectRoot, extensionPath); + const files = this.$fs.readDirectory(extensionPath) + .filter(filePath => !filePath.startsWith(".")) + .map(filePath => path.join(extensionPath, filePath)); + const target = project.addTarget(extensionFolder, 'app_extension', extensionRelativePath); + project.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', target.uuid); + project.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid); + project.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid); + + const extJsonPath = path.join(extensionsFolderPath, extensionFolder, "extension.json"); + if (this.$fs.exists(extJsonPath)) { + const extensionJson = this.$fs.readJson(extJsonPath); + _.forEach(extensionJson.frameworks, framework => { + project.addFramework( + framework, + { target: target.uuid } + ); + }); + if (extensionJson.assetcatalogCompilerAppiconName) { + project.addToBuildSettings("ASSETCATALOG_COMPILER_APPICON_NAME", extensionJson.assetcatalogCompilerAppiconName, target.uuid); + } + } + + project.addPbxGroup(files, extensionFolder, extensionPath, null, { isMain: true, target: target.uuid, filesRelativeToProject: true }); + project.addBuildProperty("PRODUCT_BUNDLE_IDENTIFIER", `${projectData.projectIdentifiers.ios}.${extensionFolder}`, "Debug", extensionFolder); + project.addBuildProperty("PRODUCT_BUNDLE_IDENTIFIER", `${projectData.projectIdentifiers.ios}.${extensionFolder}`, "Release", extensionFolder); + project.addToHeaderSearchPaths(extensionPath, target.pbxNativeTarget.productName); + + return target.uuid; + } + + private prepareExtensionSigning(targetUuids: string[], projectData:IProjectData, projectPath: string) { + const xcode = this.$pbxprojDomXcode.Xcode.open(projectPath); + const signing = xcode.getSigning(projectData.projectName); + if (signing !== undefined) { + _.forEach(targetUuids, targetUuid => { + if (signing.style === "Automatic") { + xcode.setAutomaticSigningStyleByTargetKey(targetUuid, signing.team); + } else { + for (const config in signing.configurations) { + const signingConfiguration = signing.configurations[config]; + xcode.setManualSigningStyleByTargetKey(targetUuid, signingConfiguration); + break; + } + } + }); + } + xcode.save(); + } + + public removeExtensions({pbxProjPath}: IRemoveExtensionsOptions): void { + const project = new this.$xcode.project(pbxProjPath); + project.parseSync(); + project.removeTargetsByProductType("com.apple.product-type.app-extension"); + this.$fs.writeFile(pbxProjPath, project.writeSync({omitEmptyValues: true})); + } +} + +$injector.register("iOSExtensionsService", IOSExtensionsService); diff --git a/lib/services/ios-project-service.ts b/lib/services/ios-project-service.ts index 048adffd8b..4a410a1d81 100644 --- a/lib/services/ios-project-service.ts +++ b/lib/services/ios-project-service.ts @@ -52,7 +52,8 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements, private $plistParser: IPlistParser, private $sysInfo: ISysInfo, - private $xcconfigService: IXcconfigService) { + private $xcconfigService: IXcconfigService, + private $iOSExtensionsService: IIOSExtensionsService) { super($fs, $projectDataService); } @@ -490,6 +491,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ } xcode.setAutomaticSigningStyle(projectData.projectName, teamId); + xcode.setAutomaticSigningStyleByTargetProductType("com.apple.product-type.app-extension", teamId); xcode.save(); this.$logger.trace(`Set Automatic signing style and team id ${teamId}.`); @@ -524,13 +526,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ if (!mobileprovision) { this.$errors.failWithoutHelp("Failed to find mobile provision with UUID or Name: " + provision); } - - xcode.setManualSigningStyle(projectData.projectName, { + const configuration = { team: mobileprovision.TeamIdentifier && mobileprovision.TeamIdentifier.length > 0 ? mobileprovision.TeamIdentifier[0] : undefined, uuid: mobileprovision.UUID, name: mobileprovision.Name, identity: mobileprovision.Type === "Development" ? "iPhone Developer" : "iPhone Distribution" - }); + }; + xcode.setManualSigningStyle(projectData.projectName, configuration); + xcode.setManualSigningStyleByTargetProductType("com.apple.product-type.app-extension", configuration); xcode.save(); // this.cache(uuid); @@ -788,6 +791,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f // src folder should not be copied as the pbxproject will have references to its files this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_SOURCE_FOLDER)); + this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_EXTENSION_FOLDER)); this.$fs.deleteDirectory(this.getAppResourcesDestinationDirectoryPath(projectData)); } @@ -934,8 +938,8 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f return project; } - private savePbxProj(project: any, projectData: IProjectData): void { - return this.$fs.writeFile(this.getPbxProjPath(projectData), project.writeSync()); + private savePbxProj(project: any, projectData: IProjectData, omitEmptyValues?: boolean): void { + return this.$fs.writeFile(this.getPbxProjPath(projectData), project.writeSync({omitEmptyValues})); } public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData, opts?: any): Promise { @@ -978,6 +982,10 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f // So the correct order is `pod install` to be executed before merging pod's xcconfig file. await this.$cocoapodsService.mergePodXcconfigFile(projectData, platformData, opts); } + + const pbxProjPath = this.getPbxProjPath(projectData); + this.$iOSExtensionsService.removeExtensions({pbxProjPath}); + await this.addExtensions(projectData); } public beforePrepareAllPlugins(): Promise { return Promise.resolve(); @@ -1093,15 +1101,36 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f this.savePbxProj(project, projectData); } + private async addExtensions(projectData: IProjectData): Promise { + const resorcesExtensionsPath = path.join( + projectData.getAppResourcesDirectoryPath(), + this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_EXTENSION_FOLDER + ); + const platformData = this.getPlatformData(projectData); + const pbxProjPath = this.getPbxProjPath(projectData); + await this.$iOSExtensionsService.addExtensionsFromPath({extensionsFolderPath: resorcesExtensionsPath, projectData, platformData, pbxProjPath}); + const plugins = await this.getAllInstalledPlugins(projectData); + for (const pluginIndex in plugins) { + const pluginData = plugins[pluginIndex]; + const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); + + const extensionPath = path.join(pluginPlatformsFolderPath, constants.NATIVE_EXTENSION_FOLDER); + await this.$iOSExtensionsService.addExtensionsFromPath({extensionsFolderPath: extensionPath, projectData, platformData, pbxProjPath}); + } + } + private getRootGroup(name: string, rootPath: string) { const filePathsArr: string[] = []; const rootGroup: INativeSourceCodeGroup = { name: name, files: filePathsArr, path: rootPath }; - if (this.$fs.exists(rootPath) && !this.$fs.isEmptyDir(rootPath)) { - this.$fs.readDirectory(rootPath).forEach(fileName => { - const filePath = path.join(rootGroup.path, fileName); - filePathsArr.push(filePath); - }); + if (this.$fs.exists(rootPath)) { + const stats = this.$fs.getFsStats(rootPath); + if (stats.isDirectory() && !this.$fs.isEmptyDir(rootPath)) { + this.$fs.readDirectory(rootPath).forEach(fileName => { + const filePath = path.join(rootGroup.path, fileName); + filePathsArr.push(filePath); + }); + } } return rootGroup; diff --git a/test/ios-project-service.ts b/test/ios-project-service.ts index 043bbcd8fc..e209933a38 100644 --- a/test/ios-project-service.ts +++ b/test/ios-project-service.ts @@ -22,7 +22,6 @@ import { DeviceDiscovery } from "../lib/common/mobile/mobile-core/device-discove import { IOSDeviceDiscovery } from "../lib/common/mobile/mobile-core/ios-device-discovery"; import { AndroidDeviceDiscovery } from "../lib/common/mobile/mobile-core/android-device-discovery"; import { PluginVariablesService } from "../lib/services/plugin-variables-service"; -import { PluginsService } from "../lib/services/plugins-service"; import { PluginVariablesHelper } from "../lib/common/plugin-variables-helper"; import { Utils } from "../lib/common/utils"; import { CocoaPodsService } from "../lib/services/cocoapods-service"; @@ -77,7 +76,8 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX projectIdentifiers: { android: "", ios: "" }, projectDir: "", appDirectoryPath: "", - appResourcesDirectoryPath: "" + appResourcesDirectoryPath: "", + getAppResourcesDirectoryPath: () => "" }); projectData.projectDir = temp.mkdirSync("projectDir"); projectData.appDirectoryPath = path.join(projectData.projectDir, "app"); @@ -115,7 +115,9 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX testInjector.register("iosDeviceOperations", {}); testInjector.register("pluginVariablesService", PluginVariablesService); testInjector.register("pluginVariablesHelper", PluginVariablesHelper); - testInjector.register("pluginsService", PluginsService); + testInjector.register("pluginsService", { + getAllInstalledPlugins: (): string[] => [] + }); testInjector.register("androidProcessService", {}); testInjector.register("processService", {}); testInjector.register("sysInfo", { @@ -127,6 +129,8 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX constructor() { /* */ } parseSync() { /* */ } pbxGroupByName() { /* */ } + removeTargetsByProductType() { /* */ } + writeSync() { /* */ } } }); testInjector.register("userSettingsService", { @@ -152,6 +156,10 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX testInjector.register("pacoteService", { extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise => undefined }); + testInjector.register("iOSExtensionsService", { + removeExtensions: () => { /* */ }, + addExtensionsFromPath: () => Promise.resolve() + }); return testInjector; } @@ -1055,7 +1063,9 @@ describe("iOS Project Service Signing", () => { }, setManualSigningStyle(targetName: string, manualSigning: any) { stack.push({ targetName, manualSigning }); - } + }, + setManualSigningStyleByTargetProductType: () => ({}), + setManualSigningStyleByTargetKey: () => ({}) }; }; await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev", teamId: undefined }); @@ -1074,7 +1084,9 @@ describe("iOS Project Service Signing", () => { }, setManualSigningStyle(targetName: string, manualSigning: any) { stack.push({ targetName, manualSigning }); - } + }, + setManualSigningStyleByTargetProductType: () => ({}), + setManualSigningStyleByTargetKey: () => ({}) }; }; await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDist", teamId: undefined }); @@ -1093,7 +1105,9 @@ describe("iOS Project Service Signing", () => { }, setManualSigningStyle(targetName: string, manualSigning: any) { stack.push({ targetName, manualSigning }); - } + }, + setManualSigningStyleByTargetProductType: () => ({}), + setManualSigningStyleByTargetKey: () => ({}) }; }; await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptAdHoc", teamId: undefined }); @@ -1282,6 +1296,8 @@ describe("buildProject", () => { open: () => ({ getSigning: () => ({}), setAutomaticSigningStyle: () => ({}), + setAutomaticSigningStyleByTargetProductType: () => ({}), + setAutomaticSigningStyleByTargetKey: () => ({}), save: () => ({}) }) };