Skip to content

Kddimitrov/ios app extensions #4453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 20, 2019
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
19 changes: 19 additions & 0 deletions lib/definitions/project.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
removeExtensions(options: IRemoveExtensionsOptions): void;
}

interface IAddExtensionsFromPathOptions{
extensionsFolderPath: string;
projectData: IProjectData;
platformData: IPlatformData;
pbxProjPath: string;
}

interface IRemoveExtensionsOptions {
pbxProjPath: string
}

interface IRubyFunction {
functionName: string;
functionParameters?: string;
Expand Down
32 changes: 31 additions & 1 deletion lib/definitions/xcode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
3 changes: 2 additions & 1 deletion lib/node/xcode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
92 changes: 92 additions & 0 deletions lib/services/ios-extensions-service.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
51 changes: 40 additions & 11 deletions lib/services/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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}.`);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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<void> {
Expand Down Expand Up @@ -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<void> {
return Promise.resolve();
Expand Down Expand Up @@ -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<void> {
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;
Expand Down
28 changes: 22 additions & 6 deletions test/ios-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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", {
Expand All @@ -127,6 +129,8 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX
constructor() { /* */ }
parseSync() { /* */ }
pbxGroupByName() { /* */ }
removeTargetsByProductType() { /* */ }
writeSync() { /* */ }
}
});
testInjector.register("userSettingsService", {
Expand All @@ -152,6 +156,10 @@ function createTestInjector(projectPath: string, projectName: string, xcode?: IX
testInjector.register("pacoteService", {
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => undefined
});
testInjector.register("iOSExtensionsService", {
removeExtensions: () => { /* */ },
addExtensionsFromPath: () => Promise.resolve()
});
return testInjector;
}

Expand Down Expand Up @@ -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 });
Expand All @@ -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 });
Expand All @@ -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 });
Expand Down Expand Up @@ -1282,6 +1296,8 @@ describe("buildProject", () => {
open: () => ({
getSigning: () => ({}),
setAutomaticSigningStyle: () => ({}),
setAutomaticSigningStyleByTargetProductType: () => ({}),
setAutomaticSigningStyleByTargetKey: () => ({}),
save: () => ({})
})
};
Expand Down