Skip to content

Commit ccfe399

Browse files
authored
Merge pull request #4453 from NativeScript/kddimitrov/ios-app-extensions
Kddimitrov/ios app extensions
2 parents 2b14454 + 4512b0a commit ccfe399

File tree

8 files changed

+208
-19
lines changed

8 files changed

+208
-19
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ $injector.requirePublic("projectService", "./services/project-service");
1212
$injector.require("androidProjectService", "./services/android-project-service");
1313
$injector.require("androidPluginBuildService", "./services/android-plugin-build-service");
1414
$injector.require("iOSEntitlementsService", "./services/ios-entitlements-service");
15+
$injector.require("iOSExtensionsService", "./services/ios-extensions-service");
1516
$injector.require("iOSProjectService", "./services/ios-project-service");
1617
$injector.require("iOSProvisionService", "./services/ios-provision-service");
1718
$injector.require("xcconfigService", "./services/xcconfig-service");

lib/constants.ts

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export const HASHES_FILE_NAME = ".nshashes";
4747
export const TNS_NATIVE_SOURCE_GROUP_NAME = "TNSNativeSource";
4848
export const NATIVE_SOURCE_FOLDER = "src";
4949
export const APPLICATION_RESPONSE_TIMEOUT_SECONDS = 60;
50+
export const NATIVE_EXTENSION_FOLDER = "extensions";
5051

5152
export class PackageVersion {
5253
static NEXT = "next";

lib/definitions/project.d.ts

+19
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,25 @@ interface ICocoaPodsPlatformManager {
566566
replacePlatformRow(podfileContent: string, podfilePath: string): { replacedContent: string, podfilePlatformData: IPodfilePlatformData };
567567
}
568568

569+
/**
570+
* Describes a service used to add and remove iOS extension
571+
*/
572+
interface IIOSExtensionsService {
573+
addExtensionsFromPath(options: IAddExtensionsFromPathOptions): Promise<void>;
574+
removeExtensions(options: IRemoveExtensionsOptions): void;
575+
}
576+
577+
interface IAddExtensionsFromPathOptions{
578+
extensionsFolderPath: string;
579+
projectData: IProjectData;
580+
platformData: IPlatformData;
581+
pbxProjPath: string;
582+
}
583+
584+
interface IRemoveExtensionsOptions {
585+
pbxProjPath: string
586+
}
587+
569588
interface IRubyFunction {
570589
functionName: string;
571590
functionParameters?: string;

lib/definitions/xcode.d.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ declare module "nativescript-dev-xcode" {
1313
parse(callback: () => void): void;
1414
parseSync(): void;
1515

16-
writeSync(): string;
16+
writeSync(options: any): string;
1717

1818
addFramework(filepath: string, options?: Options): void;
1919
removeFramework(filePath: string, options?: Options): void;
@@ -27,5 +27,35 @@ declare module "nativescript-dev-xcode" {
2727
updateBuildProperty(key: string, value: any): void;
2828

2929
pbxXCBuildConfigurationSection(): any;
30+
31+
addTarget(targetName: string, targetType: string, targetPath?: string): target;
32+
addBuildPhase(filePathsArray: string[],
33+
buildPhaseType: string,
34+
comment: string,
35+
target?: string,
36+
optionsOrFolderType?: Object|string,
37+
subfolderPath?: string
38+
): any;
39+
addToBuildSettings(buildSetting: string, value: any, targetUuid?: string): void;
40+
addPbxGroup(
41+
filePathsArray: string[],
42+
name: string,
43+
path: string,
44+
sourceTree: string,
45+
opt: {filesRelativeToProject?: boolean, target?: string, uuid?: string, isMain?: boolean }
46+
): group;
47+
addBuildProperty(prop: string, value: any, build_name?: string, productName?: string): void;
48+
addToHeaderSearchPaths(file: string|Object, productName?: string): void;
49+
removeTargetsByProductType(targetType: string): void
50+
}
51+
52+
class target {
53+
uuid: string;
54+
pbxNativeTarget: {productName: string}
55+
}
56+
57+
class group {
58+
uuid: string;
59+
pbxGroup: Object;
3060
}
3161
}

lib/node/xcode.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import * as xcode from "nativescript-dev-xcode";
33
declare global {
44
type IXcode = typeof xcode;
55
export namespace IXcode {
6-
export type project = typeof xcode.project;
6+
export type target = xcode.target;
7+
export type project = xcode.project;
78
export interface Options extends xcode.Options {} // tslint:disable-line
89
}
910
}
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as path from "path";
2+
3+
export class IOSExtensionsService implements IIOSExtensionsService {
4+
constructor(private $fs: IFileSystem,
5+
private $pbxprojDomXcode: IPbxprojDomXcode,
6+
private $xcode: IXcode) {
7+
}
8+
9+
public async addExtensionsFromPath({extensionsFolderPath, projectData, platformData, pbxProjPath}: IAddExtensionsFromPathOptions): Promise<void> {
10+
const targetUuids: string[] = [];
11+
if (!this.$fs.exists(extensionsFolderPath)) {
12+
return;
13+
}
14+
const project = new this.$xcode.project(pbxProjPath);
15+
project.parseSync();
16+
this.$fs.readDirectory(extensionsFolderPath)
17+
.filter(fileName => {
18+
const filePath = path.join(extensionsFolderPath, fileName);
19+
const stats = this.$fs.getFsStats(filePath);
20+
21+
return stats.isDirectory() && !fileName.startsWith(".");
22+
})
23+
.forEach(extensionFolder => {
24+
const targetUuid = this.addExtensionToProject(extensionsFolderPath, extensionFolder, project, projectData, platformData);
25+
targetUuids.push(targetUuid);
26+
});
27+
28+
this.$fs.writeFile(pbxProjPath, project.writeSync({omitEmptyValues: true}));
29+
this.prepareExtensionSigning(targetUuids, projectData, pbxProjPath);
30+
}
31+
32+
private addExtensionToProject(extensionsFolderPath: string, extensionFolder: string, project: IXcode.project, projectData: IProjectData, platformData: IPlatformData): string {
33+
const extensionPath = path.join(extensionsFolderPath, extensionFolder);
34+
const extensionRelativePath = path.relative(platformData.projectRoot, extensionPath);
35+
const files = this.$fs.readDirectory(extensionPath)
36+
.filter(filePath => !filePath.startsWith("."))
37+
.map(filePath => path.join(extensionPath, filePath));
38+
const target = project.addTarget(extensionFolder, 'app_extension', extensionRelativePath);
39+
project.addBuildPhase([], 'PBXSourcesBuildPhase', 'Sources', target.uuid);
40+
project.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', target.uuid);
41+
project.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid);
42+
43+
const extJsonPath = path.join(extensionsFolderPath, extensionFolder, "extension.json");
44+
if (this.$fs.exists(extJsonPath)) {
45+
const extensionJson = this.$fs.readJson(extJsonPath);
46+
_.forEach(extensionJson.frameworks, framework => {
47+
project.addFramework(
48+
framework,
49+
{ target: target.uuid }
50+
);
51+
});
52+
if (extensionJson.assetcatalogCompilerAppiconName) {
53+
project.addToBuildSettings("ASSETCATALOG_COMPILER_APPICON_NAME", extensionJson.assetcatalogCompilerAppiconName, target.uuid);
54+
}
55+
}
56+
57+
project.addPbxGroup(files, extensionFolder, extensionPath, null, { isMain: true, target: target.uuid, filesRelativeToProject: true });
58+
project.addBuildProperty("PRODUCT_BUNDLE_IDENTIFIER", `${projectData.projectIdentifiers.ios}.${extensionFolder}`, "Debug", extensionFolder);
59+
project.addBuildProperty("PRODUCT_BUNDLE_IDENTIFIER", `${projectData.projectIdentifiers.ios}.${extensionFolder}`, "Release", extensionFolder);
60+
project.addToHeaderSearchPaths(extensionPath, target.pbxNativeTarget.productName);
61+
62+
return target.uuid;
63+
}
64+
65+
private prepareExtensionSigning(targetUuids: string[], projectData:IProjectData, projectPath: string) {
66+
const xcode = this.$pbxprojDomXcode.Xcode.open(projectPath);
67+
const signing = xcode.getSigning(projectData.projectName);
68+
if (signing !== undefined) {
69+
_.forEach(targetUuids, targetUuid => {
70+
if (signing.style === "Automatic") {
71+
xcode.setAutomaticSigningStyleByTargetKey(targetUuid, signing.team);
72+
} else {
73+
for (const config in signing.configurations) {
74+
const signingConfiguration = signing.configurations[config];
75+
xcode.setManualSigningStyleByTargetKey(targetUuid, signingConfiguration);
76+
break;
77+
}
78+
}
79+
});
80+
}
81+
xcode.save();
82+
}
83+
84+
public removeExtensions({pbxProjPath}: IRemoveExtensionsOptions): void {
85+
const project = new this.$xcode.project(pbxProjPath);
86+
project.parseSync();
87+
project.removeTargetsByProductType("com.apple.product-type.app-extension");
88+
this.$fs.writeFile(pbxProjPath, project.writeSync({omitEmptyValues: true}));
89+
}
90+
}
91+
92+
$injector.register("iOSExtensionsService", IOSExtensionsService);

lib/services/ios-project-service.ts

+40-11
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
5252
private $platformEnvironmentRequirements: IPlatformEnvironmentRequirements,
5353
private $plistParser: IPlistParser,
5454
private $sysInfo: ISysInfo,
55-
private $xcconfigService: IXcconfigService) {
55+
private $xcconfigService: IXcconfigService,
56+
private $iOSExtensionsService: IIOSExtensionsService) {
5657
super($fs, $projectDataService);
5758
}
5859

@@ -490,6 +491,7 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
490491
}
491492

492493
xcode.setAutomaticSigningStyle(projectData.projectName, teamId);
494+
xcode.setAutomaticSigningStyleByTargetProductType("com.apple.product-type.app-extension", teamId);
493495
xcode.save();
494496

495497
this.$logger.trace(`Set Automatic signing style and team id ${teamId}.`);
@@ -524,13 +526,14 @@ export class IOSProjectService extends projectServiceBaseLib.PlatformProjectServ
524526
if (!mobileprovision) {
525527
this.$errors.failWithoutHelp("Failed to find mobile provision with UUID or Name: " + provision);
526528
}
527-
528-
xcode.setManualSigningStyle(projectData.projectName, {
529+
const configuration = {
529530
team: mobileprovision.TeamIdentifier && mobileprovision.TeamIdentifier.length > 0 ? mobileprovision.TeamIdentifier[0] : undefined,
530531
uuid: mobileprovision.UUID,
531532
name: mobileprovision.Name,
532533
identity: mobileprovision.Type === "Development" ? "iPhone Developer" : "iPhone Distribution"
533-
});
534+
};
535+
xcode.setManualSigningStyle(projectData.projectName, configuration);
536+
xcode.setManualSigningStyleByTargetProductType("com.apple.product-type.app-extension", configuration);
534537
xcode.save();
535538

536539
// this.cache(uuid);
@@ -788,6 +791,7 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
788791

789792
// src folder should not be copied as the pbxproject will have references to its files
790793
this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_SOURCE_FOLDER));
794+
this.$fs.deleteDirectory(path.join(appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_EXTENSION_FOLDER));
791795

792796
this.$fs.deleteDirectory(this.getAppResourcesDestinationDirectoryPath(projectData));
793797
}
@@ -934,8 +938,8 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
934938
return project;
935939
}
936940

937-
private savePbxProj(project: any, projectData: IProjectData): void {
938-
return this.$fs.writeFile(this.getPbxProjPath(projectData), project.writeSync());
941+
private savePbxProj(project: any, projectData: IProjectData, omitEmptyValues?: boolean): void {
942+
return this.$fs.writeFile(this.getPbxProjPath(projectData), project.writeSync({omitEmptyValues}));
939943
}
940944

941945
public async preparePluginNativeCode(pluginData: IPluginData, projectData: IProjectData, opts?: any): Promise<void> {
@@ -978,6 +982,10 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
978982
// So the correct order is `pod install` to be executed before merging pod's xcconfig file.
979983
await this.$cocoapodsService.mergePodXcconfigFile(projectData, platformData, opts);
980984
}
985+
986+
const pbxProjPath = this.getPbxProjPath(projectData);
987+
this.$iOSExtensionsService.removeExtensions({pbxProjPath});
988+
await this.addExtensions(projectData);
981989
}
982990
public beforePrepareAllPlugins(): Promise<void> {
983991
return Promise.resolve();
@@ -1093,15 +1101,36 @@ We will now place an empty obsolete compatability white screen LauncScreen.xib f
10931101
this.savePbxProj(project, projectData);
10941102
}
10951103

1104+
private async addExtensions(projectData: IProjectData): Promise<void> {
1105+
const resorcesExtensionsPath = path.join(
1106+
projectData.getAppResourcesDirectoryPath(),
1107+
this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_EXTENSION_FOLDER
1108+
);
1109+
const platformData = this.getPlatformData(projectData);
1110+
const pbxProjPath = this.getPbxProjPath(projectData);
1111+
await this.$iOSExtensionsService.addExtensionsFromPath({extensionsFolderPath: resorcesExtensionsPath, projectData, platformData, pbxProjPath});
1112+
const plugins = await this.getAllInstalledPlugins(projectData);
1113+
for (const pluginIndex in plugins) {
1114+
const pluginData = plugins[pluginIndex];
1115+
const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME);
1116+
1117+
const extensionPath = path.join(pluginPlatformsFolderPath, constants.NATIVE_EXTENSION_FOLDER);
1118+
await this.$iOSExtensionsService.addExtensionsFromPath({extensionsFolderPath: extensionPath, projectData, platformData, pbxProjPath});
1119+
}
1120+
}
1121+
10961122
private getRootGroup(name: string, rootPath: string) {
10971123
const filePathsArr: string[] = [];
10981124
const rootGroup: INativeSourceCodeGroup = { name: name, files: filePathsArr, path: rootPath };
10991125

1100-
if (this.$fs.exists(rootPath) && !this.$fs.isEmptyDir(rootPath)) {
1101-
this.$fs.readDirectory(rootPath).forEach(fileName => {
1102-
const filePath = path.join(rootGroup.path, fileName);
1103-
filePathsArr.push(filePath);
1104-
});
1126+
if (this.$fs.exists(rootPath)) {
1127+
const stats = this.$fs.getFsStats(rootPath);
1128+
if (stats.isDirectory() && !this.$fs.isEmptyDir(rootPath)) {
1129+
this.$fs.readDirectory(rootPath).forEach(fileName => {
1130+
const filePath = path.join(rootGroup.path, fileName);
1131+
filePathsArr.push(filePath);
1132+
});
1133+
}
11051134
}
11061135

11071136
return rootGroup;

test/ios-project-service.ts

+22-6
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { DeviceDiscovery } from "../lib/common/mobile/mobile-core/device-discove
2222
import { IOSDeviceDiscovery } from "../lib/common/mobile/mobile-core/ios-device-discovery";
2323
import { AndroidDeviceDiscovery } from "../lib/common/mobile/mobile-core/android-device-discovery";
2424
import { PluginVariablesService } from "../lib/services/plugin-variables-service";
25-
import { PluginsService } from "../lib/services/plugins-service";
2625
import { PluginVariablesHelper } from "../lib/common/plugin-variables-helper";
2726
import { Utils } from "../lib/common/utils";
2827
import { CocoaPodsService } from "../lib/services/cocoapods-service";
@@ -77,7 +76,8 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX
7776
projectIdentifiers: { android: "", ios: "" },
7877
projectDir: "",
7978
appDirectoryPath: "",
80-
appResourcesDirectoryPath: ""
79+
appResourcesDirectoryPath: "",
80+
getAppResourcesDirectoryPath: () => ""
8181
});
8282
projectData.projectDir = temp.mkdirSync("projectDir");
8383
projectData.appDirectoryPath = path.join(projectData.projectDir, "app");
@@ -115,7 +115,9 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX
115115
testInjector.register("iosDeviceOperations", {});
116116
testInjector.register("pluginVariablesService", PluginVariablesService);
117117
testInjector.register("pluginVariablesHelper", PluginVariablesHelper);
118-
testInjector.register("pluginsService", PluginsService);
118+
testInjector.register("pluginsService", {
119+
getAllInstalledPlugins: (): string[] => []
120+
});
119121
testInjector.register("androidProcessService", {});
120122
testInjector.register("processService", {});
121123
testInjector.register("sysInfo", {
@@ -127,6 +129,8 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX
127129
constructor() { /* */ }
128130
parseSync() { /* */ }
129131
pbxGroupByName() { /* */ }
132+
removeTargetsByProductType() { /* */ }
133+
writeSync() { /* */ }
130134
}
131135
});
132136
testInjector.register("userSettingsService", {
@@ -152,6 +156,10 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX
152156
testInjector.register("pacoteService", {
153157
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => undefined
154158
});
159+
testInjector.register("iOSExtensionsService", {
160+
removeExtensions: () => { /* */ },
161+
addExtensionsFromPath: () => Promise.resolve()
162+
});
155163
return testInjector;
156164
}
157165

@@ -1055,7 +1063,9 @@ describe("iOS Project Service Signing", () => {
10551063
},
10561064
setManualSigningStyle(targetName: string, manualSigning: any) {
10571065
stack.push({ targetName, manualSigning });
1058-
}
1066+
},
1067+
setManualSigningStyleByTargetProductType: () => ({}),
1068+
setManualSigningStyleByTargetKey: () => ({})
10591069
};
10601070
};
10611071
await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDev", teamId: undefined });
@@ -1074,7 +1084,9 @@ describe("iOS Project Service Signing", () => {
10741084
},
10751085
setManualSigningStyle(targetName: string, manualSigning: any) {
10761086
stack.push({ targetName, manualSigning });
1077-
}
1087+
},
1088+
setManualSigningStyleByTargetProductType: () => ({}),
1089+
setManualSigningStyleByTargetKey: () => ({})
10781090
};
10791091
};
10801092
await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptDist", teamId: undefined });
@@ -1093,7 +1105,9 @@ describe("iOS Project Service Signing", () => {
10931105
},
10941106
setManualSigningStyle(targetName: string, manualSigning: any) {
10951107
stack.push({ targetName, manualSigning });
1096-
}
1108+
},
1109+
setManualSigningStyleByTargetProductType: () => ({}),
1110+
setManualSigningStyleByTargetKey: () => ({})
10971111
};
10981112
};
10991113
await iOSProjectService.prepareProject(projectData, { sdk: undefined, provision: "NativeScriptAdHoc", teamId: undefined });
@@ -1282,6 +1296,8 @@ describe("buildProject", () => {
12821296
open: () => ({
12831297
getSigning: () => ({}),
12841298
setAutomaticSigningStyle: () => ({}),
1299+
setAutomaticSigningStyleByTargetProductType: () => ({}),
1300+
setAutomaticSigningStyleByTargetKey: () => ({}),
12851301
save: () => ({})
12861302
})
12871303
};

0 commit comments

Comments
 (0)