diff --git a/package.json b/package.json index 3ceb9cd4ebf0..532735d3c480 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,9 @@ "scripts/generate-artifacts.js", "scripts/generate-provider-cli.js", "scripts/generate-specs-cli.js", + "scripts/codegen/codegen-utils.js", + "scripts/codegen/generate-artifacts-executor.js", + "scripts/codegen/generate-specs-cli-executor.js", "scripts/ios-configure-glog.sh", "scripts/xcode/with-environment.sh", "scripts/launchPackager.bat", diff --git a/packages/react-native-codegen/DEFS.bzl b/packages/react-native-codegen/DEFS.bzl index aafc1d2a7a86..6f36f86d87a4 100644 --- a/packages/react-native-codegen/DEFS.bzl +++ b/packages/react-native-codegen/DEFS.bzl @@ -220,14 +220,14 @@ def rn_codegen_modules( # iOS Buck build isn't fully working in OSS, so let's skip it for OSS for now. fb_native.genrule( name = generate_module_hobjcpp_name, - cmd = "cp $(location :{})/{}.h $OUT".format(generate_fixtures_rule_name, name), + cmd = "cp $(location :{})/{}/{}.h $OUT".format(generate_fixtures_rule_name, name, name), out = "{}.h".format(name), labels = ["codegen_rule"], ) fb_native.genrule( name = generate_module_mm_name, - cmd = "cp $(location :{})/{}-generated.mm $OUT".format(generate_fixtures_rule_name, name), + cmd = "cp $(location :{})/{}/{}-generated.mm $OUT".format(generate_fixtures_rule_name, name, name), out = "{}-generated.mm".format(name), labels = ["codegen_rule"], ) diff --git a/packages/react-native-codegen/package.json b/packages/react-native-codegen/package.json index 17c796016add..eeaecf49ea9a 100644 --- a/packages/react-native-codegen/package.json +++ b/packages/react-native-codegen/package.json @@ -1,6 +1,6 @@ { "name": "react-native-codegen", - "version": "0.0.15", + "version": "0.0.16", "description": "⚛️ Code generation tools for React Native", "homepage": "https://github.com/facebook/react-native/tree/HEAD/packages/react-native-codegen", "repository": { diff --git a/packages/react-native-codegen/src/generators/RNCodegen.js b/packages/react-native-codegen/src/generators/RNCodegen.js index 6444b2cfa87a..dd9cd1283958 100644 --- a/packages/react-native-codegen/src/generators/RNCodegen.js +++ b/packages/react-native-codegen/src/generators/RNCodegen.js @@ -134,36 +134,39 @@ const SCHEMAS_GENERATORS = { ], }; -function writeMapToFiles(map: Map, outputDir: string) { +type CodeGenFile = { + name: string, + content: string, + outputDir: string, +}; + +function writeMapToFiles(map: Array) { let success = true; - map.forEach((contents: string, fileName: string) => { + map.forEach(file => { try { - const location = path.join(outputDir, fileName); + const location = path.join(file.outputDir, file.name); const dirName = path.dirname(location); if (!fs.existsSync(dirName)) { fs.mkdirSync(dirName, {recursive: true}); } - fs.writeFileSync(location, contents); + fs.writeFileSync(location, file.content); } catch (error) { success = false; - console.error(`Failed to write ${fileName} to ${outputDir}`, error); + console.error(`Failed to write ${file.name} to ${file.outputDir}`, error); } }); return success; } -function checkFilesForChanges( - map: Map, - outputDir: string, -): boolean { +function checkFilesForChanges(generated: Array): boolean { let hasChanged = false; - map.forEach((contents: string, fileName: string) => { - const location = path.join(outputDir, fileName); + generated.forEach(file => { + const location = path.join(file.outputDir, file.name); const currentContents = fs.readFileSync(location, 'utf8'); - if (currentContents !== contents) { - console.error(`- ${fileName} has changed`); + if (currentContents !== file.content) { + console.error(`- ${file.name} has changed`); hasChanged = true; } @@ -172,6 +175,16 @@ function checkFilesForChanges( return !hasChanged; } +function checkOrWriteFiles( + generatedFiles: Array, + test: void | boolean, +): boolean { + if (test === true) { + return checkFilesForChanges(generatedFiles); + } + return writeMapToFiles(generatedFiles); +} + module.exports = { generate( { @@ -185,22 +198,42 @@ module.exports = { ): boolean { schemaValidator.validate(schema); - const generatedFiles = []; + function composePath(intermediate) { + return path.join(outputDirectory, intermediate, libraryName); + } + + const componentIOSOutput = composePath('react/renderer/components/'); + const modulesIOSOutput = composePath('./'); + + const outputFoldersForGenerators = { + componentsIOS: componentIOSOutput, + modulesIOS: modulesIOSOutput, + descriptors: outputDirectory, + events: outputDirectory, + props: outputDirectory, + componentsAndroid: outputDirectory, + modulesAndroid: outputDirectory, + modulesCxx: outputDirectory, + tests: outputDirectory, + 'shadow-nodes': outputDirectory, + }; + + const generatedFiles: Array = []; + for (const name of generators) { for (const generator of LIBRARY_GENERATORS[name]) { - generatedFiles.push( - ...generator(libraryName, schema, packageName, assumeNonnull), + generator(libraryName, schema, packageName, assumeNonnull).forEach( + (contents: string, fileName: string) => { + generatedFiles.push({ + name: fileName, + content: contents, + outputDir: outputFoldersForGenerators[name], + }); + }, ); } } - - const filesToUpdate = new Map([...generatedFiles]); - - if (test === true) { - return checkFilesForChanges(filesToUpdate, outputDirectory); - } - - return writeMapToFiles(filesToUpdate, outputDirectory); + return checkOrWriteFiles(generatedFiles, test); }, generateFromSchemas( {schemas, outputDirectory}: SchemasOptions, @@ -210,20 +243,20 @@ module.exports = { schemaValidator.validate(schemas[libraryName]), ); - const generatedFiles = []; + const generatedFiles: Array = []; + for (const name of generators) { for (const generator of SCHEMAS_GENERATORS[name]) { - generatedFiles.push(...generator(schemas)); + generator(schemas).forEach((contents: string, fileName: string) => { + generatedFiles.push({ + name: fileName, + content: contents, + outputDir: outputDirectory, + }); + }); } } - - const filesToUpdate = new Map([...generatedFiles]); - - if (test === true) { - return checkFilesForChanges(filesToUpdate, outputDirectory); - } - - return writeMapToFiles(filesToUpdate, outputDirectory); + return checkOrWriteFiles(generatedFiles, test); }, generateViewConfig({libraryName, schema}: LibraryOptions): string { schemaValidator.validate(schema); diff --git a/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js b/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js new file mode 100644 index 000000000000..b86597683933 --- /dev/null +++ b/packages/react-native-codegen/src/generators/__test_fixtures__/fixtures.js @@ -0,0 +1,82 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {SchemaType} from '../../CodegenSchema.js'; + +const SCHEMA_WITH_TM_AND_FC: SchemaType = { + modules: { + ColoredView: { + type: 'Component', + components: { + ColoredView: { + extendsProps: [ + { + type: 'ReactNativeBuiltInType', + knownTypeName: 'ReactNativeCoreViewProps', + }, + ], + events: [], + props: [ + { + name: 'color', + optional: false, + typeAnnotation: { + type: 'StringTypeAnnotation', + default: null, + }, + }, + ], + commands: [], + }, + }, + }, + NativeCalculator: { + type: 'NativeModule', + aliases: {}, + spec: { + properties: [ + { + name: 'add', + optional: false, + typeAnnotation: { + type: 'FunctionTypeAnnotation', + returnTypeAnnotation: { + type: 'PromiseTypeAnnotation', + }, + params: [ + { + name: 'a', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + { + name: 'b', + optional: false, + typeAnnotation: { + type: 'NumberTypeAnnotation', + }, + }, + ], + }, + }, + ], + }, + moduleNames: ['Calculator'], + }, + }, +}; + +module.exports = { + all: SCHEMA_WITH_TM_AND_FC, +}; diff --git a/packages/react-native-codegen/src/generators/__tests__/RNCodegen-test.js b/packages/react-native-codegen/src/generators/__tests__/RNCodegen-test.js new file mode 100644 index 000000000000..4cc51277d1d0 --- /dev/null +++ b/packages/react-native-codegen/src/generators/__tests__/RNCodegen-test.js @@ -0,0 +1,74 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+react_native + * @flow strict-local + * @format + */ + +'use strict'; + +const rnCodegen = require('../RNCodegen.js'); +const fixture = require('../__test_fixtures__/fixtures.js'); +const path = require('path'); + +const outputDirectory = 'tmp/out/'; +const packageName = 'na'; + +describe('RNCodegen.generate', () => { + beforeEach(() => { + jest.resetModules(); + }); + it('when type `all`, with default paths', () => { + const componentsOutputDir = 'react/renderer/components/library'; + const modulesOutputDir = 'library'; + + const expectedPaths = { + 'library.h': modulesOutputDir, + 'library-generated.mm': modulesOutputDir, + 'ShadowNodes.h': componentsOutputDir, + 'ShadowNodes.cpp': componentsOutputDir, + 'Props.h': componentsOutputDir, + 'Props.cpp': componentsOutputDir, + 'RCTComponentViewHelpers.h': componentsOutputDir, + 'EventEmitters.h': componentsOutputDir, + 'EventEmitters.cpp': componentsOutputDir, + 'ComponentDescriptors.h': componentsOutputDir, + }; + + jest.mock('fs', () => ({ + existsSync: location => { + return true; + }, + writeFileSync: (location, content) => { + let receivedDir = path.dirname(location); + let receivedBasename = path.basename(location); + + let expectedPath = path.join( + outputDirectory, + expectedPaths[receivedBasename], + ); + expect(receivedDir).toEqual(expectedPath); + }, + })); + + const res = rnCodegen.generate( + { + libraryName: 'library', + schema: fixture.all, + outputDirectory: outputDirectory, + packageName: packageName, + assumeNonnull: true, + }, + { + generators: ['componentsIOS', 'modulesIOS'], + test: false, + }, + ); + + expect(res).toBeTruthy(); + }); +}); diff --git a/repo-config/package.json b/repo-config/package.json index f227f97ad58b..6d277e3bec6e 100644 --- a/repo-config/package.json +++ b/repo-config/package.json @@ -43,7 +43,7 @@ "mkdirp": "^0.5.1", "prettier": "^2.4.1", "react": "17.0.2", - "react-native-codegen": "^0.0.15", + "react-native-codegen": "^0.0.16", "react-test-renderer": "17.0.2", "shelljs": "^0.8.5", "signedsource": "^1.0.0", diff --git a/scripts/codegen/__test_fixtures__/fixtures.js b/scripts/codegen/__test_fixtures__/fixtures.js new file mode 100644 index 000000000000..ec6e6188440e --- /dev/null +++ b/scripts/codegen/__test_fixtures__/fixtures.js @@ -0,0 +1,87 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+react_native + * @format + */ + +'use-strict'; + +const SCHEMA_TEXT = ` + { + "modules": { + "ColoredView": { + "type": "Component", + "components": { + "ColoredView": { + "extendsProps": [ + { + "type": "ReactNativeBuiltInType", + "knownTypeName": "ReactNativeCoreViewProps" + } + ], + "events": [], + "props": [ + { + "name": "color", + "optional": false, + "typeAnnotation": { + "type": "StringTypeAnnotation", + "default": null + } + } + ], + "commands": [] + } + } + }, + "NativeCalculator": { + "type": "NativeModule", + "aliases": {}, + "spec": { + "properties": [ + { + "name": "add", + "optional": false, + "typeAnnotation": { + "type": "FunctionTypeAnnotation", + "returnTypeAnnotation": { + "type": "PromiseTypeAnnotation" + }, + "params": [ + { + "name": "a", + "optional": false, + "typeAnnotation": { + "type": "NumberTypeAnnotation" + } + }, + { + "name": "b", + "optional": false, + "typeAnnotation": { + "type": "NumberTypeAnnotation" + } + } + ] + } + } + ] + }, + "moduleNames": [ + "Calculator" + ] + } + } +} +`; + +const SCHEMA = JSON.parse(SCHEMA_TEXT); + +module.exports = { + schemaText: SCHEMA_TEXT, + schema: SCHEMA, +}; diff --git a/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/scripts/codegen/__tests__/generate-artifacts-executor-test.js new file mode 100644 index 000000000000..ed106ec820ef --- /dev/null +++ b/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+react_native + * @format + */ + +'use strict'; + +const underTest = require('../generate-artifacts-executor'); +const path = require('path'); + +describe('generateCode', () => { + it('executeNodes with the right arguents', () => { + // Define variables and expected values + const iosOutputDir = 'app/ios/build/generated/ios'; + const library = {config: {name: 'library', type: 'all'}}; + const tmpDir = 'tmp'; + const node = 'usr/bin/node'; + const pathToSchema = 'app/build/schema.json'; + const rnRoot = path.join(__dirname, '../..'); + const libraryType = 'all'; + + const tmpOutDir = path.join(tmpDir, 'out'); + + // mock used functions + let mkdirSyncInvocationCount = 0; + jest.mock('fs', () => ({ + mkdirSync: (location, config) => { + if (mkdirSyncInvocationCount === 0) { + expect(location).toEqual(tmpOutDir); + } + if (mkdirSyncInvocationCount === 1) { + expect(location).toEqual(iosOutputDir); + } + + mkdirSyncInvocationCount += 1; + }, + })); + + let execSyncInvocationCount = 0; + jest.mock('child_process', () => ({ + execSync: command => { + if (execSyncInvocationCount === 0) { + const expectedCommand = `${node} ${path.join( + rnRoot, + 'generate-specs-cli.js', + )} \ + --platform ios \ + --schemaPath ${pathToSchema} \ + --outputDir ${tmpOutDir} \ + --libraryName ${library.config.name} \ + --libraryType ${libraryType}`; + expect(command).toEqual(expectedCommand); + } + + if (execSyncInvocationCount === 1) { + expect(command).toEqual(`cp -R ${tmpOutDir}/* ${iosOutputDir}`); + } + + execSyncInvocationCount += 1; + }, + })); + + underTest._generateCode(iosOutputDir, library, tmpDir, node, pathToSchema); + expect(mkdirSyncInvocationCount).toBe(2); + }); +}); diff --git a/scripts/codegen/__tests__/generate-specs-cli-executor-test.js b/scripts/codegen/__tests__/generate-specs-cli-executor-test.js new file mode 100644 index 000000000000..ce63546a566b --- /dev/null +++ b/scripts/codegen/__tests__/generate-specs-cli-executor-test.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails oncall+react_native + * @format + */ + +'use strict'; + +const sut = require('../generate-specs-cli-executor'); +const fixtures = require('../__test_fixtures__/fixtures'); +const path = require('path'); + +describe('generateSpec', () => { + it('invokes RNCodegen with the right params', () => { + const platform = 'ios'; + const libraryType = 'all'; + const schemaPath = './'; + const componentsOutputDir = path.normalize( + 'app/ios/build/generated/ios/react/renderer/components/library', + ); + const modulesOutputDir = path.normalize( + 'app/ios/build/generated/ios/library', + ); + const outputDirectory = path.normalize('app/ios/build/generated/ios'); + const libraryName = 'library'; + const packageName = 'com.library'; + const generators = ['componentsIOS', 'modulesIOS']; + + jest.mock('fs', () => ({ + readFileSync: (path, encoding) => { + expect(path).toBe(schemaPath); + expect(encoding).toBe('utf-8'); + return fixtures.schemaText; + }, + })); + + let mkdirpSyncInvoked = 0; + jest.mock('mkdirp', () => ({ + sync: folder => { + if (mkdirpSyncInvoked === 0) { + expect(folder).toBe(outputDirectory); + } + + if (mkdirpSyncInvoked === 1) { + expect(folder).toBe(componentsOutputDir); + } + + if (mkdirpSyncInvoked === 2) { + expect(folder).toBe(modulesOutputDir); + } + + mkdirpSyncInvoked += 1; + }, + })); + + // We cannot mock directly the `RNCodegen` object because the + // code access the `lib` folder directly and request a file explicitly. + // This makes testing harder than usually. To overcome this, we created a utility + // to retrieve the `Codegen`. By doing that, we can mock the wrapper so that it returns + // an object with the same interface of the `RNCodegen` object. + jest.mock('../codegen-utils', () => ({ + getCodegen: () => ({ + generate: (libraryConfig, generatorConfigs) => { + expect(libraryConfig.libraryName).toBe(libraryName); + expect(libraryConfig.schema).toStrictEqual(fixtures.schema); + expect(libraryConfig.outputDirectory).toBe(outputDirectory); + expect(libraryConfig.packageName).toBe(packageName); + + expect(generatorConfigs.generators).toStrictEqual(generators); + expect(generatorConfigs.test).toBeUndefined(); + }, + }), + })); + + sut.execute( + platform, + schemaPath, + outputDirectory, + libraryName, + packageName, + libraryType, + ); + + expect(mkdirpSyncInvoked).toBe(3); + }); +}); diff --git a/scripts/codegen/codegen-utils.js b/scripts/codegen/codegen-utils.js new file mode 100644 index 000000000000..469f426bb79b --- /dev/null +++ b/scripts/codegen/codegen-utils.js @@ -0,0 +1,37 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +/** + * Wrapper required to abstract away from the actual codegen. + * This is needed because, when running tests in Sandcastle, not everything is setup as usually. + * For example, the `react-native-codegen` lib is not present. + * + * Thanks to this wrapper, we are able to mock the getter for the codegen in a way that allow us to return + * a custom object which mimics the Codegen interface. + * + * @return an object that can generate the code for the New Architecture. + */ +function getCodegen() { + let RNCodegen; + try { + RNCodegen = require('../../packages/react-native-codegen/lib/generators/RNCodegen.js'); + } catch (e) { + RNCodegen = require('react-native-codegen/lib/generators/RNCodegen.js'); + } + if (!RNCodegen) { + throw 'RNCodegen not found.'; + } + return RNCodegen; +} + +module.exports = { + getCodegen: getCodegen, +}; diff --git a/scripts/codegen/generate-artifacts-executor.js b/scripts/codegen/generate-artifacts-executor.js new file mode 100644 index 000000000000..94278756326e --- /dev/null +++ b/scripts/codegen/generate-artifacts-executor.js @@ -0,0 +1,382 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +/** + * This script crawls through a React Native application's dependencies and invokes the codegen + * for any libraries that require it. + * To enable codegen support, the library should include a config in the codegenConfigKey key + * in a codegenConfigFilename file. + */ + +const {execSync} = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const RN_ROOT = path.join(__dirname, '../..'); + +const CODEGEN_REPO_PATH = `${RN_ROOT}/packages/react-native-codegen`; +const CODEGEN_NPM_PATH = `${RN_ROOT}/../react-native-codegen`; +const CORE_LIBRARIES = new Set(['rncore', 'FBReactNativeSpec']); +const REACT_NATIVE_DEPENDENCY_NAME = 'react-native'; + +// HELPERS + +function isReactNativeCoreLibrary(libraryName) { + return CORE_LIBRARIES.has(libraryName); +} + +function executeNodeScript(node, script) { + execSync(`${node} ${script}`); +} + +function isAppRootValid(appRootDir) { + if (appRootDir == null) { + console.error('Missing path to React Native application'); + process.exitCode = 1; + return false; + } + return true; +} + +function readPackageJSON(appRootDir) { + return JSON.parse(fs.readFileSync(path.join(appRootDir, 'package.json'))); +} + +// Reading Libraries + +function handleReactNativeCodeLibraries( + libraries, + codegenConfigFilename, + codegenConfigKey, +) { + // Handle react-native core libraries. + // This is required when react-native is outside of node_modules. + console.log('[Codegen] Processing react-native core libraries'); + const reactNativePkgJson = path.join(RN_ROOT, codegenConfigFilename); + if (!fs.existsSync(reactNativePkgJson)) { + throw '[Codegen] Error: Could not find config file for react-native.'; + } + const reactNativeConfigFile = JSON.parse(fs.readFileSync(reactNativePkgJson)); + if ( + reactNativeConfigFile[codegenConfigKey] == null || + reactNativeConfigFile[codegenConfigKey].libraries == null + ) { + throw '[Codegen] Error: Could not find codegen config for react-native.'; + } + console.log('[Codegen] Found react-native'); + reactNativeConfigFile[codegenConfigKey].libraries.forEach(config => { + const libraryConfig = { + library: REACT_NATIVE_DEPENDENCY_NAME, + config, + libraryPath: RN_ROOT, + }; + libraries.push(libraryConfig); + }); +} + +function handleThirdPartyLibraries( + libraries, + baseCodegenConfigFileDir, + dependencies, + codegenConfigFilename, + codegenConfigKey, +) { + // Determine which of these are codegen-enabled libraries + const configDir = baseCodegenConfigFileDir || path.join(RN_ROOT, '..'); + console.log( + `\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${configDir}`, + ); + + // Handle third-party libraries + Object.keys(dependencies).forEach(dependency => { + if (dependency === REACT_NATIVE_DEPENDENCY_NAME) { + // react-native should already be added. + return; + } + const codegenConfigFileDir = path.join(configDir, dependency); + const configFilePath = path.join( + codegenConfigFileDir, + codegenConfigFilename, + ); + if (fs.existsSync(configFilePath)) { + const configFile = JSON.parse(fs.readFileSync(configFilePath)); + if ( + configFile[codegenConfigKey] != null && + configFile[codegenConfigKey].libraries != null + ) { + console.log(`[Codegen] Found ${dependency}`); + configFile[codegenConfigKey].libraries.forEach(config => { + const libraryConfig = { + library: dependency, + config, + libraryPath: codegenConfigFileDir, + }; + libraries.push(libraryConfig); + }); + } + } + }); +} + +function handleInAppLibraries( + libraries, + pkgJson, + codegenConfigKey, + appRootDir, +) { + console.log( + '\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in the app', + ); + + // Handle in-app libraries + if ( + pkgJson[codegenConfigKey] != null && + pkgJson[codegenConfigKey].libraries != null + ) { + console.log(`[Codegen] Found ${pkgJson.name}`); + pkgJson[codegenConfigKey].libraries.forEach(config => { + const libraryConfig = { + library: pkgJson.name, + config, + libraryPath: appRootDir, + }; + libraries.push(libraryConfig); + }); + } +} + +// CodeGen + +function getCodeGenCliPath() { + let codegenCliPath; + if (fs.existsSync(CODEGEN_REPO_PATH)) { + codegenCliPath = CODEGEN_REPO_PATH; + + if (!fs.existsSync(path.join(CODEGEN_REPO_PATH, 'lib'))) { + console.log('\n\n[Codegen] >>>>> Building react-native-codegen package'); + execSync('yarn install', { + cwd: codegenCliPath, + stdio: 'inherit', + }); + execSync('yarn build', { + cwd: codegenCliPath, + stdio: 'inherit', + }); + } + } else if (fs.existsSync(CODEGEN_NPM_PATH)) { + codegenCliPath = CODEGEN_NPM_PATH; + } else { + throw "error: Could not determine react-native-codegen location. Try running 'yarn install' or 'npm install' in your project root."; + } + return codegenCliPath; +} + +function computeIOSOutputDir(outputPath, appRootDir) { + return path.join(outputPath ? outputPath : appRootDir, 'build/generated/ios'); +} + +function generateSchema(tmpDir, library, node, codegenCliPath) { + const pathToSchema = path.join(tmpDir, 'schema.json'); + const pathToJavaScriptSources = path.join( + library.libraryPath, + library.config.jsSrcsDir, + ); + + console.log(`\n\n[Codegen] >>>>> Processing ${library.config.name}`); + // Generate one schema for the entire library... + executeNodeScript( + node, + `${path.join( + codegenCliPath, + 'lib', + 'cli', + 'combine', + 'combine-js-to-schema-cli.js', + )} ${pathToSchema} ${pathToJavaScriptSources}`, + ); + console.log(`[Codegen] Generated schema: ${pathToSchema}`); + return pathToSchema; +} + +function generateCode(iosOutputDir, library, tmpDir, node, pathToSchema) { + // ...then generate native code artifacts. + const libraryTypeArg = library.config.type + ? `--libraryType ${library.config.type}` + : ''; + + const tmpOutputDir = path.join(tmpDir, 'out'); + fs.mkdirSync(tmpOutputDir, {recursive: true}); + + executeNodeScript( + node, + `${path.join(RN_ROOT, 'scripts', 'generate-specs-cli.js')} \ + --platform ios \ + --schemaPath ${pathToSchema} \ + --outputDir ${tmpOutputDir} \ + --libraryName ${library.config.name} \ + ${libraryTypeArg}`, + ); + + // Finally, copy artifacts to the final output directory. + fs.mkdirSync(iosOutputDir, {recursive: true}); + execSync(`cp -R ${tmpOutputDir}/* ${iosOutputDir}`); + console.log(`[Codegen] Generated artifacts: ${iosOutputDir}`); +} + +function generateNativeCodegenFiles( + libraries, + fabricEnabled, + iosOutputDir, + node, + codegenCliPath, + schemaPaths, +) { + let fabricEnabledTypes = ['components', 'all']; + libraries.forEach(library => { + if ( + !fabricEnabled && + fabricEnabledTypes.indexOf(library.config.type) >= 0 + ) { + console.log( + `[Codegen] ${library.config.name} skipped because fabric is not enabled.`, + ); + return; + } + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), library.config.name)); + const pathToSchema = generateSchema(tmpDir, library, node, codegenCliPath); + generateCode(iosOutputDir, library, tmpDir, node, pathToSchema); + + // Filter the react native core library out. + // In the future, core library and third party library should + // use the same way to generate/register the fabric components. + if (!isReactNativeCoreLibrary(library.config.name)) { + schemaPaths[library.config.name] = pathToSchema; + } + }); +} + +function createComponentProvider( + fabricEnabled, + schemaPaths, + node, + iosOutputDir, +) { + if (fabricEnabled) { + console.log('\n\n>>>>> Creating component provider'); + // Save the list of spec paths to a temp file. + const schemaListTmpPath = `${os.tmpdir()}/rn-tmp-schema-list.json`; + const fd = fs.openSync(schemaListTmpPath, 'w'); + fs.writeSync(fd, JSON.stringify(schemaPaths)); + fs.closeSync(fd); + console.log(`Generated schema list: ${schemaListTmpPath}`); + + // Generate FabricComponentProvider. + // Only for iOS at this moment. + executeNodeScript( + node, + `${path.join( + RN_ROOT, + 'scripts', + 'generate-provider-cli.js', + )} --platform ios --schemaListPath "${schemaListTmpPath}" --outputDir ${iosOutputDir}`, + ); + console.log(`Generated provider in: ${iosOutputDir}`); + } +} + +// Execute + +/** + * This function is the entry point for the codegen. It: + * - reads the package json + * - extracts the libraries + * - setups the CLI to generate the code + * - generate the code + * + * @parameter appRootDir: the directory with the app source code, where the `codegenConfigFilename` lives. + * @parameter outputPath: the base output path for the CodeGen. + * @parameter node: the path to the node executable, used to run the codegen scripts. + * @parameter codegenConfigFilename: the file that contains the codeGen configuration. The default is `package.json`. + * @parameter codegenConfigKey: the key in the codegenConfigFile that controls the codegen. + * @parameter baseCodegenConfigFileDir: the directory of the codeGenConfigFile. + * @parameter fabricEnabled: whether fabric is enabled or not. + * @throws If it can't find a config file for react-native. + * @throws If it can't find a CodeGen configuration in the file. + * @throws If it can't find a cli for the CodeGen. + */ +function execute( + appRootDir, + outputPath, + node, + codegenConfigFilename, + codegenConfigKey, + baseCodegenConfigFileDir, + fabricEnabled, +) { + if (!isAppRootValid(appRootDir)) { + return; + } + + try { + const pkgJson = readPackageJSON(appRootDir); + const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies}; + const libraries = []; + + handleReactNativeCodeLibraries( + libraries, + codegenConfigFilename, + codegenConfigKey, + ); + handleThirdPartyLibraries( + libraries, + baseCodegenConfigFileDir, + dependencies, + codegenConfigFilename, + codegenConfigKey, + ); + handleInAppLibraries(libraries, pkgJson, codegenConfigKey, appRootDir); + + if (libraries.length === 0) { + console.log('[Codegen] No codegen-enabled libraries found.'); + return; + } + + const codegenCliPath = getCodeGenCliPath(); + + const schemaPaths = {}; + + const iosOutputDir = computeIOSOutputDir(outputPath, appRootDir); + + generateNativeCodegenFiles( + libraries, + fabricEnabled, + iosOutputDir, + node, + codegenCliPath, + schemaPaths, + ); + + createComponentProvider(fabricEnabled, schemaPaths, node, iosOutputDir); + } catch (err) { + console.error(err); + process.exitCode = 1; + } + + console.log('\n\n[Codegen] Done.'); + return; +} + +module.exports = { + execute: execute, + _executeNodeScript: executeNodeScript, // exported for testing purposes only + _generateCode: generateCode, // exported for testing purposes only +}; diff --git a/scripts/codegen/generate-specs-cli-executor.js b/scripts/codegen/generate-specs-cli-executor.js new file mode 100644 index 000000000000..fe37b07fd9db --- /dev/null +++ b/scripts/codegen/generate-specs-cli-executor.js @@ -0,0 +1,127 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +const fs = require('fs'); +const mkdirp = require('mkdirp'); +const path = require('path'); +const utils = require('./codegen-utils'); +const RNCodegen = utils.getCodegen(); + +const GENERATORS = { + all: { + android: ['componentsAndroid', 'modulesAndroid'], + ios: ['componentsIOS', 'modulesIOS'], + }, + components: { + android: ['componentsAndroid'], + ios: ['componentsIOS'], + }, + modules: { + android: ['modulesAndroid'], + ios: ['modulesIOS'], + }, +}; + +function createOutputDirectoryIfNeeded(outputDirectory, libraryName) { + if (!outputDirectory) { + outputDirectory = path.resolve(__dirname, '..', 'Libraries', libraryName); + } + mkdirp.sync(outputDirectory); +} + +function createFolderIfDefined(folder) { + if (folder) { + mkdirp.sync(folder); + } +} + +/** + * This function read a JSON schema from a path and parses it. + * It throws if the schema don't exists or it can't be parsed. + * + * @parameter schemaPath: the path to the schema + * @return a valid schema + * @throw an Error if the schema doesn't exists in a given path or if it can't be parsed. + */ +function readAndParseSchema(schemaPath) { + const schemaText = fs.readFileSync(schemaPath, 'utf-8'); + + if (schemaText == null) { + throw new Error(`Can't find schema at ${schemaPath}`); + } + + try { + return JSON.parse(schemaText); + } catch (err) { + throw new Error(`Can't parse schema to JSON. ${schemaPath}`); + } +} + +function validateLibraryType(libraryType) { + if (GENERATORS[libraryType] == null) { + throw new Error(`Invalid library type. ${libraryType}`); + } +} + +function generateSpec( + platform, + schemaPath, + outputDirectory, + libraryName, + packageName, + libraryType, +) { + validateLibraryType(libraryType); + + let schema = readAndParseSchema(schemaPath); + + createOutputDirectoryIfNeeded(outputDirectory, libraryName); + function composePath(intermediate) { + return path.join(outputDirectory, intermediate, libraryName); + } + + // These are hardcoded and should not be changed. + // The codegen creates some C++ code with #include directive + // which uses these paths. Those directive are not customizable yet. + createFolderIfDefined(composePath('react/renderer/components/')); + createFolderIfDefined(composePath('./')); + + RNCodegen.generate( + { + libraryName, + schema, + outputDirectory, + packageName, + }, + { + generators: GENERATORS[libraryType][platform], + }, + ); + + if (platform === 'android') { + // Move all components C++ files to a structured jni folder for now. + // Note: this should've been done by RNCodegen's generators, but: + // * the generators don't support platform option yet + // * this subdir structure is Android-only, not applicable to iOS + const files = fs.readdirSync(outputDirectory); + const jniOutputDirectory = `${outputDirectory}/jni/react/renderer/components/${libraryName}`; + mkdirp.sync(jniOutputDirectory); + files + .filter(f => f.endsWith('.h') || f.endsWith('.cpp')) + .forEach(f => { + fs.renameSync(`${outputDirectory}/${f}`, `${jniOutputDirectory}/${f}`); + }); + } +} + +module.exports = { + execute: generateSpec, +}; diff --git a/scripts/generate-artifacts.js b/scripts/generate-artifacts.js index 18d3a50c69e4..bde850b940de 100644 --- a/scripts/generate-artifacts.js +++ b/scripts/generate-artifacts.js @@ -9,17 +9,7 @@ 'use strict'; -/** - * This script crawls through a React Native application's dependencies and invokes the codegen - * for any libraries that require it. - * To enable codegen support, the library should include a config in the CODEGEN_CONFIG_KEY key - * in a CODEGEN_CONFIG_FILENAME file. - */ - -const {execSync} = require('child_process'); -const fs = require('fs'); -const os = require('os'); -const path = require('path'); +const executor = require('./codegen/generate-artifacts-executor.js'); const yargs = require('yargs'); const argv = yargs @@ -62,257 +52,21 @@ const argv = yargs .usage('Usage: $0 -p [path to app]') .demandOption(['p']).argv; -const RN_ROOT = path.join(__dirname, '..'); const CODEGEN_CONFIG_FILENAME = argv.f; const CODEGEN_CONFIG_FILE_DIR = argv.c; const CODEGEN_CONFIG_KEY = argv.k; const CODEGEN_FABRIC_ENABLED = argv.e; const NODE = argv.n; -const CODEGEN_REPO_PATH = `${RN_ROOT}/packages/react-native-codegen`; -const CODEGEN_NPM_PATH = `${RN_ROOT}/../react-native-codegen`; -const CORE_LIBRARIES = new Set(['rncore', 'FBReactNativeSpec']); -const REACT_NATIVE_DEPENDENCY_NAME = 'react-native'; - -function isReactNativeCoreLibrary(libraryName) { - return CORE_LIBRARIES.has(libraryName); -} - -function executeNodeScript(script) { - execSync(`${NODE} ${script}`); -} - -function main(appRootDir, outputPath) { - if (appRootDir == null) { - console.error('Missing path to React Native application'); - process.exitCode = 1; - return; - } - - try { - // Get app package.json - const pkgJson = JSON.parse( - fs.readFileSync(path.join(appRootDir, 'package.json')), - ); - - // Get dependencies for the app - const dependencies = {...pkgJson.dependencies, ...pkgJson.devDependencies}; - - const libraries = []; - - // Handle react-native core libraries. - // This is required when react-native is outside of node_modules. - console.log('[Codegen] Processing react-native core libraries'); - const reactNativePkgJson = path.join(RN_ROOT, CODEGEN_CONFIG_FILENAME); - if (!fs.existsSync(reactNativePkgJson)) { - throw '[Codegen] Error: Could not find config file for react-native.'; - } - const reactNativeConfigFile = JSON.parse( - fs.readFileSync(reactNativePkgJson), - ); - if ( - reactNativeConfigFile[CODEGEN_CONFIG_KEY] == null || - reactNativeConfigFile[CODEGEN_CONFIG_KEY].libraries == null - ) { - throw '[Codegen] Error: Could not find codegen config for react-native.'; - } - console.log('[Codegen] Found react-native'); - reactNativeConfigFile[CODEGEN_CONFIG_KEY].libraries.forEach(config => { - const libraryConfig = { - library: REACT_NATIVE_DEPENDENCY_NAME, - config, - libraryPath: RN_ROOT, - }; - libraries.push(libraryConfig); - }); - - // Determine which of these are codegen-enabled libraries - const confifDir = CODEGEN_CONFIG_FILE_DIR || path.join(RN_ROOT, '..'); - console.log( - `\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in ${confifDir}`, - ); - - // Handle third-party libraries - Object.keys(dependencies).forEach(dependency => { - if (dependency === REACT_NATIVE_DEPENDENCY_NAME) { - // react-native should already be added. - return; - } - const codegenConfigFileDir = path.join(confifDir, dependency); - const configFilePath = path.join( - codegenConfigFileDir, - CODEGEN_CONFIG_FILENAME, - ); - if (fs.existsSync(configFilePath)) { - const configFile = JSON.parse(fs.readFileSync(configFilePath)); - if ( - configFile[CODEGEN_CONFIG_KEY] != null && - configFile[CODEGEN_CONFIG_KEY].libraries != null - ) { - console.log(`[Codegen] Found ${dependency}`); - configFile[CODEGEN_CONFIG_KEY].libraries.forEach(config => { - const libraryConfig = { - library: dependency, - config, - libraryPath: codegenConfigFileDir, - }; - libraries.push(libraryConfig); - }); - } - } - }); - - console.log( - '\n\n[Codegen] >>>>> Searching for codegen-enabled libraries in the app', - ); - - // Handle in-app libraries - if ( - pkgJson[CODEGEN_CONFIG_KEY] != null && - pkgJson[CODEGEN_CONFIG_KEY].libraries != null - ) { - console.log(`[Codegen] Found ${pkgJson.name}`); - pkgJson[CODEGEN_CONFIG_KEY].libraries.forEach(config => { - const libraryConfig = { - library: pkgJson.name, - config, - libraryPath: appRootDir, - }; - libraries.push(libraryConfig); - }); - } - - if (libraries.length === 0) { - console.log('[Codegen] No codegen-enabled libraries found.'); - return; - } - - // 4. Locate codegen package - let codegenCliPath; - if (fs.existsSync(CODEGEN_REPO_PATH)) { - codegenCliPath = CODEGEN_REPO_PATH; - - if (!fs.existsSync(path.join(CODEGEN_REPO_PATH, 'lib'))) { - console.log( - '\n\n[Codegen] >>>>> Building react-native-codegen package', - ); - execSync('yarn install', { - cwd: codegenCliPath, - stdio: 'inherit', - }); - execSync('yarn build', { - cwd: codegenCliPath, - stdio: 'inherit', - }); - } - } else if (fs.existsSync(CODEGEN_NPM_PATH)) { - codegenCliPath = CODEGEN_NPM_PATH; - } else { - throw "error: Could not determine react-native-codegen location. Try running 'yarn install' or 'npm install' in your project root."; - } - - const schemaPaths = {}; - - const iosOutputDir = path.join( - outputPath ? outputPath : appRootDir, - 'build/generated/ios', - ); - - // 5. For each codegen-enabled library, generate the native code spec files - libraries.forEach(library => { - if (!CODEGEN_FABRIC_ENABLED && library.config.type === 'components') { - console.log( - `[Codegen] ${library.config.name} skipped because fabric is not enabled.`, - ); - return; - } - const tmpDir = fs.mkdtempSync( - path.join(os.tmpdir(), library.config.name), - ); - const pathToSchema = path.join(tmpDir, 'schema.json'); - const pathToJavaScriptSources = path.join( - library.libraryPath, - library.config.jsSrcsDir, - ); - const pathToOutputDirIOS = path.join( - iosOutputDir, - library.config.type === 'components' - ? 'react/renderer/components' - : './', - library.config.name, - ); - const pathToTempOutputDir = path.join(tmpDir, 'out'); - - console.log(`\n\n[Codegen] >>>>> Processing ${library.config.name}`); - // Generate one schema for the entire library... - executeNodeScript( - `${path.join( - codegenCliPath, - 'lib', - 'cli', - 'combine', - 'combine-js-to-schema-cli.js', - )} ${pathToSchema} ${pathToJavaScriptSources}`, - ); - console.log(`[Codegen] Generated schema: ${pathToSchema}`); - - // ...then generate native code artifacts. - const libraryTypeArg = library.config.type - ? `--libraryType ${library.config.type}` - : ''; - fs.mkdirSync(pathToTempOutputDir, {recursive: true}); - executeNodeScript( - `${path.join( - RN_ROOT, - 'scripts', - 'generate-specs-cli.js', - )} --platform ios --schemaPath ${pathToSchema} --outputDir ${pathToTempOutputDir} --libraryName ${ - library.config.name - } ${libraryTypeArg}`, - ); - - // Finally, copy artifacts to the final output directory. - fs.mkdirSync(pathToOutputDirIOS, {recursive: true}); - execSync(`cp -R ${pathToTempOutputDir}/* ${pathToOutputDirIOS}`); - console.log(`[Codegen] Generated artifacts: ${pathToOutputDirIOS}`); - - // Filter the react native core library out. - // In the future, core library and third party library should - // use the same way to generate/register the fabric components. - if (!isReactNativeCoreLibrary(library.config.name)) { - schemaPaths[library.config.name] = pathToSchema; - } - }); - - if (CODEGEN_FABRIC_ENABLED) { - console.log('\n\n>>>>> Creating component provider'); - // Save the list of spec paths to a temp file. - const schemaListTmpPath = `${os.tmpdir()}/rn-tmp-schema-list.json`; - const fd = fs.openSync(schemaListTmpPath, 'w'); - fs.writeSync(fd, JSON.stringify(schemaPaths)); - fs.closeSync(fd); - console.log(`Generated schema list: ${schemaListTmpPath}`); - - // Generate FabricComponentProvider. - // Only for iOS at this moment. - executeNodeScript( - `${path.join( - RN_ROOT, - 'scripts', - 'generate-provider-cli.js', - )} --platform ios --schemaListPath "${schemaListTmpPath}" --outputDir ${iosOutputDir}`, - ); - console.log(`Generated provider in: ${iosOutputDir}`); - } - } catch (err) { - console.error(err); - process.exitCode = 1; - } - - // 5. Done! - console.log('\n\n[Codegen] Done.'); - return; -} const appRoot = argv.path; const outputPath = argv.outputPath; -main(appRoot, outputPath); + +executor.execute( + appRoot, + outputPath, + NODE, + CODEGEN_CONFIG_FILENAME, + CODEGEN_CONFIG_KEY, + CODEGEN_CONFIG_FILE_DIR, + CODEGEN_FABRIC_ENABLED, +); diff --git a/scripts/generate-specs-cli.js b/scripts/generate-specs-cli.js index 550b363f371c..19862d20131d 100644 --- a/scripts/generate-specs-cli.js +++ b/scripts/generate-specs-cli.js @@ -9,20 +9,8 @@ 'use strict'; -let RNCodegen; -try { - RNCodegen = require('../packages/react-native-codegen/lib/generators/RNCodegen.js'); -} catch (e) { - RNCodegen = require('react-native-codegen/lib/generators/RNCodegen.js'); - if (!RNCodegen) { - throw 'RNCodegen not found.'; - } -} - -const fs = require('fs'); -const mkdirp = require('mkdirp'); -const path = require('path'); const yargs = require('yargs'); +const executor = require('./codegen/generate-specs-cli-executor'); const argv = yargs .option('p', { @@ -36,7 +24,7 @@ const argv = yargs .option('o', { alias: 'outputDir', describe: - 'Path to directory where native code source files should be saved.', + 'Path to the root directory where native code source files should be saved.', }) .option('n', { alias: 'libraryName', @@ -59,81 +47,8 @@ const argv = yargs 'Please provide platform, schema path, and output directory.', ).argv; -const GENERATORS = { - all: { - android: ['componentsAndroid', 'modulesAndroid'], - ios: ['componentsIOS', 'modulesIOS'], - }, - components: { - android: ['componentsAndroid'], - ios: ['componentsIOS'], - }, - modules: { - android: ['modulesAndroid'], - ios: ['modulesIOS'], - }, -}; - -function generateSpec( - platform, - schemaPath, - outputDirectory, - libraryName, - packageName, - libraryType, -) { - const schemaText = fs.readFileSync(schemaPath, 'utf-8'); - - if (schemaText == null) { - throw new Error(`Can't find schema at ${schemaPath}`); - } - - if (!outputDirectory) { - outputDirectory = path.resolve(__dirname, '..', 'Libraries', libraryName); - } - mkdirp.sync(outputDirectory); - - let schema; - try { - schema = JSON.parse(schemaText); - } catch (err) { - throw new Error(`Can't parse schema to JSON. ${schemaPath}`); - } - - if (GENERATORS[libraryType] == null) { - throw new Error(`Invalid library type. ${libraryType}`); - } - - RNCodegen.generate( - { - libraryName, - schema, - outputDirectory, - packageName, - }, - { - generators: GENERATORS[libraryType][platform], - }, - ); - - if (platform === 'android') { - // Move all components C++ files to a structured jni folder for now. - // Note: this should've been done by RNCodegen's generators, but: - // * the generators don't support platform option yet - // * this subdir structure is Android-only, not applicable to iOS - const files = fs.readdirSync(outputDirectory); - const jniOutputDirectory = `${outputDirectory}/jni/react/renderer/components/${libraryName}`; - mkdirp.sync(jniOutputDirectory); - files - .filter(f => f.endsWith('.h') || f.endsWith('.cpp')) - .forEach(f => { - fs.renameSync(`${outputDirectory}/${f}`, `${jniOutputDirectory}/${f}`); - }); - } -} - function main() { - generateSpec( + executor.execute( argv.platform, argv.schemaPath, argv.outputDir, diff --git a/scripts/react_native_pods_utils/script_phases.sh b/scripts/react_native_pods_utils/script_phases.sh index 2e54a05de52d..6c41ce1cbaa2 100755 --- a/scripts/react_native_pods_utils/script_phases.sh +++ b/scripts/react_native_pods_utils/script_phases.sh @@ -87,12 +87,12 @@ generateCodegenArtifactsFromSchema () { describe "Generating codegen artifacts from schema" pushd "$RN_DIR" >/dev/null || exit 1 if [ "$RCT_SCRIPT_LIBRARY_TYPE" = "all" ]; then - runSpecCodegen "$TEMP_OUTPUT_DIR/$RCT_SCRIPT_CODEGEN_MODULE_DIR/$RCT_SCRIPT_LIBRARY_NAME" "modules" - runSpecCodegen "$TEMP_OUTPUT_DIR/$RCT_SCRIPT_CODEGEN_COMPONENT_DIR/$RCT_SCRIPT_LIBRARY_NAME" "components" + runSpecCodegen "$TEMP_OUTPUT_DIR" "modules" + runSpecCodegen "$TEMP_OUTPUT_DIR" "components" elif [ "$RCT_SCRIPT_LIBRARY_TYPE" = "components" ]; then - runSpecCodegen "$TEMP_OUTPUT_DIR/$RCT_SCRIPT_CODEGEN_COMPONENT_DIR/$RCT_SCRIPT_LIBRARY_NAME" "$RCT_SCRIPT_LIBRARY_TYPE" + runSpecCodegen "$TEMP_OUTPUT_DIR" "$RCT_SCRIPT_LIBRARY_TYPE" elif [ "$RCT_SCRIPT_LIBRARY_TYPE" = "modules" ]; then - runSpecCodegen "$TEMP_OUTPUT_DIR/$RCT_SCRIPT_CODEGEN_MODULE_DIR/$RCT_SCRIPT_LIBRARY_NAME" "$RCT_SCRIPT_LIBRARY_TYPE" + runSpecCodegen "$TEMP_OUTPUT_DIR" "$RCT_SCRIPT_LIBRARY_TYPE" fi popd >/dev/null || exit 1 }