UNPKG

nativescript

Version:

Command-line interface for building NativeScript projects

889 lines • 54 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.IOSProjectService = exports.VisionSimulatorPlatformSdkName = exports.VisionDevicePlatformSdkName = exports.SimulatorPlatformSdkName = exports.DevicePlatformSdkName = void 0; const path = require("path"); const shell = require("shelljs"); const _ = require("lodash"); const constants = require("../constants"); const constants_1 = require("../common/constants"); const helpers = require("../common/helpers"); const helpers_1 = require("../common/helpers"); const projectServiceBaseLib = require("./platform-project-service-base"); const plist_merge_patch_1 = require("plist-merge-patch"); const os_1 = require("os"); const plist = require("plist"); const fastGlob = require("fast-glob"); const constants_2 = require("../constants"); const helpers_2 = require("../common/helpers"); const yok_1 = require("../common/yok"); exports.DevicePlatformSdkName = "iphoneos"; exports.SimulatorPlatformSdkName = "iphonesimulator"; exports.VisionDevicePlatformSdkName = "xros"; exports.VisionSimulatorPlatformSdkName = "xrsimulator"; const FRAMEWORK_EXTENSIONS = [".framework", ".xcframework"]; const getPlatformSdkName = (buildData) => { const forDevice = !buildData || buildData.buildForDevice || buildData.buildForAppStore; const isvisionOS = yok_1.injector .resolve("devicePlatformsConstants") .isvisionOS(buildData.platform); if (isvisionOS) { return forDevice ? exports.VisionDevicePlatformSdkName : exports.VisionSimulatorPlatformSdkName; } return forDevice ? exports.DevicePlatformSdkName : exports.SimulatorPlatformSdkName; }; const getConfigurationName = (release) => { return release ? constants_1.Configurations.Release : constants_1.Configurations.Debug; }; class IOSProjectService extends projectServiceBaseLib.PlatformProjectServiceBase { constructor($fs, $options, $childProcess, $cocoapodsService, $errors, $logger, $injector, $projectDataService, $devicePlatformsConstants, $hostInfo, $xcprojService, $iOSProvisionService, $iOSSigningService, $pbxprojDomXcode, $xcode, $iOSEntitlementsService, $platformEnvironmentRequirements, $plistParser, $xcconfigService, $xcodebuildService, $iOSExtensionsService, $iOSWatchAppService, $iOSNativeTargetService, $sysInfo, $tempService, $spmService, $mobileHelper, $projectConfigService) { super($fs, $projectDataService); this.$options = $options; this.$childProcess = $childProcess; this.$cocoapodsService = $cocoapodsService; this.$errors = $errors; this.$logger = $logger; this.$injector = $injector; this.$devicePlatformsConstants = $devicePlatformsConstants; this.$hostInfo = $hostInfo; this.$xcprojService = $xcprojService; this.$iOSProvisionService = $iOSProvisionService; this.$iOSSigningService = $iOSSigningService; this.$pbxprojDomXcode = $pbxprojDomXcode; this.$xcode = $xcode; this.$iOSEntitlementsService = $iOSEntitlementsService; this.$platformEnvironmentRequirements = $platformEnvironmentRequirements; this.$plistParser = $plistParser; this.$xcconfigService = $xcconfigService; this.$xcodebuildService = $xcodebuildService; this.$iOSExtensionsService = $iOSExtensionsService; this.$iOSWatchAppService = $iOSWatchAppService; this.$iOSNativeTargetService = $iOSNativeTargetService; this.$sysInfo = $sysInfo; this.$tempService = $tempService; this.$spmService = $spmService; this.$mobileHelper = $mobileHelper; this.$projectConfigService = $projectConfigService; this._platformsDirCache = null; this._platformData = null; } getPlatformData(projectData) { var _a; if (!projectData && !this._platformData) { throw new Error("First call of getPlatformData without providing projectData."); } if (projectData && projectData.platformsDir && this._platformsDirCache !== projectData.platformsDir) { const platform = this.$mobileHelper.normalizePlatformName((_a = this.$options.platformOverride) !== null && _a !== void 0 ? _a : this.$devicePlatformsConstants.iOS); const projectRoot = this.$options.hostProjectPath ? this.$options.hostProjectPath : path.join(projectData.platformsDir, platform.toLowerCase()); const runtimePackage = this.$projectDataService.getRuntimePackage(projectData.projectDir, platform.toLowerCase()); this._platformData = { frameworkPackageName: runtimePackage.name, normalizedPlatformName: platform, platformNameLowerCase: platform.toLowerCase(), appDestinationDirectoryPath: path.join(projectRoot, projectData.projectName), platformProjectService: this, projectRoot: projectRoot, getBuildOutputPath: (options) => { const config = getConfigurationName(!options || options.release); return path.join(projectRoot, constants.BUILD_DIR, `${config}-${getPlatformSdkName(options)}`); }, getValidBuildOutputData: (buildOptions) => { const forDevice = !buildOptions || !!buildOptions.buildForDevice || !!buildOptions.buildForAppStore; if (forDevice) { const ipaFileName = _.find(this.$fs.readDirectory(this._platformData.getBuildOutputPath(buildOptions)), (entry) => path.extname(entry) === ".ipa"); return { packageNames: [ipaFileName, `${projectData.projectName}.ipa`], }; } return { packageNames: [ `${projectData.projectName}.app`, `${projectData.projectName}.zip`, ], }; }, frameworkDirectoriesExtensions: FRAMEWORK_EXTENSIONS, frameworkDirectoriesNames: [ "Metadata", "metadataGenerator", "NativeScript", "internal", ], targetedOS: ["darwin"], configurationFileName: constants.INFO_PLIST_FILE_NAME, configurationFilePath: path.join(projectRoot, projectData.projectName, projectData.projectName + `-${constants.INFO_PLIST_FILE_NAME}`), relativeToFrameworkConfigurationFilePath: path.join("__PROJECT_NAME__", "__PROJECT_NAME__-Info.plist"), fastLivesyncFileExtensions: [ ".tiff", ".tif", ".jpg", "jpeg", "gif", ".png", ".bmp", ".BMPf", ".ico", ".cur", ".xbm", ], // https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIImage_Class/ }; } return this._platformData; } async validateOptions(projectId, provision, teamId) { if (provision && teamId) { this.$errors.fail("The options --provision and --teamId are mutually exclusive."); } if (provision === true) { await this.$iOSProvisionService.listProvisions(projectId); this.$errors.fail("Please provide provisioning profile uuid or name with the --provision option."); } if (teamId === true) { await this.$iOSProvisionService.listTeams(); this.$errors.fail("Please provide team id or team name with the --teamId options."); } return true; } getAppResourcesDestinationDirectoryPath(projectData) { return path.join(this.getPlatformData(projectData).projectRoot, projectData.projectName, "Resources"); } async validate(projectData, options, notConfiguredEnvOptions) { if (!this.$hostInfo.isDarwin) { return; } const checkEnvironmentRequirementsOutput = await this.$platformEnvironmentRequirements.checkEnvironmentRequirements({ platform: this.getPlatformData(projectData).normalizedPlatformName, projectDir: projectData.projectDir, options, notConfiguredEnvOptions, }); if (checkEnvironmentRequirementsOutput && checkEnvironmentRequirementsOutput.canExecute) { const xcodeWarning = await this.$sysInfo.getXcodeWarning(); if (xcodeWarning) { this.$logger.warn(xcodeWarning); } } return { checkEnvironmentRequirementsOutput, }; } async createProject(frameworkDir, frameworkVersion, projectData) { this.$fs.ensureDirectoryExists(path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER)); shell.cp("-R", path.join(frameworkDir, "*"), this.getPlatformData(projectData).projectRoot); } //TODO: plamen5kov: revisit this method, might have unnecessary/obsolete logic async interpolateData(projectData) { const projectRootFilePath = path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER); // Starting with NativeScript for iOS 1.6.0, the project Info.plist file resides not in the platform project, // but in the hello-world app template as a platform specific resource. if (this.$fs.exists(path.join(projectRootFilePath, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + "-Info.plist"))) { this.replaceFileName("-Info.plist", projectRootFilePath, projectData); } this.replaceFileName("-Prefix.pch", projectRootFilePath, projectData); if (this.$fs.exists(path.join(projectRootFilePath, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + ".entitlements"))) { this.replaceFileName(".entitlements", projectRootFilePath, projectData); } const xcschemeDirPath = path.join(this.getPlatformData(projectData).projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + constants_2.IosProjectConstants.XcodeProjExtName, "xcshareddata/xcschemes"); const xcschemeFilePath = path.join(xcschemeDirPath, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + constants_2.IosProjectConstants.XcodeSchemeExtName); if (this.$fs.exists(xcschemeFilePath)) { this.$logger.trace("Found shared scheme at xcschemeFilePath, renaming to match project name."); this.$logger.trace("Checkpoint 0"); this.replaceFileContent(xcschemeFilePath, projectData); this.$logger.trace("Checkpoint 1"); this.replaceFileName(constants_2.IosProjectConstants.XcodeSchemeExtName, xcschemeDirPath, projectData); this.$logger.trace("Checkpoint 2"); } else { this.$logger.trace("Copying xcscheme from template not found at " + xcschemeFilePath); } this.replaceFileName(constants_2.IosProjectConstants.XcodeProjExtName, this.getPlatformData(projectData).projectRoot, projectData); const pbxprojFilePath = this.getPbxProjPath(projectData); this.replaceFileContent(pbxprojFilePath, projectData); const internalDirPath = path.join(projectRootFilePath, "..", "internal"); const xcframeworksFilePath = path.join(internalDirPath, "XCFrameworks.zip"); if (this.$fs.exists(xcframeworksFilePath)) { await this.$fs.unzip(xcframeworksFilePath, internalDirPath); this.$fs.deleteFile(xcframeworksFilePath); } } interpolateConfigurationFile(projectData) { return undefined; } async cleanProject(projectRoot, projectData) { return null; } afterCreateProject(projectRoot, projectData) { this.$fs.rename(path.join(projectRoot, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER), path.join(projectRoot, projectData.projectName)); } async buildProject(projectRoot, projectData, buildData) { const platformData = this.getPlatformData(projectData); const handler = (data) => { this.emit(constants.BUILD_OUTPUT_EVENT_NAME, data); }; if (buildData.buildForDevice) { await this.$iOSSigningService.setupSigningForDevice(projectRoot, projectData, buildData); await (0, helpers_1.attachAwaitDetach)(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, this.$xcodebuildService.buildForDevice(platformData, projectData, buildData)); } else if (buildData.buildForAppStore) { await (0, helpers_1.attachAwaitDetach)(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, this.$xcodebuildService.buildForAppStore(platformData, projectData, buildData)); } else { await (0, helpers_1.attachAwaitDetach)(constants.BUILD_OUTPUT_EVENT_NAME, this.$childProcess, handler, this.$xcodebuildService.buildForSimulator(platformData, projectData, buildData)); } this.validateApplicationIdentifier(projectData); } isPlatformPrepared(projectRoot, projectData) { return this.$fs.exists(path.join(projectRoot, projectData.projectName, constants.APP_FOLDER_NAME)); } cleanDeviceTempFolder(deviceIdentifier) { return Promise.resolve(); } async isDynamicFramework(frameworkPath) { const isDynamicFrameworkBundle = async (bundlePath, frameworkName) => { const frameworkBinaryPath = path.join(bundlePath, frameworkName); const fileResult = (await this.$childProcess.spawnFromEvent("file", [frameworkBinaryPath], "close")).stdout; const isDynamicallyLinked = _.includes(fileResult, "dynamically linked"); return isDynamicallyLinked; }; if (path.extname(frameworkPath) === ".xcframework") { let isDynamic = true; const plistJson = this.$plistParser.parseFileSync(path.join(frameworkPath, "Info.plist")); for (const library of plistJson.AvailableLibraries) { const singlePlatformFramework = path.join(frameworkPath, library.LibraryIdentifier, library.LibraryPath); if (this.$fs.exists(singlePlatformFramework)) { const frameworkName = path.basename(singlePlatformFramework, path.extname(singlePlatformFramework)); isDynamic = await isDynamicFrameworkBundle(singlePlatformFramework, frameworkName); break; } } return isDynamic; } else { const frameworkName = path.basename(frameworkPath, path.extname(frameworkPath)); return await isDynamicFrameworkBundle(frameworkPath, frameworkName); } } async addFramework(frameworkPath, projectData) { if (this.$hostInfo.isWindows) { return; } this.validateFramework(frameworkPath); const project = this.createPbxProj(projectData); const frameworkAddOptions = { customFramework: true }; const dynamic = await this.isDynamicFramework(frameworkPath); if (dynamic) { frameworkAddOptions["embed"] = true; frameworkAddOptions["sign"] = true; } if (this.$options.hostProjectPath) { // always mark xcframeworks for embedding frameworkAddOptions["embed"] = true; frameworkAddOptions["sign"] = false; } // Note: we used to prepend "$(SRCROOT)/" to the framework path, but seems like it's not needed anymore // "$(SRCROOT)/" + const frameworkRelativePath = this.getLibSubpathRelativeToProjectPath(frameworkPath, projectData); project.addFramework(frameworkRelativePath, frameworkAddOptions); // filePathsArray, buildPhaseType, comment, target, optionsOrFolderType, subfolderPath // project.addBuildPhase( // [], // "PBXShellScriptBuildPhase", // "Debug SRCROOT", // undefined, // { // shellPath: "/bin/sh", // shellScript: `echo "SRCROOT: $SRCROOT"`, // } // ); this.savePbxProj(project, projectData); } async addStaticLibrary(staticLibPath, projectData) { // Copy files to lib folder. const libraryName = path.basename(staticLibPath, ".a"); const headersSubpath = path.join(path.dirname(staticLibPath), "include", libraryName); // Add static library to project file and setup header search paths const project = this.createPbxProj(projectData); const relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(staticLibPath, projectData); project.addFramework(relativeStaticLibPath); const relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath, projectData)); project.addToHeaderSearchPaths({ relativePath: relativeHeaderSearchPath }); this.generateModulemap(headersSubpath, libraryName); this.savePbxProj(project, projectData); } async prepareProject(projectData, prepareData) { const projectRoot = this.$options.hostProjectPath ? this.$options.hostProjectPath : path.join(projectData.platformsDir, this.$devicePlatformsConstants.iOS.toLowerCase()); const platformData = this.getPlatformData(projectData); const pluginsData = this.getAllProductionPlugins(projectData); const pbxProjPath = this.getPbxProjPath(projectData); this.$iOSExtensionsService.removeExtensions({ pbxProjPath }); await this.addExtensions(projectData, pluginsData); const resourcesDirectoryPath = projectData.getAppResourcesDirectoryPath(); const provision = prepareData && prepareData.provision; const teamId = prepareData && prepareData.teamId; if (provision) { await this.$iOSSigningService.setupSigningFromProvision(projectRoot, projectData, provision, prepareData.mobileProvisionData); } if (teamId) { await this.$iOSSigningService.setupSigningFromTeam(projectRoot, projectData, teamId); } const project = this.createPbxProj(projectData); if (this.$options.hostProjectPath) { try { project.addPbxGroup([], "NativeScript", "NativeScript", null, { isMain: true, filesRelativeToProject: true, uuid: "NATIVESCRIPTNATIVESCRIPT", }); /** * 1. Add platforms/ios/{projectname}/app build to the host app */ // Note: allow customization of this targetFolderName // const targetFolderName = "app"; const buildFolderPath = path.join(this.$options.hostProjectPath, projectData.projectName); project.addResourceFile(buildFolderPath, {}, "NATIVESCRIPTNATIVESCRIPT"); // filePathsArray, buildPhaseType, comment, target, optionsOrFolderType, subfolderPath // project.addBuildPhase( // [], // "PBXShellScriptBuildPhase", // "Copy Metadata (DEBUG)", // undefined, // { // shellPath: "/bin/sh", // shellScript: `cp ./platforms/ios/build/Debug-iphonesimulator/metadata-arm64.bin $CONFIGURATION_BUILD_DIR`, // outputPaths: [ // JSON.stringify("$(CONFIGURATION_BUILD_DIR)/metadata-arm64.bin"), // // JSON.stringify("$(CONFIGURATION_BUILD_DIR)/metadata-arm64e.bin"), // // JSON.stringify("$(CONFIGURATION_BUILD_DIR)/metadata-i386.bin"), // // JSON.stringify("$(CONFIGURATION_BUILD_DIR)/metadata-x86_64.bin"), // ], // } // ); const metadataPath = path.relative(this.$options.hostProjectPath, buildFolderPath); project.addToOtherLinkerFlags(JSON.stringify(`-sectcreate __DATA __TNSMetadata "${metadataPath}/metadata-arm64.bin"`)); // // no shorthand way to get UUID of build phase that i can tell // // methods return the phase as an object but ommitted the actual key (uuid we need) // // this does it same way nativescript-dev-xcode does but gets the uuid we need // const resourcesBuildPhaseKeys = Object.keys( // project.hash.project.objects["PBXResourcesBuildPhase"] // ); // // console.log('resourcesBuildPhaseKeys:', resourcesBuildPhaseKeys); // const buildPhaseUUID = resourcesBuildPhaseKeys[0]; // const comment = `${targetFolderName} in Resources`; // project.hash.project.objects["PBXResourcesBuildPhase"][ // buildPhaseUUID // ].files.forEach((f: any) => { // console.log(f); // }); // if ( // !project.hash.project.objects["PBXResourcesBuildPhase"][ // buildPhaseUUID // ].files.find((f: any) => f.comment === comment) // ) { // project.addResourceFile(buildFolderPath, {}, buildPhaseUUID); // } /** * 2. Ensure metadata is copied as a file * The apps metadata-{arch}.bin should be added as a file reference. */ // TODO this.savePbxProj(project, projectData); } catch (err) { this.$logger.trace("Error adding NativeScript group to host project", err); } } const resources = project.pbxGroupByName("Resources"); if (resources && !this.$options.hostProjectPath) { const references = project.pbxFileReferenceSection(); const xcodeProjectImages = _.map(resources.children, (resource) => this.replace(references[resource.value].name)); this.$logger.trace("Images from Xcode project"); this.$logger.trace(xcodeProjectImages); const appResourcesImages = this.$fs.readDirectory(this.getAppResourcesDestinationDirectoryPath(projectData)); this.$logger.trace("Current images from App_Resources"); this.$logger.trace(appResourcesImages); const imagesToAdd = _.difference(appResourcesImages, xcodeProjectImages); this.$logger.trace(`New images to add into xcode project: ${imagesToAdd.join(", ")}`); _.each(imagesToAdd, (image) => project.addResourceFile(path.relative(this.getPlatformData(projectData).projectRoot, path.join(this.getAppResourcesDestinationDirectoryPath(projectData), image)))); const imagesToRemove = _.difference(xcodeProjectImages, appResourcesImages); this.$logger.trace(`Images to remove from xcode project: ${imagesToRemove.join(", ")}`); _.each(imagesToRemove, (image) => project.removeResourceFile(path.join(this.getAppResourcesDestinationDirectoryPath(projectData), image))); this.savePbxProj(project, projectData); let resourcesNativeCodePath = path.join(resourcesDirectoryPath, platformData.normalizedPlatformName, constants.NATIVE_SOURCE_FOLDER); if (!this.$fs.exists(resourcesNativeCodePath)) { resourcesNativeCodePath = path.join(resourcesDirectoryPath, this.$devicePlatformsConstants.iOS, constants.NATIVE_SOURCE_FOLDER); } await this.prepareNativeSourceCode(constants.TNS_NATIVE_SOURCE_GROUP_NAME, resourcesNativeCodePath, projectData); const nativeSource = this.$projectConfigService.getValue(`${this._platformData.platformNameLowerCase}.NativeSource`, []); if (nativeSource === null || nativeSource === void 0 ? void 0 : nativeSource.length) { for (const source of nativeSource) { await this.prepareNativeSourceCode(source.name, source.path, projectData); } } } this.$iOSWatchAppService.removeWatchApp({ pbxProjPath }); const addedWatchApp = await this.$iOSWatchAppService.addWatchAppFromPath({ watchAppFolderPath: path.join(resourcesDirectoryPath, platformData.normalizedPlatformName), projectData, platformData, pbxProjPath, }); if (addedWatchApp) { this.$logger.warn("The support for Apple Watch App is currently in Beta. For more information about the current development state and any known issues, please check the relevant GitHub issue: https://github.com/NativeScript/nativescript-cli/issues/4589"); } } prepareAppResources(projectData) { const platformData = this.getPlatformData(projectData); const projectAppResourcesPath = projectData.getAppResourcesDirectoryPath(projectData.projectDir); const platformsAppResourcesPath = this.getAppResourcesDestinationDirectoryPath(projectData); this.$fs.deleteDirectory(platformsAppResourcesPath); this.$fs.ensureDirectoryExists(platformsAppResourcesPath); const platformAppResourcesPath = path.join(projectAppResourcesPath, platformData.normalizedPlatformName); // this allows App_Resources/visionOS if (this.$fs.exists(platformAppResourcesPath)) { this.$fs.copyFile(path.join(platformAppResourcesPath, "*"), platformsAppResourcesPath); } else { // otherwise falls back to App_Resources/iOS this.$fs.copyFile(path.join(projectAppResourcesPath, this.$devicePlatformsConstants.iOS, "*"), platformsAppResourcesPath); } this.$fs.deleteFile(path.join(platformsAppResourcesPath, platformData.configurationFileName)); this.$fs.deleteFile(path.join(platformsAppResourcesPath, constants.PODFILE_NAME)); this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, constants.NATIVE_SOURCE_FOLDER)); this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, constants.NATIVE_EXTENSION_FOLDER)); this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, "watchapp")); this.$fs.deleteDirectory(path.join(platformsAppResourcesPath, "watchextension")); } async processConfigurationFilesFromAppResources(projectData, opts) { await this.mergeInfoPlists(projectData, opts); await this.$iOSEntitlementsService.merge(projectData); await this.mergeProjectXcconfigFiles(projectData); } ensureConfigurationFileInAppResources() { return null; } async mergeInfoPlists(projectData, buildOptions) { const projectDir = projectData.projectDir; const infoPlistPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, this.getPlatformData(projectData).configurationFileName); this.ensureConfigurationFileInAppResources(); const reporterTraceMessage = "Info.plist:"; const reporter = { log: (txt) => this.$logger.trace(`${reporterTraceMessage} ${txt}`), warn: (txt) => this.$logger.warn(`${reporterTraceMessage} ${txt}`), }; const session = new plist_merge_patch_1.PlistSession(reporter); const makePatch = (plistPath) => { if (!this.$fs.exists(plistPath)) { this.$logger.trace("No plist found at: " + plistPath); return; } this.$logger.trace("Schedule merge plist at: " + plistPath); session.patch({ name: path.relative(projectDir, plistPath), read: () => this.$fs.readText(plistPath), }); }; const allPlugins = this.getAllProductionPlugins(projectData); for (const plugin of allPlugins) { const pluginInfoPlistPath = path.join(plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME), this.getPlatformData(projectData).configurationFileName); makePatch(pluginInfoPlistPath); } makePatch(infoPlistPath); if (projectData.projectIdentifiers && projectData.projectIdentifiers.ios) { session.patch({ name: "CFBundleIdentifier from package.json nativescript.id", read: () => `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleIdentifier</key> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> </dict> </plist>`, }); } if (!buildOptions.release && projectData.projectIdentifiers && projectData.projectIdentifiers.ios) { session.patch({ name: "CFBundleURLTypes from package.json nativescript.id", read: () => `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>${projectData.projectIdentifiers.ios.replace(/[^A-Za-z0-9]/g, "")}</string> </array> </dict> </array> </dict> </plist>`, }); } const plistContent = session.build(); this.$logger.trace("Info.plist: Write to: " + this.getPlatformData(projectData).configurationFilePath); this.$fs.writeFile(this.getPlatformData(projectData).configurationFilePath, plistContent); } getAllProductionPlugins(projectData) { return (this.$injector.resolve("pluginsService")).getAllProductionPlugins(projectData, this.getPlatformData(projectData).platformNameLowerCase); } replace(name) { if (_.startsWith(name, '"')) { name = name.substr(1, name.length - 2); } return name.replace(/\\\"/g, '"'); } getLibSubpathRelativeToProjectPath(targetPath, projectData) { const projectRoot = this.getPlatformData(projectData).projectRoot; const frameworkPath = path.relative(projectRoot, targetPath); // console.log({ // targetPath, // projectRoot, // frameworkPath, // resolved: path.resolve(projectRoot, frameworkPath), // }); return frameworkPath; } getPbxProjPath(projectData) { if (this.$options.hostProjectPath) { let xcodeProjectPath = this.$xcprojService.findXcodeProject(this.$options.hostProjectPath); if (!xcodeProjectPath) { this.$errors.fail("Xcode project not found at the specified directory"); } return path.join(xcodeProjectPath, "project.pbxproj"); } return path.join(this.$xcprojService.getXcodeprojPath(projectData, this.getPlatformData(projectData).projectRoot), "project.pbxproj"); } createPbxProj(projectData) { const project = new this.$xcode.project(this.getPbxProjPath(projectData)); project.parseSync(); return project; } savePbxProj(project, projectData, omitEmptyValues) { return this.$fs.writeFile(this.getPbxProjPath(projectData), project.writeSync({ omitEmptyValues })); } async preparePluginNativeCode(pluginData, projectData, opts) { const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); const sourcePath = path.join(pluginPlatformsFolderPath, "src"); await this.prepareNativeSourceCode(pluginData.name, sourcePath, projectData); await this.prepareResources(pluginPlatformsFolderPath, pluginData, projectData); await this.prepareFrameworks(pluginPlatformsFolderPath, pluginData, projectData); await this.prepareStaticLibs(pluginPlatformsFolderPath, pluginData, projectData); } async removePluginNativeCode(pluginData, projectData) { const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); this.removeNativeSourceCode(pluginPlatformsFolderPath, pluginData, projectData); this.removeFrameworks(pluginPlatformsFolderPath, pluginData, projectData); this.removeStaticLibs(pluginPlatformsFolderPath, pluginData, projectData); const projectRoot = this.getPlatformData(projectData).projectRoot; this.$cocoapodsService.removePodfileFromProject(pluginData.name, this.$cocoapodsService.getPluginPodfilePath(pluginData), projectData, projectRoot); } async handleNativeDependenciesChange(projectData, opts) { const platformData = this.getPlatformData(projectData); const pluginsData = this.getAllProductionPlugins(projectData); this.setProductBundleIdentifier(projectData); await this.applyPluginsCocoaPods(pluginsData, projectData, platformData); await this.$cocoapodsService.applyPodfileFromAppResources(projectData, platformData); await this.$cocoapodsService.applyPodfileArchExclusions(projectData, platformData); await this.$cocoapodsService.applyPodfileFromExtensions(projectData, platformData); const projectPodfilePath = this.$cocoapodsService.getProjectPodfilePath(platformData.projectRoot); if (this.$fs.exists(projectPodfilePath)) { await this.$cocoapodsService.executePodInstall(platformData.projectRoot, this.$xcprojService.getXcodeprojPath(projectData, platformData.projectRoot)); // The `pod install` command adds a new target to the .pbxproject. This target adds additional build phases to Xcode project. // Some of these phases relies on env variables (like PODS_PODFILE_DIR_PATH or PODS_ROOT). // These variables are produced from merge of pod's xcconfig file and project's xcconfig file. // So the correct order is `pod install` to be executed before merging pod's xcconfig file. await this.$cocoapodsService.mergePodXcconfigFile(projectData, platformData, opts); } const pluginSpmPackages = []; for (const plugin of pluginsData) { if (plugin.fullPath) { const pluginConfigPath = path.join(plugin.fullPath, constants.CONFIG_FILE_NAME_TS); if (this.$fs.exists(pluginConfigPath)) { const config = this.$projectConfigService.readConfig(plugin.fullPath); const packages = _.get(config, `${platformData.platformNameLowerCase}.SPMPackages`, []); if (packages.length) { pluginSpmPackages.push(...packages); } } } } await this.$spmService.applySPMPackages(platformData, projectData, pluginSpmPackages); } beforePrepareAllPlugins(projectData, dependencies) { return Promise.resolve(dependencies); } async checkForChanges(changesInfo, prepareData, projectData) { const { provision, teamId } = prepareData; const hasProvision = provision !== undefined; const hasTeamId = teamId !== undefined; if (hasProvision || hasTeamId) { // Check if the native project's signing is set to the provided provision... const pbxprojPath = this.getPbxProjPath(projectData); if (this.$fs.exists(pbxprojPath)) { const xcode = this.$pbxprojDomXcode.Xcode.open(pbxprojPath); const signing = xcode.getSigning(projectData.projectName); if (hasProvision) { if (signing && signing.style === "Manual") { for (const name in signing.configurations) { const config = signing.configurations[name]; if (config.uuid !== provision && config.name !== provision) { changesInfo.signingChanged = true; break; } } } else { // Specifying provisioning profile requires "Manual" signing style. // If the current signing style was not "Manual" it was probably "Automatic" or, // it was not uniform for the debug and release build configurations. changesInfo.signingChanged = true; } } if (hasTeamId) { if (signing && signing.style === "Automatic") { if (signing.team !== teamId) { const teamIdsForName = await this.$iOSProvisionService.getTeamIdsWithName(teamId); if (!teamIdsForName.some((id) => id === signing.team)) { changesInfo.signingChanged = true; } } } else { // Specifying team id or name requires "Automatic" signing style. // If the current signing style was not "Automatic" it was probably "Manual". changesInfo.signingChanged = true; } } } else { changesInfo.signingChanged = true; } } } getDeploymentTarget(projectData) { const target = this.$xcconfigService.readPropertyValue(this.getBuildXCConfigFilePath(projectData), "IPHONEOS_DEPLOYMENT_TARGET"); return target; } setProductBundleIdentifier(projectData) { const project = this.createPbxProj(projectData); this.$iOSNativeTargetService.setXcodeTargetBuildConfigurationProperties([ { name: "PRODUCT_BUNDLE_IDENTIFIER", value: `"${projectData.projectIdentifiers.ios}"`, }, ], projectData.projectName, project); this.savePbxProj(project, projectData); } getAllLibsForPluginWithFileExtension(pluginData, fileExtension) { const fileExtensions = _.isArray(fileExtension) ? fileExtension : [fileExtension]; const filterCallback = (fileName, pluginPlatformsFolderPath) => fileExtensions.indexOf(path.extname(fileName)) !== -1; return this.getAllNativeLibrariesForPlugin(pluginData, IOSProjectService.IOS_PLATFORM_NAME, filterCallback); } validateFramework(libraryPath) { const infoPlistPath = path.join(libraryPath, constants.INFO_PLIST_FILE_NAME); if (!this.$fs.exists(infoPlistPath)) { this.$errors.fail("The bundle at %s does not contain an Info.plist file.", libraryPath); } const plistJson = this.$plistParser.parseFileSync(infoPlistPath); const packageType = plistJson["CFBundlePackageType"]; if (packageType !== "FMWK" && packageType !== "XFWK") { this.$errors.fail("The bundle at %s does not appear to be a dynamic framework.", libraryPath); } } replaceFileContent(file, projectData) { const fileContent = this.$fs.readText(file); const replacedContent = helpers.stringReplaceAll(fileContent, IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER, projectData.projectName); this.$fs.writeFile(file, replacedContent); } replaceFileName(fileNamePart, fileRootLocation, projectData) { const oldFileName = IOSProjectService.IOS_PROJECT_NAME_PLACEHOLDER + fileNamePart; const newFileName = projectData.projectName + fileNamePart; this.$fs.rename(path.join(fileRootLocation, oldFileName), path.join(fileRootLocation, newFileName)); } async prepareNativeSourceCode(groupName, sourceFolderPath, projectData) { const project = this.createPbxProj(projectData); const group = await this.getRootGroup(groupName, sourceFolderPath); project.addPbxGroup(group.files, group.name, group.path, null, { isMain: true, filesRelativeToProject: true, }); project.addToHeaderSearchPaths(group.path); const headerFiles = this.$fs.exists(sourceFolderPath) ? this.$fs.enumerateFilesInDirectorySync(sourceFolderPath, (file, stat) => stat.isDirectory() || path.extname(file) === ".h") : []; if (headerFiles.length > 0 && !this.$fs.exists(path.join(sourceFolderPath, "module.modulemap"))) { this.$logger.warn(`warning: Directory ${sourceFolderPath} with native iOS source code doesn't contain a modulemap file. Metadata for it will not be generated and it will not be accessible from JavaScript. To learn more see https://docs.nativescript.org/guides/ios-source-code`); } this.savePbxProj(project, projectData); } async addExtensions(projectData, pluginsData) { const resorcesExtensionsPath = path.join(projectData.getAppResourcesDirectoryPath(), this.getPlatformData(projectData).normalizedPlatformName, constants.NATIVE_EXTENSION_FOLDER); const platformData = this.getPlatformData(projectData); const pbxProjPath = this.getPbxProjPath(projectData); const addedExtensionsFromResources = await this.$iOSExtensionsService.addExtensionsFromPath({ extensionsFolderPath: resorcesExtensionsPath, projectData, platformData, pbxProjPath, }); let addedExtensionsFromPlugins = false; for (const pluginIndex in pluginsData) { const pluginData = pluginsData[pluginIndex]; const pluginPlatformsFolderPath = pluginData.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); const extensionPath = path.join(pluginPlatformsFolderPath, constants.NATIVE_EXTENSION_FOLDER); const addedExtensionFromPlugin = await this.$iOSExtensionsService.addExtensionsFromPath({ extensionsFolderPath: extensionPath, projectData, platformData, pbxProjPath, }); addedExtensionsFromPlugins = addedExtensionsFromPlugins || addedExtensionFromPlugin; } if (addedExtensionsFromResources || addedExtensionsFromPlugins) { this.$logger.warn("Let us know if there are other Extension features you'd like! https://github.com/NativeScript/NativeScript/issues"); } } async getRootGroup(name, rootPath) { const filePathsArr = []; const rootGroup = { name: name, files: filePathsArr, path: rootPath, }; if (fastGlob.isDynamicPattern(rootPath)) { const projectRoot = this.$projectDataService.getProjectData().projectDir; const filePaths = await fastGlob(rootPath); for (const filePath of filePaths) { const sourceFilePath = path.normalize(path.join(projectRoot, filePath)); filePathsArr.push(sourceFilePath); } } else { 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; } async prepareResources(pluginPlatformsFolderPath, pluginData, projectData) { const project = this.createPbxProj(projectData); const resourcesPath = path.join(pluginPlatformsFolderPath, "Resources"); if (this.$fs.exists(resourcesPath) && !this.$fs.isEmptyDir(resourcesPath)) { for (const fileName of this.$fs.readDirectory(resourcesPath)) { const filePath = path.join(resourcesPath, fileName); project.addResourceFile(filePath); } } this.savePbxProj(project, projectData); } async prepareFrameworks(pluginPlatformsFolderPath, pluginData, projectData) { for (const fileName of this.getAllLibsForPluginWithFileExtension(pluginData, FRAMEWORK_EXTENSIONS)) { await this.addFramework(path.join(pluginPlatformsFolderPath, fileName), projectData); } } async prepareStaticLibs(pluginPlatformsFolderPath, pluginData, projectData) { for (const fileName of this.getAllLibsForPluginWithFileExtension(pluginData, ".a")) { await this.addStaticLibrary(path.join(pluginPlatformsFolderPath, fileName), projectData); } } async removeNativeSourceCode(pluginPlatformsFolderPath, pluginData, projectData) { const project = this.createPbxProj(projectData); const group = await this.getRootGroup(pluginData.name, pluginPlatformsFolderPath); project.removePbxGroup(group.name, group.path); project.removeFromHeaderSearchPaths(group.path); this.savePbxProj(project, projectData); } removeFrameworks(pluginPlatformsFolderPath, pluginData, projectData) { const project = this.createPbxProj(projectData); _.each(this.getAllLibsForPluginWithFileExtension(pluginData, FRAMEWORK_EXTENSIONS), (fileName) => { const relativeFrameworkPath = this.getLibSubpathRelativeToProjectPath(fileName, projectData); project.removeFramework(relativeFrameworkPath, { customFramework: true, embed: true, }); }); this.savePbxProj(project, projectData); } removeStaticLibs(pluginPlatformsFolderPath, pluginData, projectData) { const project = this.createPbxProj(projectData); _.each(this.getAllLibsForPluginWithFileExtension(pluginData, ".a"), (fileName) => { const staticLibPath = path.join(pluginPlatformsFolderPath, fileName); const relativeStaticLibPath = this.getLibSubpathRelativeToProjectPath(path.basename(staticLibPath), projectData); project.removeFramework(relativeStaticLibPath); const headersSubpath = path.join("include", path.basename(staticLibPath, ".a")); const relativeHeaderSearchPath = path.join(this.getLibSubpathRelativeToProjectPath(headersSubpath, projectData)); project.removeFromHeaderSearchPaths({ relativePath: relativeHeaderSearchPath, }); }); this.savePbxProj(project, projectData); } generateModulemap(headersFolderPath, libraryName) { const headersFilter = (fileName, containingFolderPath) => path.extname(fileName) === ".h" && this.$fs.getFsStats(path.join(containingFolderPath, fileName)).isFile(); const headersFolderContents = this.$fs.readDirectory(headersFolderPath); let headers = _(headersFolderContents) .filter((item) => headersFilter(item, headersFolderPath)) .value(); if (!headers.length) { this.$fs.deleteFile(path.join(headersFolderPath, "module.modulemap")); return; } headers = _.map(headers, (value) => `header "${value}"`); const modulemap = `module ${libraryName} { explicit module ${libraryName} { ${headers.join(" ")} } }`; this.$fs.writeFile(path.join(headersFolderPath, "module.modulemap"), modulemap); } async mergeProjectXcconfigFiles(projectData) { const platformData = this.getPlatformData(projectData); const pluginsXcconfigFilePaths = _.values(this.$xcconfigService.getPluginsXcconfigFilePaths(platformData.projectRoot)); for (const pluginsXcconfigFilePath of pluginsXcconfigFilePaths) { this.$fs.deleteFile(pluginsXcconfigFilePath); } const allPlugins = this.getAllProductionPlugins(projectData); for (const plugin of allPlugins) { const pluginPlatformsFolderPath = plugin.pluginPlatformsFolderPath(IOSProjectService.IOS_PLATFORM_NAME); const pluginXcconfigFilePath = path.join(pluginPlatformsFolderPath, constants_2.BUILD_XCCONFIG_FILE_NAME); if (this.$fs.exists(pluginXcconfigFilePath)) { for (const pluginsXcconfigFilePath of pluginsXcconfigFilePaths) { await this.$xcconfigService.mergeFiles(pluginXcconfigFilePath, pluginsXcconfigFilePath); } } } const appResourcesXcconfigPath = path.join(projectData.appResourcesDirectoryPath, this.getPlatformData(projectData).normalizedPlatformName, constants_2.BUILD_XCCONFIG_FILE_NAME); if (this.$fs.exists(appResourcesXcconfigPath)) { for (const pluginsXcconfigFilePath