1082815dcSEvan Baconimport { ExpoConfig } from '@expo/config-types';
2082815dcSEvan Baconimport assert from 'assert';
3082815dcSEvan Baconimport path from 'path';
4082815dcSEvan Baconimport slugify from 'slugify';
5082815dcSEvan Baconimport xcode, {
6082815dcSEvan Bacon  PBXFile,
7082815dcSEvan Bacon  PBXGroup,
8082815dcSEvan Bacon  PBXNativeTarget,
9082815dcSEvan Bacon  PBXProject,
10082815dcSEvan Bacon  UUID,
11082815dcSEvan Bacon  XCBuildConfiguration,
12082815dcSEvan Bacon  XCConfigurationList,
13082815dcSEvan Bacon  XcodeProject,
14082815dcSEvan Bacon} from 'xcode';
15082815dcSEvan Baconimport pbxFile from 'xcode/lib/pbxFile';
16082815dcSEvan Bacon
17*8a424bebSJames Ideimport { trimQuotes } from './string';
18082815dcSEvan Baconimport { addWarningIOS } from '../../utils/warnings';
19082815dcSEvan Baconimport * as Paths from '../Paths';
20082815dcSEvan Bacon
21082815dcSEvan Baconexport type ProjectSectionEntry = [string, PBXProject];
22082815dcSEvan Bacon
23082815dcSEvan Baconexport type NativeTargetSection = Record<string, PBXNativeTarget>;
24082815dcSEvan Bacon
25082815dcSEvan Baconexport type NativeTargetSectionEntry = [string, PBXNativeTarget];
26082815dcSEvan Bacon
27082815dcSEvan Baconexport type ConfigurationLists = Record<string, XCConfigurationList>;
28082815dcSEvan Bacon
29082815dcSEvan Baconexport type ConfigurationListEntry = [string, XCConfigurationList];
30082815dcSEvan Bacon
31082815dcSEvan Baconexport type ConfigurationSectionEntry = [string, XCBuildConfiguration];
32082815dcSEvan Bacon
33082815dcSEvan Baconexport function getProjectName(projectRoot: string) {
34082815dcSEvan Bacon  const sourceRoot = Paths.getSourceRoot(projectRoot);
35082815dcSEvan Bacon  return path.basename(sourceRoot);
36082815dcSEvan Bacon}
37082815dcSEvan Bacon
38082815dcSEvan Baconexport function resolvePathOrProject(
39082815dcSEvan Bacon  projectRootOrProject: string | XcodeProject
40082815dcSEvan Bacon): XcodeProject | null {
41082815dcSEvan Bacon  if (typeof projectRootOrProject === 'string') {
42082815dcSEvan Bacon    try {
43082815dcSEvan Bacon      return getPbxproj(projectRootOrProject);
44082815dcSEvan Bacon    } catch {
45082815dcSEvan Bacon      return null;
46082815dcSEvan Bacon    }
47082815dcSEvan Bacon  }
48082815dcSEvan Bacon  return projectRootOrProject;
49082815dcSEvan Bacon}
50082815dcSEvan Bacon
51082815dcSEvan Bacon// TODO: come up with a better solution for using app.json expo.name in various places
52082815dcSEvan Baconexport function sanitizedName(name: string) {
53082815dcSEvan Bacon  // Default to the name `app` when every safe character has been sanitized
54082815dcSEvan Bacon  return sanitizedNameForProjects(name) || sanitizedNameForProjects(slugify(name)) || 'app';
55082815dcSEvan Bacon}
56082815dcSEvan Bacon
57082815dcSEvan Baconfunction sanitizedNameForProjects(name: string) {
58082815dcSEvan Bacon  return name
59082815dcSEvan Bacon    .replace(/[\W_]+/g, '')
60082815dcSEvan Bacon    .normalize('NFD')
61082815dcSEvan Bacon    .replace(/[\u0300-\u036f]/g, '');
62082815dcSEvan Bacon}
63082815dcSEvan Bacon
64082815dcSEvan Bacon// TODO: it's silly and kind of fragile that we look at app config to determine
65082815dcSEvan Bacon// the ios project paths. Overall this function needs to be revamped, just a
66082815dcSEvan Bacon// placeholder for now! Make this more robust when we support applying config
67082815dcSEvan Bacon// at any time (currently it's only applied on eject).
68082815dcSEvan Baconexport function getHackyProjectName(projectRoot: string, config: ExpoConfig): string {
69082815dcSEvan Bacon  // Attempt to get the current ios folder name (apply).
70082815dcSEvan Bacon  try {
71082815dcSEvan Bacon    return getProjectName(projectRoot);
72082815dcSEvan Bacon  } catch {
73082815dcSEvan Bacon    // If no iOS project exists then create a new one (eject).
74082815dcSEvan Bacon    const projectName = config.name;
75082815dcSEvan Bacon    assert(projectName, 'Your project needs a name in app.json/app.config.js.');
76082815dcSEvan Bacon    return sanitizedName(projectName);
77082815dcSEvan Bacon  }
78082815dcSEvan Bacon}
79082815dcSEvan Bacon
80082815dcSEvan Baconfunction createProjectFileForGroup({ filepath, group }: { filepath: string; group: PBXGroup }) {
81082815dcSEvan Bacon  const file = new pbxFile(filepath);
82082815dcSEvan Bacon
83082815dcSEvan Bacon  const conflictingFile = group.children.find((child) => child.comment === file.basename);
84082815dcSEvan Bacon  if (conflictingFile) {
85082815dcSEvan Bacon    // This can happen when a file like the GoogleService-Info.plist needs to be added and the eject command is run twice.
86082815dcSEvan Bacon    // Not much we can do here since it might be a conflicting file.
87082815dcSEvan Bacon    return null;
88082815dcSEvan Bacon  }
89082815dcSEvan Bacon  return file;
90082815dcSEvan Bacon}
91082815dcSEvan Bacon
92082815dcSEvan Bacon/**
93082815dcSEvan Bacon * Add a resource file (ex: `SplashScreen.storyboard`, `Images.xcassets`) to an Xcode project.
94082815dcSEvan Bacon * This is akin to creating a new code file in Xcode with `⌘+n`.
95082815dcSEvan Bacon */
96082815dcSEvan Baconexport function addResourceFileToGroup({
97082815dcSEvan Bacon  filepath,
98082815dcSEvan Bacon  groupName,
99082815dcSEvan Bacon  // Should add to `PBXBuildFile Section`
100082815dcSEvan Bacon  isBuildFile,
101082815dcSEvan Bacon  project,
102082815dcSEvan Bacon  verbose,
103082815dcSEvan Bacon  targetUuid,
104082815dcSEvan Bacon}: {
105082815dcSEvan Bacon  filepath: string;
106082815dcSEvan Bacon  groupName: string;
107082815dcSEvan Bacon  isBuildFile?: boolean;
108082815dcSEvan Bacon  project: XcodeProject;
109082815dcSEvan Bacon  verbose?: boolean;
110082815dcSEvan Bacon  targetUuid?: string;
111082815dcSEvan Bacon}): XcodeProject {
112082815dcSEvan Bacon  return addFileToGroupAndLink({
113082815dcSEvan Bacon    filepath,
114082815dcSEvan Bacon    groupName,
115082815dcSEvan Bacon    project,
116082815dcSEvan Bacon    verbose,
117082815dcSEvan Bacon    targetUuid,
118082815dcSEvan Bacon    addFileToProject({ project, file }) {
119082815dcSEvan Bacon      project.addToPbxFileReferenceSection(file);
120082815dcSEvan Bacon      if (isBuildFile) {
121082815dcSEvan Bacon        project.addToPbxBuildFileSection(file);
122082815dcSEvan Bacon      }
123082815dcSEvan Bacon      project.addToPbxResourcesBuildPhase(file);
124082815dcSEvan Bacon    },
125082815dcSEvan Bacon  });
126082815dcSEvan Bacon}
127082815dcSEvan Bacon
128082815dcSEvan Bacon/**
129082815dcSEvan Bacon * Add a build source file (ex: `AppDelegate.m`, `ViewController.swift`) to an Xcode project.
130082815dcSEvan Bacon * This is akin to creating a new code file in Xcode with `⌘+n`.
131082815dcSEvan Bacon */
132082815dcSEvan Baconexport function addBuildSourceFileToGroup({
133082815dcSEvan Bacon  filepath,
134082815dcSEvan Bacon  groupName,
135082815dcSEvan Bacon  project,
136082815dcSEvan Bacon  verbose,
137082815dcSEvan Bacon  targetUuid,
138082815dcSEvan Bacon}: {
139082815dcSEvan Bacon  filepath: string;
140082815dcSEvan Bacon  groupName: string;
141082815dcSEvan Bacon  project: XcodeProject;
142082815dcSEvan Bacon  verbose?: boolean;
143082815dcSEvan Bacon  targetUuid?: string;
144082815dcSEvan Bacon}): XcodeProject {
145082815dcSEvan Bacon  return addFileToGroupAndLink({
146082815dcSEvan Bacon    filepath,
147082815dcSEvan Bacon    groupName,
148082815dcSEvan Bacon    project,
149082815dcSEvan Bacon    verbose,
150082815dcSEvan Bacon    targetUuid,
151082815dcSEvan Bacon    addFileToProject({ project, file }) {
152082815dcSEvan Bacon      project.addToPbxFileReferenceSection(file);
153082815dcSEvan Bacon      project.addToPbxBuildFileSection(file);
154082815dcSEvan Bacon      project.addToPbxSourcesBuildPhase(file);
155082815dcSEvan Bacon    },
156082815dcSEvan Bacon  });
157082815dcSEvan Bacon}
158082815dcSEvan Bacon
159082815dcSEvan Bacon// TODO(brentvatne): I couldn't figure out how to do this with an existing
160082815dcSEvan Bacon// higher level function exposed by the xcode library, but we should find out how to do
161082815dcSEvan Bacon// that and replace this with it
162082815dcSEvan Baconexport function addFileToGroupAndLink({
163082815dcSEvan Bacon  filepath,
164082815dcSEvan Bacon  groupName,
165082815dcSEvan Bacon  project,
166082815dcSEvan Bacon  verbose,
167082815dcSEvan Bacon  addFileToProject,
168082815dcSEvan Bacon  targetUuid,
169082815dcSEvan Bacon}: {
170082815dcSEvan Bacon  filepath: string;
171082815dcSEvan Bacon  groupName: string;
172082815dcSEvan Bacon  project: XcodeProject;
173082815dcSEvan Bacon  verbose?: boolean;
174082815dcSEvan Bacon  targetUuid?: string;
175082815dcSEvan Bacon  addFileToProject: (props: { file: PBXFile; project: XcodeProject }) => void;
176082815dcSEvan Bacon}): XcodeProject {
177082815dcSEvan Bacon  const group = pbxGroupByPathOrAssert(project, groupName);
178082815dcSEvan Bacon
179082815dcSEvan Bacon  const file = createProjectFileForGroup({ filepath, group });
180082815dcSEvan Bacon
181082815dcSEvan Bacon  if (!file) {
182082815dcSEvan Bacon    if (verbose) {
183082815dcSEvan Bacon      // This can happen when a file like the GoogleService-Info.plist needs to be added and the eject command is run twice.
184082815dcSEvan Bacon      // Not much we can do here since it might be a conflicting file.
185082815dcSEvan Bacon      addWarningIOS(
186082815dcSEvan Bacon        'ios-xcode-project',
187082815dcSEvan Bacon        `Skipped adding duplicate file "${filepath}" to PBXGroup named "${groupName}"`
188082815dcSEvan Bacon      );
189082815dcSEvan Bacon    }
190082815dcSEvan Bacon    return project;
191082815dcSEvan Bacon  }
192082815dcSEvan Bacon
193082815dcSEvan Bacon  if (targetUuid != null) {
194082815dcSEvan Bacon    file.target = targetUuid;
195082815dcSEvan Bacon  } else {
196082815dcSEvan Bacon    const applicationNativeTarget = project.getTarget('com.apple.product-type.application');
197082815dcSEvan Bacon    file.target = applicationNativeTarget?.uuid;
198082815dcSEvan Bacon  }
199082815dcSEvan Bacon
200082815dcSEvan Bacon  file.uuid = project.generateUuid();
201082815dcSEvan Bacon  file.fileRef = project.generateUuid();
202082815dcSEvan Bacon
203082815dcSEvan Bacon  addFileToProject({ project, file });
204082815dcSEvan Bacon
205082815dcSEvan Bacon  group.children.push({
206082815dcSEvan Bacon    value: file.fileRef,
207082815dcSEvan Bacon    comment: file.basename,
208082815dcSEvan Bacon  });
209082815dcSEvan Bacon  return project;
210082815dcSEvan Bacon}
211082815dcSEvan Bacon
212082815dcSEvan Baconexport function getApplicationNativeTarget({
213082815dcSEvan Bacon  project,
214082815dcSEvan Bacon  projectName,
215082815dcSEvan Bacon}: {
216082815dcSEvan Bacon  project: XcodeProject;
217082815dcSEvan Bacon  projectName: string;
218082815dcSEvan Bacon}) {
219082815dcSEvan Bacon  const applicationNativeTarget = project.getTarget('com.apple.product-type.application');
220082815dcSEvan Bacon  assert(
221082815dcSEvan Bacon    applicationNativeTarget,
222082815dcSEvan Bacon    `Couldn't locate application PBXNativeTarget in '.xcodeproj' file.`
223082815dcSEvan Bacon  );
224082815dcSEvan Bacon  assert(
225082815dcSEvan Bacon    String(applicationNativeTarget.target.name) === projectName,
226082815dcSEvan Bacon    `Application native target name mismatch. Expected ${projectName}, but found ${applicationNativeTarget.target.name}.`
227082815dcSEvan Bacon  );
228082815dcSEvan Bacon  return applicationNativeTarget;
229082815dcSEvan Bacon}
230082815dcSEvan Bacon
231082815dcSEvan Bacon/**
232082815dcSEvan Bacon * Add a framework to the default app native target.
233082815dcSEvan Bacon *
234082815dcSEvan Bacon * @param projectName Name of the PBX project.
235082815dcSEvan Bacon * @param framework String ending in `.framework`, i.e. `StoreKit.framework`
236082815dcSEvan Bacon */
237082815dcSEvan Baconexport function addFramework({
238082815dcSEvan Bacon  project,
239082815dcSEvan Bacon  projectName,
240082815dcSEvan Bacon  framework,
241082815dcSEvan Bacon}: {
242082815dcSEvan Bacon  project: XcodeProject;
243082815dcSEvan Bacon  projectName: string;
244082815dcSEvan Bacon  framework: string;
245082815dcSEvan Bacon}) {
246082815dcSEvan Bacon  const target = getApplicationNativeTarget({ project, projectName });
247082815dcSEvan Bacon  return project.addFramework(framework, { target: target.uuid });
248082815dcSEvan Bacon}
249082815dcSEvan Bacon
250082815dcSEvan Baconfunction splitPath(path: string): string[] {
251082815dcSEvan Bacon  // TODO: Should we account for other platforms that may not use `/`
252082815dcSEvan Bacon  return path.split('/');
253082815dcSEvan Bacon}
254082815dcSEvan Bacon
255082815dcSEvan Baconconst findGroup = (
256082815dcSEvan Bacon  group: PBXGroup | undefined,
257082815dcSEvan Bacon  name: string
258082815dcSEvan Bacon):
259082815dcSEvan Bacon  | {
260082815dcSEvan Bacon      value: UUID;
261082815dcSEvan Bacon      comment?: string;
262082815dcSEvan Bacon    }
263082815dcSEvan Bacon  | undefined => {
264082815dcSEvan Bacon  if (!group) {
265082815dcSEvan Bacon    return undefined;
266082815dcSEvan Bacon  }
267082815dcSEvan Bacon
268082815dcSEvan Bacon  return group.children.find((group) => group.comment === name);
269082815dcSEvan Bacon};
270082815dcSEvan Bacon
271082815dcSEvan Baconfunction findGroupInsideGroup(
272082815dcSEvan Bacon  project: XcodeProject,
273082815dcSEvan Bacon  group: PBXGroup | undefined,
274082815dcSEvan Bacon  name: string
275082815dcSEvan Bacon): null | PBXGroup {
276082815dcSEvan Bacon  const foundGroup = findGroup(group, name);
277082815dcSEvan Bacon  if (foundGroup) {
278082815dcSEvan Bacon    return project.getPBXGroupByKey(foundGroup.value) ?? null;
279082815dcSEvan Bacon  }
280082815dcSEvan Bacon  return null;
281082815dcSEvan Bacon}
282082815dcSEvan Bacon
283082815dcSEvan Baconfunction pbxGroupByPathOrAssert(project: XcodeProject, path: string): PBXGroup {
284082815dcSEvan Bacon  const { firstProject } = project.getFirstProject();
285082815dcSEvan Bacon
286082815dcSEvan Bacon  let group = project.getPBXGroupByKey(firstProject.mainGroup);
287082815dcSEvan Bacon
288082815dcSEvan Bacon  const components = splitPath(path);
289082815dcSEvan Bacon  for (const name of components) {
290082815dcSEvan Bacon    const nextGroup = findGroupInsideGroup(project, group, name);
291082815dcSEvan Bacon    if (nextGroup) {
292082815dcSEvan Bacon      group = nextGroup;
293082815dcSEvan Bacon    } else {
294082815dcSEvan Bacon      break;
295082815dcSEvan Bacon    }
296082815dcSEvan Bacon  }
297082815dcSEvan Bacon
298082815dcSEvan Bacon  if (!group) {
299082815dcSEvan Bacon    throw Error(`Xcode PBXGroup with name "${path}" could not be found in the Xcode project.`);
300082815dcSEvan Bacon  }
301082815dcSEvan Bacon
302082815dcSEvan Bacon  return group;
303082815dcSEvan Bacon}
304082815dcSEvan Bacon
305082815dcSEvan Baconexport function ensureGroupRecursively(project: XcodeProject, filepath: string): PBXGroup | null {
306082815dcSEvan Bacon  const components = splitPath(filepath);
307082815dcSEvan Bacon  const hasChild = (group: PBXGroup, name: string) =>
308082815dcSEvan Bacon    group.children.find(({ comment }) => comment === name);
309082815dcSEvan Bacon  const { firstProject } = project.getFirstProject();
310082815dcSEvan Bacon
311082815dcSEvan Bacon  let topMostGroup = project.getPBXGroupByKey(firstProject.mainGroup);
312082815dcSEvan Bacon
313082815dcSEvan Bacon  for (const pathComponent of components) {
314082815dcSEvan Bacon    if (topMostGroup && !hasChild(topMostGroup, pathComponent)) {
315082815dcSEvan Bacon      topMostGroup.children.push({
316082815dcSEvan Bacon        comment: pathComponent,
317082815dcSEvan Bacon        value: project.pbxCreateGroup(pathComponent, '""'),
318082815dcSEvan Bacon      });
319082815dcSEvan Bacon    }
320082815dcSEvan Bacon    topMostGroup = project.pbxGroupByName(pathComponent);
321082815dcSEvan Bacon  }
322082815dcSEvan Bacon  return topMostGroup ?? null;
323082815dcSEvan Bacon}
324082815dcSEvan Bacon
325082815dcSEvan Bacon/**
326082815dcSEvan Bacon * Get the pbxproj for the given path
327082815dcSEvan Bacon */
328082815dcSEvan Baconexport function getPbxproj(projectRoot: string): XcodeProject {
329082815dcSEvan Bacon  const projectPath = Paths.getPBXProjectPath(projectRoot);
330082815dcSEvan Bacon  const project = xcode.project(projectPath);
331082815dcSEvan Bacon  project.parseSync();
332082815dcSEvan Bacon  return project;
333082815dcSEvan Bacon}
334082815dcSEvan Bacon
335082815dcSEvan Bacon/**
336082815dcSEvan Bacon * Get the productName for a project, if the name is using a variable `$(TARGET_NAME)`, then attempt to get the value of that variable.
337082815dcSEvan Bacon *
338082815dcSEvan Bacon * @param project
339082815dcSEvan Bacon */
340082815dcSEvan Baconexport function getProductName(project: XcodeProject): string {
341082815dcSEvan Bacon  let productName = '$(TARGET_NAME)';
342082815dcSEvan Bacon  try {
343082815dcSEvan Bacon    // If the product name is numeric, this will fail (it's a getter).
344082815dcSEvan Bacon    // If the bundle identifier' final component is only numeric values, then the PRODUCT_NAME
345082815dcSEvan Bacon    // will be a numeric value, this results in a bug where the product name isn't useful,
346082815dcSEvan Bacon    // i.e. `com.bacon.001` -> `1` -- in this case, use the first target name.
347082815dcSEvan Bacon    productName = project.productName;
348082815dcSEvan Bacon  } catch {}
349082815dcSEvan Bacon
350082815dcSEvan Bacon  if (productName === '$(TARGET_NAME)') {
351082815dcSEvan Bacon    const targetName = project.getFirstTarget()?.firstTarget?.productName;
352082815dcSEvan Bacon    productName = targetName ?? productName;
353082815dcSEvan Bacon  }
354082815dcSEvan Bacon
355082815dcSEvan Bacon  return productName;
356082815dcSEvan Bacon}
357082815dcSEvan Bacon
358082815dcSEvan Baconexport function getProjectSection(project: XcodeProject) {
359082815dcSEvan Bacon  return project.pbxProjectSection();
360082815dcSEvan Bacon}
361082815dcSEvan Bacon
362082815dcSEvan Baconexport function getXCConfigurationListEntries(project: XcodeProject): ConfigurationListEntry[] {
363082815dcSEvan Bacon  const lists = project.pbxXCConfigurationList();
364082815dcSEvan Bacon  return Object.entries(lists).filter(isNotComment);
365082815dcSEvan Bacon}
366082815dcSEvan Bacon
367082815dcSEvan Baconexport function getBuildConfigurationsForListId(
368082815dcSEvan Bacon  project: XcodeProject,
369082815dcSEvan Bacon  configurationListId: string
370082815dcSEvan Bacon): ConfigurationSectionEntry[] {
371082815dcSEvan Bacon  const configurationListEntries = getXCConfigurationListEntries(project);
372082815dcSEvan Bacon  const [, configurationList] = configurationListEntries.find(
373082815dcSEvan Bacon    ([key]) => key === configurationListId
374082815dcSEvan Bacon  ) as ConfigurationListEntry;
375082815dcSEvan Bacon
376082815dcSEvan Bacon  const buildConfigurations = configurationList.buildConfigurations.map((i) => i.value);
377082815dcSEvan Bacon
378082815dcSEvan Bacon  return Object.entries(project.pbxXCBuildConfigurationSection())
379082815dcSEvan Bacon    .filter(isNotComment)
380082815dcSEvan Bacon    .filter(isBuildConfig)
381082815dcSEvan Bacon    .filter(([key]: ConfigurationSectionEntry) => buildConfigurations.includes(key));
382082815dcSEvan Bacon}
383082815dcSEvan Bacon
384082815dcSEvan Baconexport function getBuildConfigurationForListIdAndName(
385082815dcSEvan Bacon  project: XcodeProject,
386082815dcSEvan Bacon  {
387082815dcSEvan Bacon    configurationListId,
388082815dcSEvan Bacon    buildConfiguration,
389082815dcSEvan Bacon  }: { configurationListId: string; buildConfiguration: string }
390082815dcSEvan Bacon): ConfigurationSectionEntry {
391082815dcSEvan Bacon  const xcBuildConfigurationEntry = getBuildConfigurationsForListId(
392082815dcSEvan Bacon    project,
393082815dcSEvan Bacon    configurationListId
394082815dcSEvan Bacon  ).find((i) => trimQuotes(i[1].name) === buildConfiguration);
395082815dcSEvan Bacon  if (!xcBuildConfigurationEntry) {
396082815dcSEvan Bacon    throw new Error(
397082815dcSEvan Bacon      `Build configuration '${buildConfiguration}' does not exist in list with id '${configurationListId}'`
398082815dcSEvan Bacon    );
399082815dcSEvan Bacon  }
400082815dcSEvan Bacon  return xcBuildConfigurationEntry;
401082815dcSEvan Bacon}
402082815dcSEvan Bacon
403082815dcSEvan Baconexport function isBuildConfig([, sectionItem]: ConfigurationSectionEntry): boolean {
404082815dcSEvan Bacon  return sectionItem.isa === 'XCBuildConfiguration';
405082815dcSEvan Bacon}
406082815dcSEvan Bacon
407082815dcSEvan Baconexport function isNotTestHost([, sectionItem]: ConfigurationSectionEntry): boolean {
408082815dcSEvan Bacon  return !sectionItem.buildSettings.TEST_HOST;
409082815dcSEvan Bacon}
410082815dcSEvan Bacon
411082815dcSEvan Baconexport function isNotComment([key]:
412082815dcSEvan Bacon  | ConfigurationSectionEntry
413082815dcSEvan Bacon  | ProjectSectionEntry
414082815dcSEvan Bacon  | ConfigurationListEntry
415082815dcSEvan Bacon  | NativeTargetSectionEntry): boolean {
416082815dcSEvan Bacon  return !key.endsWith(`_comment`);
417082815dcSEvan Bacon}
418082815dcSEvan Bacon
419082815dcSEvan Bacon// Remove surrounding double quotes if they exist.
420082815dcSEvan Baconexport function unquote(value: string): string {
421082815dcSEvan Bacon  // projects with numeric names will fail due to a bug in the xcode package.
422082815dcSEvan Bacon  if (typeof value === 'number') {
423082815dcSEvan Bacon    value = String(value);
424082815dcSEvan Bacon  }
425082815dcSEvan Bacon  return value.match(/^"(.*)"$/)?.[1] ?? value;
426082815dcSEvan Bacon}
427