cmake-ts
Version:
cmake-js rewrite in typescript to support advanced build configurations
155 lines (127 loc) • 5.51 kB
text/typescript
import { join, relative, resolve } from "path"
import { copy, ensureDir, pathExists, readFile, remove, writeFile } from "fs-extra"
import { ArgumentBuilder } from "./argumentBuilder.js"
import type { BuildConfiguration, Options } from "./config-types.d"
import { getConfigFile, parseBuildConfigs } from "./config.js"
import { applyOverrides } from "./override.js"
import { RuntimeDistribution } from "./runtimeDistribution.js"
import { runProgram } from "./utils/exec.js"
import { logger } from "./utils/logger.js"
import { retry } from "./utils/retry.js"
/**
* Build the project via cmake-ts
*
* @param opts - The options to use for the build
* @returns The configurations that were built or null if there was an error
*/
export async function build(opts: Options): Promise<BuildConfiguration[] | null> {
if (opts.command.type === "error") {
logger.error("The given options are invalid")
return null
}
if (opts.command.type === "none") {
return []
}
if (opts.help) {
return []
}
const packageJsonPath = resolve(join(process.cwd(), "package.json"))
const configFile = await getConfigFile(packageJsonPath)
if (configFile instanceof Error) {
logger.error(configFile)
return null
}
// set the missing options to their default value
const configsToBuild = await parseBuildConfigs(opts, configFile)
if (configsToBuild === null) {
logger.error("No configs to build")
return null
}
for (const config of configsToBuild) {
try {
// eslint-disable-next-line no-await-in-loop
await buildConfig(config, opts)
} catch (err) {
logger.error("Error building config", config.name, err)
return null
}
}
return configsToBuild
}
export async function buildConfig(config: BuildConfiguration, opts: Options) {
logger.debug("config", JSON.stringify(config, null, 2))
config.targetDirectory = resolve(join(config.packageDirectory, config.targetDirectory))
logger.debug("running in", config.packageDirectory, "command", opts)
logger.debug("> Setting up staging directory... ")
config.stagingDirectory = resolve(join(config.packageDirectory, config.stagingDirectory))
const stagingExists = await pathExists(config.stagingDirectory)
if (stagingExists) {
await retry(() => remove(config.stagingDirectory))
}
await ensureDir(config.stagingDirectory)
const dist = new RuntimeDistribution(config)
// Download files
logger.debug("> Distribution File Download... ")
await dist.ensureDownloaded()
logger.debug("> Determining ABI... ")
await dist.determineABI()
logger.debug("> Building directories... ")
const subDirectory = join(
config.os,
config.arch,
config.runtime,
`${config.libc}-${dist.abi()}-${config.buildType}`,
config.addonSubdirectory,
)
const stagingDir = resolve(join(config.stagingDirectory, subDirectory))
const targetDir = resolve(join(config.targetDirectory, subDirectory))
applyOverrides(config)
const argBuilder = new ArgumentBuilder(config, dist)
const [configureCmd, configureArgs] = await argBuilder.configureCommand()
const [buildCmd, buildArgs] = argBuilder.buildCommand(stagingDir)
logger.info(getConfigInfo(config, dist))
// Invoke CMake
logger.debug(`> Configure: ${configureCmd} ${configureArgs.map((a) => `"${a}"`).join(" ")} in ${stagingDir}`)
await ensureDir(stagingDir)
await runProgram(configureCmd, configureArgs, stagingDir)
// Actually build the software
logger.debug(`> Build ${config.generatorBinary} ${buildArgs.map((a) => `"${a}"`).join(" ")} in ${stagingDir}`)
await runProgram(buildCmd, buildArgs, stagingDir)
// Copy back the previously built binary
logger.debug(`> Copying ${config.projectName}.node to ${targetDir}`)
const addonPath = join(targetDir, `${config.projectName}.node`)
const sourceAddonPath = config.generatorToUse.includes("Visual Studio")
? join(stagingDir, config.buildType, `${config.projectName}.node`)
: join(stagingDir, `${config.projectName}.node`)
await ensureDir(targetDir)
await retry(() => copy(sourceAddonPath, addonPath))
logger.debug("Adding the built config to the manifest file...")
// read the manifest if it exists
const manifestPath = join(config.targetDirectory, "manifest.json")
let manifest: Record<string, string> = {}
if (await pathExists(manifestPath)) {
const manifestContent = await readFile(manifestPath, "utf-8")
manifest = JSON.parse(manifestContent)
}
// add the new entry to the manifest
manifest[serializeConfig(config, config.packageDirectory)] = relative(config.targetDirectory, addonPath)
const manifestSerialized = JSON.stringify(manifest, null, 2)
await retry(() => writeFile(manifestPath, manifestSerialized))
}
function getConfigInfo(config: BuildConfiguration, dist: RuntimeDistribution): string {
return `${config.name} ${config.os} ${config.arch} ${config.libc} ${config.runtime} ${config.runtimeVersion} ABI ${dist.abi()}
${config.generatorToUse} ${config.generatorFlags?.join(" ") ?? ""} ${config.buildType} ${config.toolchainFile !== undefined ? `toolchain ${config.toolchainFile}` : ""}
${config.CMakeOptions.join(" ")}`
}
function serializeConfig(config: BuildConfiguration, rootDir: string) {
return JSON.stringify(
config,
// replace absolute paths with relative paths in the values
(_key, value) => {
if (typeof value === "string" && value.startsWith(rootDir)) {
return relative(rootDir, value)
}
return value
},
)
}