microsoft/vscode-react-native

Public

mirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
1.5.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

src/extension/ios/plistBuddy.ts

300lines · modecode

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license. See LICENSE file in the project root for details.
3
4import * as path from "path";
5import * as glob from "glob";
6import * as fs from "fs";
7import * as semver from "semver";
8
9import { Node } from "../../common/node/node";
10import { ChildProcess } from "../../common/node/childProcess";
11import { ErrorHelper } from "../../common/error/errorHelper";
12import { InternalErrorCode } from "../../common/error/internalErrorCode";
13import { ProjectVersionHelper } from "../../common/projectVersionHelper";
14import { getFileNameWithoutExtension } from "../../common/utils";
15import customRequire from "../../common/customRequire";
16import { AppLauncher } from "../appLauncher";
17
18export interface ConfigurationData {
19 fullProductName: string;
20 configurationFolder: string;
21}
22
23export class PlistBuddy {
24 private static plistBuddyExecutable = "/usr/libexec/PlistBuddy";
25
26 private readonly TARGET_BUILD_DIR_SEARCH_KEY = "TARGET_BUILD_DIR";
27 private readonly FULL_PRODUCT_NAME_SEARCH_KEY = "FULL_PRODUCT_NAME";
28
29 private nodeChildProcess: ChildProcess;
30
31 constructor({ nodeChildProcess = new Node.ChildProcess() } = {}) {
32 this.nodeChildProcess = nodeChildProcess;
33 }
34
35 public getBundleId(
36 iosProjectRoot: string,
37 projectRoot: string,
38 simulator: boolean = true,
39 configuration: string = "Debug",
40 productName?: string,
41 scheme?: string,
42 ): Promise<string> {
43 return ProjectVersionHelper.getReactNativeVersions(projectRoot).then(rnVersions => {
44 let productsFolder;
45 if (semver.gte(rnVersions.reactNativeVersion, "0.59.0")) {
46 if (!scheme) {
47 // If no scheme were provided via runOptions.scheme or via runArguments then try to get scheme using the way RN CLI does.
48 scheme = this.getInferredScheme(
49 iosProjectRoot,
50 projectRoot,
51 rnVersions.reactNativeVersion,
52 );
53 }
54 productsFolder = path.join(iosProjectRoot, "build", scheme, "Build", "Products");
55 } else {
56 productsFolder = path.join(iosProjectRoot, "build", "Build", "Products");
57 }
58
59 const sdkType = this.getSdkType(simulator, scheme);
60 let configurationFolder = path.join(productsFolder, `${configuration}-${sdkType}`);
61 let executable = "";
62 if (productName) {
63 executable = `${productName}.app`;
64 if (!fs.existsSync(path.join(configurationFolder, executable))) {
65 const configurationData = this.getConfigurationData(
66 projectRoot,
67 rnVersions.reactNativeVersion,
68 iosProjectRoot,
69 configuration,
70 scheme,
71 sdkType,
72 configurationFolder,
73 );
74
75 configurationFolder = configurationData.configurationFolder;
76 }
77 } else {
78 const executableList = this.findExecutable(configurationFolder);
79 if (!executableList.length) {
80 const configurationData = this.getConfigurationData(
81 projectRoot,
82 rnVersions.reactNativeVersion,
83 iosProjectRoot,
84 configuration,
85 scheme,
86 sdkType,
87 configurationFolder,
88 );
89
90 configurationFolder = configurationData.configurationFolder;
91 executableList.push(configurationData.fullProductName);
92 } else if (executableList.length > 1) {
93 throw ErrorHelper.getInternalError(
94 InternalErrorCode.IOSFoundMoreThanOneExecutablesCleanupBuildFolder,
95 configurationFolder,
96 );
97 }
98 executable = `${executableList[0]}`;
99 }
100
101 const infoPlistPath = path.join(configurationFolder, executable, "Info.plist");
102 return this.invokePlistBuddy("Print:CFBundleIdentifier", infoPlistPath);
103 });
104 }
105
106 public setPlistProperty(plistFile: string, property: string, value: string): Promise<void> {
107 // Attempt to set the value, and if it fails due to the key not existing attempt to create the key
108 return this.invokePlistBuddy(`Set ${property} ${value}`, plistFile)
109 .catch(() => this.invokePlistBuddy(`Add ${property} string ${value}`, plistFile))
110 .then(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
111 }
112
113 public setPlistBooleanProperty(
114 plistFile: string,
115 property: string,
116 value: boolean,
117 ): Promise<void> {
118 // Attempt to set the value, and if it fails due to the key not existing attempt to create the key
119 return this.invokePlistBuddy(`Set ${property} ${value}`, plistFile)
120 .catch(() => this.invokePlistBuddy(`Add ${property} bool ${value}`, plistFile))
121 .then(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
122 }
123
124 public deletePlistProperty(plistFile: string, property: string): Promise<void> {
125 return this.invokePlistBuddy(`Delete ${property}`, plistFile)
126 .catch(err => {
127 if (!err.toString().toLowerCase().includes("does not exist")) {
128 throw err;
129 }
130 })
131 .then(() => {}); // eslint-disable-line @typescript-eslint/no-empty-function
132 }
133
134 public readPlistProperty(plistFile: string, property: string): Promise<string> {
135 return this.invokePlistBuddy(`Print ${property}`, plistFile);
136 }
137
138 public getBuildPathAndProductName(
139 iosProjectRoot: string,
140 projectWorkspaceConfigName: string,
141 configuration: string,
142 scheme: string,
143 sdkType: string,
144 ): ConfigurationData {
145 const buildSettings = this.nodeChildProcess.execFileSync(
146 "xcodebuild",
147 [
148 "-workspace",
149 projectWorkspaceConfigName,
150 "-scheme",
151 scheme,
152 "-sdk",
153 sdkType,
154 "-configuration",
155 configuration,
156 "-showBuildSettings",
157 ],
158 {
159 encoding: "utf8",
160 cwd: iosProjectRoot,
161 },
162 );
163
164 const targetBuildDir = this.fetchParameterFromBuildSettings(
165 <string>buildSettings,
166 this.TARGET_BUILD_DIR_SEARCH_KEY,
167 );
168 const fullProductName = this.fetchParameterFromBuildSettings(
169 <string>buildSettings,
170 this.FULL_PRODUCT_NAME_SEARCH_KEY,
171 );
172
173 if (!targetBuildDir) {
174 throw new Error("Failed to get the target build directory.");
175 }
176 if (!fullProductName) {
177 throw new Error("Failed to get full product name.");
178 }
179
180 return {
181 fullProductName,
182 configurationFolder: targetBuildDir,
183 };
184 }
185
186 public getInferredScheme(
187 iosProjectRoot: string,
188 projectRoot: string,
189 rnVersion: string,
190 ): string {
191 const projectWorkspaceConfigName = this.getProjectWorkspaceConfigName(
192 iosProjectRoot,
193 projectRoot,
194 rnVersion,
195 );
196 return getFileNameWithoutExtension(projectWorkspaceConfigName);
197 }
198
199 public getSdkType(simulator: boolean, scheme?: string): string {
200 const sdkSuffix = simulator ? "simulator" : "os";
201 const deviceType =
202 (scheme?.toLowerCase().indexOf("tvos") ?? -1) > -1 ? "appletv" : "iphone";
203 return `${deviceType}${sdkSuffix}`;
204 }
205
206 public getProjectWorkspaceConfigName(
207 iosProjectRoot: string,
208 projectRoot: string,
209 rnVersion: string,
210 ): string {
211 // Portion of code was taken from https://github.com/react-native-community/cli/blob/master/packages/platform-ios/src/commands/runIOS/index.js
212 // and modified a little bit
213 /**
214 * Copyright (c) Facebook, Inc. and its affiliates.
215 *
216 * This source code is licensed under the MIT license found in the
217 * LICENSE file in the root directory of this source tree.
218 *
219 * @flow
220 * @format
221 */
222 let iOSCliFolderName: string;
223 if (semver.gte(rnVersion, "0.60.0")) {
224 iOSCliFolderName = "cli-platform-ios";
225 } else {
226 iOSCliFolderName = "cli";
227 }
228
229 const findXcodeProject = customRequire(
230 path.join(
231 AppLauncher.getNodeModulesRootByProjectPath(projectRoot),
232 `node_modules/@react-native-community/${iOSCliFolderName}/build/commands/runIOS/findXcodeProject`,
233 ),
234 ).default;
235 const xcodeProject = findXcodeProject(fs.readdirSync(iosProjectRoot));
236 if (!xcodeProject) {
237 throw new Error(`Could not find Xcode project files in "${iosProjectRoot}" folder`);
238 }
239
240 return xcodeProject.name;
241 }
242
243 public getConfigurationData(
244 projectRoot: string,
245 reactNativeVersion: string,
246 iosProjectRoot: string,
247 configuration: string,
248 scheme: string | undefined,
249 sdkType: string,
250 oldConfigurationFolder: string,
251 ): ConfigurationData {
252 if (!scheme) {
253 throw ErrorHelper.getInternalError(
254 InternalErrorCode.IOSCouldNotFoundExecutableInFolder,
255 oldConfigurationFolder,
256 );
257 }
258 const projectWorkspaceConfigName = this.getProjectWorkspaceConfigName(
259 iosProjectRoot,
260 projectRoot,
261 reactNativeVersion,
262 );
263 return this.getBuildPathAndProductName(
264 iosProjectRoot,
265 projectWorkspaceConfigName,
266 configuration,
267 scheme,
268 sdkType,
269 );
270 }
271
272 /**
273 * @param {string} buildSettings
274 * @param {string} parameterName
275 * @returns {string | null}
276 */
277 public fetchParameterFromBuildSettings(
278 buildSettings: string,
279 parameterName: string,
280 ): string | null {
281 const targetBuildMatch = new RegExp(`${parameterName} = (.+)$`, "m").exec(buildSettings);
282 return targetBuildMatch && targetBuildMatch[1] ? targetBuildMatch[1].trim() : null;
283 }
284
285 private findExecutable(folder: string): string[] {
286 return glob.sync("*.app", {
287 cwd: folder,
288 });
289 }
290
291 private invokePlistBuddy(command: string, plistFile: string): Promise<string> {
292 return this.nodeChildProcess
293 .exec(`${PlistBuddy.plistBuddyExecutable} -c '${command}' '${plistFile}'`)
294 .then(res =>
295 res.outcome.then((result: string) => {
296 return result.toString().trim();
297 }),
298 );
299 }
300}
301