xref: /expo/tools/src/prebuilds/XcodeGen.ts (revision 1627a7bb)
1eeffdb10STomasz Sapetaimport fs from 'fs-extra';
2*d7f57c45SDominik Sokalimport path from 'path';
3eeffdb10STomasz Sapetaimport semver from 'semver';
4eeffdb10STomasz Sapeta
59657025fSTomasz Sapetaimport { Podspec } from '../CocoaPods';
6*d7f57c45SDominik Sokalimport { EXPOTOOLS_DIR, IOS_DIR } from '../Constants';
7*d7f57c45SDominik Sokalimport { arrayize, spawnAsync } from '../Utils';
8eeffdb10STomasz Sapetaimport {
9eeffdb10STomasz Sapeta  ProjectSpec,
10eeffdb10STomasz Sapeta  ProjectSpecDependency,
11eeffdb10STomasz Sapeta  ProjectSpecPlatform,
12eeffdb10STomasz Sapeta  XcodeConfig,
13eeffdb10STomasz Sapeta} from './XcodeGen.types';
14eeffdb10STomasz Sapeta
15eeffdb10STomasz Sapetaconst PODS_DIR = path.join(IOS_DIR, 'Pods');
16eeffdb10STomasz Sapetaconst PODS_PUBLIC_HEADERS_DIR = path.join(PODS_DIR, 'Headers', 'Public');
17815da3b4STomasz Sapetaconst PODS_PRIVATE_HEADERS_DIR = path.join(PODS_DIR, 'Headers', 'Private');
18eeffdb10STomasz Sapetaconst PLATFORMS_MAPPING: Record<string, ProjectSpecPlatform> = {
19eeffdb10STomasz Sapeta  ios: 'iOS',
20eeffdb10STomasz Sapeta  osx: 'macOS',
21eeffdb10STomasz Sapeta  macos: 'macOS',
22eeffdb10STomasz Sapeta  tvos: 'tvOS',
23eeffdb10STomasz Sapeta  watchos: 'watchOS',
24eeffdb10STomasz Sapeta};
25eeffdb10STomasz Sapeta
26eeffdb10STomasz Sapetaexport const INFO_PLIST_FILENAME = 'Info-generated.plist';
27eeffdb10STomasz Sapeta
28eeffdb10STomasz Sapeta/**
29eeffdb10STomasz Sapeta * Generates `.xcodeproj` from given project spec and saves it at given dir.
30eeffdb10STomasz Sapeta */
31eeffdb10STomasz Sapetaexport async function generateXcodeProjectAsync(dir: string, spec: ProjectSpec): Promise<string> {
32eeffdb10STomasz Sapeta  const specPath = path.join(dir, `${spec.name}.spec.json`);
33eeffdb10STomasz Sapeta
34eeffdb10STomasz Sapeta  // Save the spec to the file so `xcodegen` can use it.
35eeffdb10STomasz Sapeta  await fs.outputJSON(specPath, spec, {
36eeffdb10STomasz Sapeta    spaces: 2,
37eeffdb10STomasz Sapeta  });
38eeffdb10STomasz Sapeta
39eeffdb10STomasz Sapeta  // Generate `.xcodeproj` from given spec. The binary is provided by `@expo/xcodegen` package.
40eeffdb10STomasz Sapeta  await spawnAsync('yarn', ['--silent', 'run', 'xcodegen', '--quiet', '--spec', specPath], {
41eeffdb10STomasz Sapeta    cwd: EXPOTOOLS_DIR,
42eeffdb10STomasz Sapeta    stdio: 'inherit',
43eeffdb10STomasz Sapeta  });
44eeffdb10STomasz Sapeta
45eeffdb10STomasz Sapeta  // Remove temporary spec file.
46eeffdb10STomasz Sapeta  await fs.remove(specPath);
47eeffdb10STomasz Sapeta
48eeffdb10STomasz Sapeta  return path.join(dir, `${spec.name}.xcodeproj`);
49eeffdb10STomasz Sapeta}
50eeffdb10STomasz Sapeta
51eeffdb10STomasz Sapeta/**
52eeffdb10STomasz Sapeta * Creates `xcodegen` spec from the podspec. It's very naive, but covers all our cases so far.
53eeffdb10STomasz Sapeta */
54eeffdb10STomasz Sapetaexport async function createSpecFromPodspecAsync(
55eeffdb10STomasz Sapeta  podspec: Podspec,
56eeffdb10STomasz Sapeta  dependencyResolver: (dependencyName: string) => Promise<ProjectSpecDependency | null>
57eeffdb10STomasz Sapeta): Promise<ProjectSpec> {
58eeffdb10STomasz Sapeta  const platforms = Object.keys(podspec.platforms);
59eeffdb10STomasz Sapeta  const deploymentTarget = platforms.reduce((acc, platform) => {
60eeffdb10STomasz Sapeta    acc[PLATFORMS_MAPPING[platform]] = podspec.platforms[platform];
61eeffdb10STomasz Sapeta    return acc;
62eeffdb10STomasz Sapeta  }, {} as Record<ProjectSpecPlatform, string>);
63eeffdb10STomasz Sapeta
64eeffdb10STomasz Sapeta  const dependenciesNames = podspec.dependencies ? Object.keys(podspec.dependencies) : [];
65eeffdb10STomasz Sapeta
66eeffdb10STomasz Sapeta  const dependencies = (
67eeffdb10STomasz Sapeta    await Promise.all(dependenciesNames.map((dependencyName) => dependencyResolver(dependencyName)))
68eeffdb10STomasz Sapeta  ).filter(Boolean) as ProjectSpecDependency[];
69eeffdb10STomasz Sapeta
70eeffdb10STomasz Sapeta  const bundleId = podNameToBundleId(podspec.name);
71eeffdb10STomasz Sapeta
72eeffdb10STomasz Sapeta  return {
73eeffdb10STomasz Sapeta    name: podspec.name,
74eeffdb10STomasz Sapeta    targets: {
75eeffdb10STomasz Sapeta      [podspec.name]: {
76eeffdb10STomasz Sapeta        type: 'framework',
77eeffdb10STomasz Sapeta        platform: platforms.map((platform) => PLATFORMS_MAPPING[platform]),
78eeffdb10STomasz Sapeta        sources: [
79eeffdb10STomasz Sapeta          {
80eeffdb10STomasz Sapeta            path: '',
81eeffdb10STomasz Sapeta            name: podspec.name,
82eeffdb10STomasz Sapeta            createIntermediateGroups: true,
83eeffdb10STomasz Sapeta            includes: arrayize(podspec.source_files),
84eeffdb10STomasz Sapeta            excludes: [
85eeffdb10STomasz Sapeta              INFO_PLIST_FILENAME,
86eeffdb10STomasz Sapeta              `${podspec.name}.spec.json`,
87eeffdb10STomasz Sapeta              '*.xcodeproj',
88eeffdb10STomasz Sapeta              '*.xcframework',
89eeffdb10STomasz Sapeta              '*.podspec',
90eeffdb10STomasz Sapeta              ...arrayize(podspec.exclude_files),
91eeffdb10STomasz Sapeta            ],
92eeffdb10STomasz Sapeta            compilerFlags: podspec.compiler_flags,
93eeffdb10STomasz Sapeta          },
94eeffdb10STomasz Sapeta        ],
95eeffdb10STomasz Sapeta        dependencies: [
96eeffdb10STomasz Sapeta          ...arrayize(podspec.frameworks).map((framework) => ({
97eeffdb10STomasz Sapeta            sdk: `${framework}.framework`,
98eeffdb10STomasz Sapeta          })),
99eeffdb10STomasz Sapeta          ...dependencies,
100eeffdb10STomasz Sapeta        ],
101eeffdb10STomasz Sapeta        settings: {
102eeffdb10STomasz Sapeta          base: mergeXcodeConfigs(podspec.pod_target_xcconfig ?? {}, {
103eeffdb10STomasz Sapeta            MACH_O_TYPE: 'staticlib',
104eeffdb10STomasz Sapeta          }),
105eeffdb10STomasz Sapeta        },
106eeffdb10STomasz Sapeta        info: {
107eeffdb10STomasz Sapeta          path: INFO_PLIST_FILENAME,
108eeffdb10STomasz Sapeta          properties: mergeXcodeConfigs(
109eeffdb10STomasz Sapeta            {
110eeffdb10STomasz Sapeta              CFBundleIdentifier: bundleId,
111eeffdb10STomasz Sapeta              CFBundleName: podspec.name,
112eeffdb10STomasz Sapeta              CFBundleShortVersionString: podspec.version,
113*d7f57c45SDominik Sokal              CFBundleVersion: String(semver.major(podspec.version)),
114eeffdb10STomasz Sapeta            },
115eeffdb10STomasz Sapeta            podspec.info_plist ?? {}
116eeffdb10STomasz Sapeta          ),
117eeffdb10STomasz Sapeta        },
118eeffdb10STomasz Sapeta      },
119eeffdb10STomasz Sapeta    },
120eeffdb10STomasz Sapeta    options: {
121eeffdb10STomasz Sapeta      minimumXcodeGenVersion: '2.18.0',
122eeffdb10STomasz Sapeta      deploymentTarget,
123eeffdb10STomasz Sapeta    },
124eeffdb10STomasz Sapeta    settings: {
125eeffdb10STomasz Sapeta      base: {
126eeffdb10STomasz Sapeta        PRODUCT_BUNDLE_IDENTIFIER: bundleId,
127eeffdb10STomasz Sapeta        IPHONEOS_DEPLOYMENT_TARGET: podspec.platforms.ios,
128eeffdb10STomasz Sapeta        FRAMEWORK_SEARCH_PATHS: constructFrameworkSearchPaths(dependencies),
129eeffdb10STomasz Sapeta        HEADER_SEARCH_PATHS: constructHeaderSearchPaths(dependenciesNames),
130eeffdb10STomasz Sapeta
131eeffdb10STomasz Sapeta        // Suppresses deprecation warnings coming from frameworks like OpenGLES.
132eeffdb10STomasz Sapeta        VALIDATE_WORKSPACE_SKIPPED_SDK_FRAMEWORKS: arrayize(podspec.frameworks).join(' '),
133eeffdb10STomasz Sapeta      },
134eeffdb10STomasz Sapeta    },
135eeffdb10STomasz Sapeta  };
136eeffdb10STomasz Sapeta}
137eeffdb10STomasz Sapeta
138eeffdb10STomasz Sapetafunction constructFrameworkSearchPaths(dependencies: ProjectSpecDependency[]): string {
139eeffdb10STomasz Sapeta  const frameworks = dependencies.filter((dependency) => !!dependency.framework) as {
140eeffdb10STomasz Sapeta    framework: string;
141eeffdb10STomasz Sapeta  }[];
142eeffdb10STomasz Sapeta
143eeffdb10STomasz Sapeta  return (
144eeffdb10STomasz Sapeta    '$(inherited) ' + frameworks.map((dependency) => path.dirname(dependency.framework)).join(' ')
145eeffdb10STomasz Sapeta  ).trim();
146eeffdb10STomasz Sapeta}
147eeffdb10STomasz Sapeta
148eeffdb10STomasz Sapetafunction constructHeaderSearchPaths(dependencies: string[]): string {
149eeffdb10STomasz Sapeta  // A set of pod names to include in header search paths.
150eeffdb10STomasz Sapeta  // For simplicity, we add some more (usually transitive) than direct dependencies.
151eeffdb10STomasz Sapeta  const podsToSearchForHeaders = new Set([
152eeffdb10STomasz Sapeta    // Some pods' have headers at its root level (ZXingObjC and all our modules).
153eeffdb10STomasz Sapeta    // Without this we would have to use `#import <ZXingObjC*.h>` instead of `#import <ZXingObjC/ZXingObjC*.h>`
154eeffdb10STomasz Sapeta    '',
155eeffdb10STomasz Sapeta
156eeffdb10STomasz Sapeta    ...dependencies,
157eeffdb10STomasz Sapeta
158eeffdb10STomasz Sapeta    'DoubleConversion',
159eeffdb10STomasz Sapeta    'React-callinvoker',
160eeffdb10STomasz Sapeta    'React-Core',
161eeffdb10STomasz Sapeta    'React-cxxreact',
162eeffdb10STomasz Sapeta    'React-jsi',
163eeffdb10STomasz Sapeta    'React-jsiexecutor',
164eeffdb10STomasz Sapeta    'React-jsinspector',
165eeffdb10STomasz Sapeta    'Yoga',
166eeffdb10STomasz Sapeta    'glog',
167eeffdb10STomasz Sapeta  ]);
168eeffdb10STomasz Sapeta
169815da3b4STomasz Sapeta  function headerSearchPathsForDir(dir: string): string {
170815da3b4STomasz Sapeta    return [...podsToSearchForHeaders]
171815da3b4STomasz Sapeta      .map((podName) => '"' + path.join(dir, podName) + '"')
172815da3b4STomasz Sapeta      .join(' ');
173815da3b4STomasz Sapeta  }
174815da3b4STomasz Sapeta
175815da3b4STomasz Sapeta  return [
176815da3b4STomasz Sapeta    '$(inherited)',
177815da3b4STomasz Sapeta    headerSearchPathsForDir(PODS_PUBLIC_HEADERS_DIR),
178815da3b4STomasz Sapeta    headerSearchPathsForDir(PODS_PRIVATE_HEADERS_DIR),
179815da3b4STomasz Sapeta  ].join(' ');
180eeffdb10STomasz Sapeta}
181eeffdb10STomasz Sapeta
182eeffdb10STomasz Sapeta/**
183eeffdb10STomasz Sapeta * Merges Xcode config from left to right.
184eeffdb10STomasz Sapeta * Values containing `$(inherited)` are properly taken into account.
185eeffdb10STomasz Sapeta */
186eeffdb10STomasz Sapetafunction mergeXcodeConfigs(...configs: XcodeConfig[]): XcodeConfig {
187eeffdb10STomasz Sapeta  const result: XcodeConfig = {};
188eeffdb10STomasz Sapeta
189eeffdb10STomasz Sapeta  for (const config of configs) {
190eeffdb10STomasz Sapeta    for (const key in config) {
191eeffdb10STomasz Sapeta      const value = config[key];
192eeffdb10STomasz Sapeta      result[key] = mergeXcodeConfigValue(result[key], value);
193eeffdb10STomasz Sapeta    }
194eeffdb10STomasz Sapeta  }
195eeffdb10STomasz Sapeta  return result;
196eeffdb10STomasz Sapeta}
197eeffdb10STomasz Sapeta
198eeffdb10STomasz Sapetafunction mergeXcodeConfigValue(prevValue: string | undefined, nextValue: string): string {
199eeffdb10STomasz Sapeta  if (prevValue && typeof prevValue === 'string' && prevValue.includes('$(inherited)')) {
200eeffdb10STomasz Sapeta    return '$(inherited) ' + (prevValue + ' ' + nextValue).replace(/\\s*$\(inherited\)\s*/g, ' ');
201eeffdb10STomasz Sapeta  }
202eeffdb10STomasz Sapeta  return nextValue;
203eeffdb10STomasz Sapeta}
204eeffdb10STomasz Sapeta
205eeffdb10STomasz Sapeta/**
206eeffdb10STomasz Sapeta * Simple conversion from pod name to framework's bundle identifier.
207eeffdb10STomasz Sapeta */
208eeffdb10STomasz Sapetafunction podNameToBundleId(podName: string): string {
209eeffdb10STomasz Sapeta  return podName
210eeffdb10STomasz Sapeta    .replace(/^UM/, 'unimodules')
211eeffdb10STomasz Sapeta    .replace(/^EX/, 'expo')
212eeffdb10STomasz Sapeta    .replace(/(\_|[^\w\d\.])+/g, '.')
213eeffdb10STomasz Sapeta    .replace(/\.*([A-Z]+)/g, (_, p1) => `.${p1.toLowerCase()}`);
214eeffdb10STomasz Sapeta}
215