diff options
| author | Leitwolf | 2026-11-12 22:36:11 +0300 |
|---|---|---|
| committer | GitHub | 2026-11-12 22:36:11 +0300 |
| commit | 86214c96285f73d352f934b0da33d231150898f0 (patch) | |
| tree | affc0bfef110459bc8fcf3e8f197ed95cc46f6bb | |
| parent | 95cdafcf1aa01b5d32171c56e6a94137402643b9 (diff) | |
| parent | 7cb809c6560fc4ed29f41e4e4199cd91af6cbed1 (diff) | |
| download | tazel-86214c96285f73d352f934b0da33d231150898f0.tar.gz | |
Build and run C, C++, Objective-C, Objective-C++ and Zig targets
13 files changed, 1717 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb7111..73127ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,3 +19,7 @@ Versions follow [Semantic Versioning](https://semver.org). The earlier Tazel, 0. - The plugin API, `com.progmasoft.tazel:tazel-api` (API version 1), and the Gradle plugin `com.progmasoft.tazel.plugin`, taken from the legacy Tazel. `tazelPlugin { }` now holds `group`, `name` and `version`, which the plugin descriptor records; the group must be a reversed domain and the version a Semantic Version. +- `tazel build` and `tazel run` for C, C++, Objective-C, Objective-C++ and Zig targets, with `--release` and + `--triple`. Each translation unit is compiled on its own and again only when the depfile of the compiler or the + command line says it changed; libraries and programs are archived or linked when an input changed. Clang, GCC and + `zig cc` are found on PATH; Clang and `zig cc` build for other triples. Outputs go to `.tazel/out/<triple>/<mode>`. diff --git a/sources/main/kotlin/com/progmasoft/tazel/cli/BuildCommands.kt b/sources/main/kotlin/com/progmasoft/tazel/cli/BuildCommands.kt new file mode 100644 index 0000000..1567f18 --- /dev/null +++ b/sources/main/kotlin/com/progmasoft/tazel/cli/BuildCommands.kt @@ -0,0 +1,194 @@ +/* + * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> + * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + */ + +package com.progmasoft.tazel.cli + +import com.progmasoft.tazel.driver.BuildFailure +import com.progmasoft.tazel.driver.BuildPlan +import com.progmasoft.tazel.driver.Builder +import com.progmasoft.tazel.driver.CcToolchain +import com.progmasoft.tazel.dsl.BuildMode +import com.progmasoft.tazel.dsl.ProjectException +import com.progmasoft.tazel.dsl.TargetKind +import com.progmasoft.tazel.dsl.TargetTriple +import com.progmasoft.tazel.label.LabelException +import com.progmasoft.tazel.label.Labels +import com.progmasoft.tazel.label.PackageId +import com.progmasoft.tazel.label.PackagePattern +import com.progmasoft.tazel.label.TargetLabel +import com.progmasoft.tazel.project.Module +import com.progmasoft.tazel.project.ModuleLoader + +/** What `tazel build` and `tazel run` were given on the command line. */ +internal data class BuildOptions( + val mode: BuildMode, + val triple: String?, + val selections: List<String>, + /** After `--`: what `tazel run` passes to the program. */ + val programArguments: List<String>, +) + +/** Reads the options of `tazel build` and `tazel run`; [command] names it in messages. */ +internal fun buildOptions(command: String, arguments: List<String>): BuildOptions { + var mode = BuildMode.DEBUG + var triple: String? = null + val selections = mutableListOf<String>() + var index = 0 + while (index < arguments.size) { + when (val argument = arguments[index]) { + "--" -> return BuildOptions(mode, triple, selections, arguments.drop(index + 1)) + "--release" -> mode = BuildMode.RELEASE + "--triple" -> { + triple = + arguments.getOrNull(index + 1) + ?: throw Usage("tazel $command --triple needs an LLVM target triple after it") + index++ + } + else -> + if (argument.startsWith("-")) { + throw Usage("tazel $command has no option $argument; write tazel help") + } else { + selections += argument + } + } + index++ + } + return BuildOptions(mode, triple, selections, emptyList()) +} + +/** A command line that is wrong; it exits with [ExitCode.USAGE]. */ +internal class Usage(message: String) : Exception(message) + +/** `tazel build` and `tazel run`. */ +internal class BuildCommands( + private val environment: Environment, + private val out: Appendable, + private val loader: ModuleLoader = ModuleLoader(), +) { + private val host = TargetTriple.host() + + fun build(options: BuildOptions): Int { + val (module, labels, triple) = select(options, run = false) + build(module, labels, triple, options.mode) + return ExitCode.SUCCESS + } + + fun run(options: BuildOptions): Int { + val (module, labels, triple) = select(options, run = true) + if (triple.text != host.text) { + throw Stop("tazel run runs programs of this machine ($host), not of $triple") + } + val label = labels.single() + val products = build(module, labels, triple, options.mode) + val settings = module.configure(label, triple, options.mode).settings + val program = products.getValue(label) + return environment.processes.runWith( + listOf(program.toString()) + settings.args + options.programArguments, + environment.directory, + settings.env, + ) + } + + private fun build( + module: Module, + labels: List<TargetLabel>, + triple: TargetTriple, + mode: BuildMode, + ): Map<TargetLabel, java.nio.file.Path> { + val plan = BuildPlan(module, triple, mode) + labels.forEach(plan::add) + val execution = module.file.execution + val builder = + Builder( + plan, + CcToolchain(environment.programs, host), + environment.programs, + environment.processes, + out, + host, + execution.jobs ?: Runtime.getRuntime().availableProcessors(), + execution.keepGoing, + ) + val result = + try { + builder.build() + } catch (failure: BuildFailure) { + throw Stop(failure.message.orEmpty()) + } + out.appendLine( + "%12s %d target(s) for %s, %s; %d command(s) run" + .format("Built", plan.targets.size, triple, mode.name.lowercase(), result.commandsRun) + ) + return result.products + } + + private data class Selected( + val module: Module, + val labels: List<TargetLabel>, + val triple: TargetTriple, + ) + + private fun select(options: BuildOptions, run: Boolean): Selected { + val root = loader.findRoot(environment.directory) + val module = loader.load(root) + val triple = options.triple?.let(TargetTriple::parse) ?: host + val current = + module.packageOf(environment.directory)?.id + ?: if (options.selections.isEmpty()) { + throw Stop( + "${environment.directory} is in no package; name targets such as //... or //app/:app" + ) + } else PackageId.ROOT + val written = options.selections.ifEmpty { listOf(current.toString() + "...") } + val labels = mutableListOf<TargetLabel>() + for (text in written) { + val selection = + try { + if (options.selections.isEmpty()) PackagePattern(current) + else Labels.selection(text, current) + } catch (error: LabelException) { + throw Usage(error.message.orEmpty()) + } + val selected = module.select(selection) + val forTriple = selected.filter { triple in module.triplesOf(it, options.mode, host) } + if (selection is TargetLabel && forTriple.isEmpty()) { + throw Stop("$selection is not built for $triple; name one of its triples with --triple") + } + // Without a label, only the package of the directory is meant, not the ones below it. + labels += + if (options.selections.isEmpty()) forTriple.filter { it.packageId == current } + else forTriple + } + if (run) { + val bins = labels.distinct().filter { module.declaration(it).second.kind == TargetKind.BIN } + return when { + options.selections.size > 1 -> throw Usage("tazel run takes one label") + bins.size == 1 -> Selected(module, bins, triple) + bins.isEmpty() -> throw Stop("no bin target to run among ${written.joinToString()}") + else -> throw Stop("${bins.joinToString()} are bin targets; name the one to run") + } + } + if (labels.isEmpty()) throw Stop("nothing to build for $triple in ${written.joinToString()}") + return Selected(module, labels.distinct(), triple) + } + + companion object { + /** Runs [block], turning what the project or the command line got wrong into exit codes. */ + fun guarded(err: Appendable, block: () -> Int): Int = + try { + block() + } catch (usage: Usage) { + err.appendLine("error: ${usage.message}") + ExitCode.USAGE + } catch (error: ProjectException) { + err.appendLine("error: ${error.message}") + ExitCode.FAILURE + } catch (stop: Stop) { + err.appendLine("error: ${stop.message}") + stop.hint?.let(err::appendLine) + ExitCode.FAILURE + } + } +} diff --git a/sources/main/kotlin/com/progmasoft/tazel/cli/Main.kt b/sources/main/kotlin/com/progmasoft/tazel/cli/Main.kt index c5a8f46..66fa08f 100644 --- a/sources/main/kotlin/com/progmasoft/tazel/cli/Main.kt +++ b/sources/main/kotlin/com/progmasoft/tazel/cli/Main.kt @@ -39,16 +39,24 @@ internal val HELP = """ Tazel builds C, C++, Objective-C, Objective-C++ and Zig with Kotlin project files. - Usage: tazel <command> + Usage: tazel <command> [options] [labels] Commands: + build builds targets, such as tazel build //app/:greeter or tazel build //... + run builds and runs one bin target; what follows -- goes to the program custom runs a command of tazel_custom.txt, or lists them version prints the version of Tazel help prints this help + + Options of build and run: + --release builds in release mode; without it, debug + --triple <triple> builds for that LLVM target triple; without it, this machine's + + Without a label, build and run take the package of the current directory. """ .trimIndent() -private val COMMANDS = listOf("custom", "version", "help") +private val COMMANDS = listOf("build", "run", "custom", "version", "help") /** Where a command runs and what it starts programs with; tests give their own. */ internal class Environment( @@ -84,6 +92,22 @@ internal fun run( out.appendLine("tazel ${tazelVersion()}") ExitCode.SUCCESS } else usage(err, "tazel version takes no arguments") + "build", + "run" -> + BuildCommands.guarded(err) { + refuseWithoutSandbox(environment.directory) + val command = arguments.first() + val options = buildOptions(command, arguments.drop(1)) + val commands = BuildCommands(environment, out) + if (command == "run") { + commands.run(options) + } else { + if (options.programArguments.isNotEmpty()) { + throw Usage("tazel build takes nothing after --") + } + commands.build(options) + } + } // What follows `custom` belongs to the command it names, whatever it looks like. "custom" -> { val words = arguments.drop(1) diff --git a/sources/main/kotlin/com/progmasoft/tazel/driver/BuildGraph.kt b/sources/main/kotlin/com/progmasoft/tazel/driver/BuildGraph.kt new file mode 100644 index 0000000..4b4ec25 --- /dev/null +++ b/sources/main/kotlin/com/progmasoft/tazel/driver/BuildGraph.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> + * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + */ + +package com.progmasoft.tazel.driver + +/** + * One piece of work of a command: a build step of a plugin, the compilation of a target, a test + * suite. It starts once all of [dependsOn] have finished. [name] is what a profile shows; [id] is + * unique in the graph, while two projects may each have a target of the same name. + */ +class BuildUnit<R>( + val id: String, + val name: String = id, + val dependsOn: Set<String> = emptySet(), + val work: () -> R, +) + +/** The units of a command, in the order they should start when nothing else decides. */ +class BuildGraph<R>(val units: List<BuildUnit<R>>) { + /** The units that wait for each unit. */ + val dependents: Map<String, List<BuildUnit<R>>> = + units + .flatMap { unit -> unit.dependsOn.map { it to unit } } + .groupBy({ (id, _) -> id }, { (_, unit) -> unit }) + + init { + val ids = units.map(BuildUnit<R>::id) + val repeated = ids.groupBy { it }.filterValues { it.size > 1 }.keys + require(repeated.isEmpty()) { "units are repeated: $repeated" } + val missing = units.flatMap(BuildUnit<R>::dependsOn).toSet() - ids.toSet() + require(missing.isEmpty()) { "units depend on what is not there: $missing" } + require(!cyclic()) { "the units depend on each other" } + } + + // A unit can start once what it waits for has, so a cycle is what never gets to start. + private fun cyclic(): Boolean { + val waiting = units.associate { it.id to it.dependsOn.size }.toMutableMap() + val ready = ArrayDeque(units.filter { it.dependsOn.isEmpty() }) + var started = 0 + while (ready.isNotEmpty()) { + started++ + for (unit in dependents[ready.removeFirst().id].orEmpty()) { + val left = waiting.getValue(unit.id) - 1 + waiting[unit.id] = left + if (left == 0) ready += unit + } + } + return started != units.size + } +} diff --git a/sources/main/kotlin/com/progmasoft/tazel/driver/BuildPlan.kt b/sources/main/kotlin/com/progmasoft/tazel/driver/BuildPlan.kt new file mode 100644 index 0000000..49c0e01 --- /dev/null +++ b/sources/main/kotlin/com/progmasoft/tazel/driver/BuildPlan.kt @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> + * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + */ + +package com.progmasoft.tazel.driver + +import com.progmasoft.tazel.dsl.BuildMode +import com.progmasoft.tazel.dsl.Language +import com.progmasoft.tazel.dsl.ProjectException +import com.progmasoft.tazel.dsl.TargetKind +import com.progmasoft.tazel.dsl.TargetTriple +import com.progmasoft.tazel.label.TargetLabel +import com.progmasoft.tazel.project.ConfiguredTarget +import com.progmasoft.tazel.project.ExternalDependency +import com.progmasoft.tazel.project.Module +import com.progmasoft.tazel.project.ModuleDependency +import java.nio.file.Files +import java.nio.file.Path + +/** A configured target with where its outputs go. */ +class PlannedTarget( + val target: ConfiguredTarget, + /** The directory of its `BUILD.kts`, which its paths are relative to. */ + val packageDirectory: Path, + /** The directory of its outputs: objects, staged headers and the product. */ + val outputDirectory: Path, + /** The program or library it makes. */ + val product: Path, + /** Its libraries, the nearest first, each once: what it links, in link order. */ + val libraries: List<PlannedTarget>, +) { + val label: TargetLabel + get() = target.label + + val kind: TargetKind + get() = target.kind + + val language: Language + get() = target.language + + /** The libraries it depends on directly. */ + val direct: List<PlannedTarget> + get() = libraries.filter { library -> target.dependencies.any { it.label == library.label } } +} + +/** + * Configures what a command builds: the selected targets and every library they depend on, for one + * triple and build mode. A target is configured once, however many depend on it. + */ +class BuildPlan(val module: Module, val triple: TargetTriple, val mode: BuildMode) { + private val planned = linkedMapOf<TargetLabel, PlannedTarget>() + private val visiting = linkedSetOf<TargetLabel>() + + /** The targets of the plan, each after the libraries it depends on. */ + val targets: List<PlannedTarget> + get() = planned.values.toList() + + /** The output directory of a triple and build mode, below `.tazel/out` of the module. */ + val outputRoot: Path = + module.root.resolve(".tazel/out").resolve(triple.text).resolve(mode.name.lowercase()) + + /** Adds [label] and what it depends on. */ + fun add(label: TargetLabel): PlannedTarget { + planned[label]?.let { + return it + } + if (!visiting.add(label)) { + throw ProjectException( + "the targets depend on each other: ${(visiting.dropWhile { it != label } + label).joinToString(" -> ")}" + ) + } + val target = module.configure(label, triple, mode) + val direct = + target.dependencies.map { dependency -> + when (dependency) { + is ModuleDependency -> add(dependency.label) + is ExternalDependency -> + throw ProjectException( + "$label depends on ${dependency.label}: dependencies from ViGet and local .vipkg " + + "files are not built by this Tazel yet" + ) + } + } + // Nearest first, each library once, a library after everything that depends on it. + val order = mutableListOf<PlannedTarget>() + fun visit(library: PlannedTarget) { + order.remove(library) + order += library + library.direct.forEach(::visit) + } + direct.forEach(::visit) + val (pkg, _) = module.declaration(label) + val directory = outputRoot.resolve(label.packageId.path.ifEmpty { "_root" }).resolve(label.name) + val name = target.settings.outputName ?: label.name + val os = triple.os + val product = + directory.resolve( + when (target.kind) { + TargetKind.BIN, + TargetKind.TEST -> OutputNames.executable(name, os) + TargetKind.STATICLIB -> OutputNames.staticLibrary(name, os) + TargetKind.SHAREDLIB -> OutputNames.sharedLibrary(name, os, target.settings.soVersion) + } + ) + visiting.remove(label) + return PlannedTarget(target, pkg.directory, directory, product, order.toList()).also { + planned[label] = it + } + } + + /** Whether a shared library links [library], so it is compiled as position-independent code. */ + fun linkedIntoSharedLibrary(library: PlannedTarget): Boolean = + planned.values.any { it.kind == TargetKind.SHAREDLIB && library in it.libraries } + + /** Creates the output directory of [target]. */ + fun prepare(target: PlannedTarget) { + Files.createDirectories(target.outputDirectory) + } +} diff --git a/sources/main/kotlin/com/progmasoft/tazel/driver/Builder.kt b/sources/main/kotlin/com/progmasoft/tazel/driver/Builder.kt new file mode 100644 index 0000000..c0811f6 --- /dev/null +++ b/sources/main/kotlin/com/progmasoft/tazel/driver/Builder.kt @@ -0,0 +1,215 @@ +/* + * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> + * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + */ + +package com.progmasoft.tazel.driver + +import com.progmasoft.tazel.dsl.BuildMode +import com.progmasoft.tazel.dsl.FilePath +import com.progmasoft.tazel.dsl.Language +import com.progmasoft.tazel.dsl.Os +import com.progmasoft.tazel.dsl.ProjectException +import com.progmasoft.tazel.dsl.TargetKind +import com.progmasoft.tazel.dsl.ZigOptimize +import com.progmasoft.tazel.label.TargetLabel +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.concurrent.atomic.AtomicInteger + +/** A compiler, archiver or linker that failed, with what it printed. */ +class BuildFailure(message: String) : Exception(message) + +/** What a build did. */ +data class BuildResult(val products: Map<TargetLabel, Path>, val commandsRun: Int) + +/** + * Builds the targets of a [BuildPlan]: each translation unit is compiled on its own and only when + * its object is out of date (by the depfile the compiler writes and the command line it ran with); + * a library or program is archived or linked when one of its inputs changed. + */ +class Builder( + private val plan: BuildPlan, + private val toolchain: CcToolchain, + private val programs: Programs, + private val processes: Processes, + private val out: Appendable, + private val host: com.progmasoft.tazel.dsl.TargetTriple, + private val jobs: Int, + private val keepGoing: Boolean, +) { + private val fileSets = FileSets(ignoreCase = host.os == Os.WINDOWS) + private val commandsRun = AtomicInteger() + + fun build(): BuildResult { + val units = mutableListOf<BuildUnit<Unit>>() + for (target in plan.targets) { + plan.prepare(target) + units += if (target.language == Language.ZIG) zigUnits(target) else ccUnits(target) + } + try { + JobPool(jobs, keepGoing).run(BuildGraph(units)) + } catch (failures: JobFailures) { + throw BuildFailure(failures.failures.joinToString("\n") { it.message.orEmpty() }) + } + return BuildResult(plan.targets.associate { it.label to it.product }, commandsRun.get()) + } + + private fun finalUnit(label: TargetLabel) = "final $label" + + private fun ccUnits(target: PlannedTarget): List<BuildUnit<Unit>> { + val settings = target.target.settings + val compiler = toolchain.compiler(settings.compiler, target.language, plan.triple) + val commands = CcCommands(plan, compiler) + commands.stageHeaders(target) + val sources = + commands.translationUnits( + target, + fileSets.expand(target.packageDirectory, settings.sources, settings.excludes), + ) + val after = target.direct.map { finalUnit(it.label) }.toSet() + val compiles = sources.map { source -> + val objectFile = commands.objectOf(target, source) + BuildUnit("compile ${target.label} $source", "compile $source", after) { + val depfile = objectFile.resolveSibling(objectFile.fileName.toString() + ".d") + val command = commands.compile(target, source, objectFile, depfile) + if (!UpToDate.checkObject(objectFile, depfile, command, target.packageDirectory)) { + Files.createDirectories(objectFile.parent) + run( + "Compiling", + "${target.label} ${target.packageDirectory.relativize(source)}", + command, + target.packageDirectory, + objectFile, + ) + } + } + } + val objects = sources.map { commands.objectOf(target, it) } + val final = + BuildUnit( + finalUnit(target.label), + "link ${target.label}", + compiles.map { it.id }.toSet() + after, + ) { + val inputs = objects + target.libraries.map { it.product } + val (verb, command) = + if (target.kind == TargetKind.STATICLIB) "Archiving" to commands.archive(target, objects) + else "Linking" to commands.link(target, objects) + if (!UpToDate.check(target.product, command, inputs)) { + if (target.kind == TargetKind.STATICLIB) Files.deleteIfExists(target.product) + run(verb, target.label.toString(), command, target.packageDirectory, target.product) + if (target.kind == TargetKind.SHAREDLIB) linkUnversioned(target) + } + copyData(target) + } + return compiles + final + } + + private fun zigUnits(target: PlannedTarget): List<BuildUnit<Unit>> { + val after = target.direct.map { finalUnit(it.label) }.toSet() + return listOf( + BuildUnit(finalUnit(target.label), "zig ${target.label}", after) { + val command = zigCommand(target) + run("Building", target.label.toString(), command, target.packageDirectory, target.product) + copyData(target) + } + ) + } + + private fun zigCommand(target: PlannedTarget): List<String> { + val settings = target.target.settings + val zig = programs.find("zig")?.toString() ?: throw ProjectException("zig is not on PATH") + val root = + target.packageDirectory.resolve((settings.sources.single() as FilePath).path).normalize() + if (!Files.isRegularFile(root)) throw ProjectException("${target.label}: no file $root") + val command = mutableListOf(zig) + command += + when (target.kind) { + TargetKind.BIN -> listOf("build-exe") + TargetKind.STATICLIB -> listOf("build-lib") + TargetKind.SHAREDLIB -> listOf("build-lib", "-dynamic") + TargetKind.TEST -> listOf("test", "--test-no-exec") + } + command += root.toString() + command += "-femit-bin=${target.product}" + command += listOf("--cache-dir", target.outputDirectory.resolve("zig-cache").toString()) + val optimize = + settings.zigOptimize ?: if (plan.mode == BuildMode.RELEASE) ZigOptimize.SAFE else null + command += + listOf( + "-O", + when (optimize) { + null -> "Debug" + ZigOptimize.SAFE -> "ReleaseSafe" + ZigOptimize.SPEED -> "ReleaseFast" + ZigOptimize.SIZE -> "ReleaseSmall" + }, + ) + if (plan.triple.text != host.text) command += listOf("-target", zigTriple(plan.triple)) + if (target.libraries.isNotEmpty()) { + for (library in target.libraries) { + library.target.settings.headers.forEach { + command += "-I" + library.packageDirectory.resolve(it).normalize() + } + command += library.product.toString() + } + if (target.libraries.any { it.language != Language.ZIG }) command += "-lc" + } + return command + } + + /** `libcore.so` beside `libcore.so.1`, so the linker of a dependent finds the library. */ + private fun linkUnversioned(target: PlannedTarget) { + if (target.target.settings.soVersion == null) return + if (plan.triple.os == Os.WINDOWS) return + val name = target.target.settings.outputName ?: target.label.name + val plain = target.product.resolveSibling(OutputNames.sharedLibrary(name, plan.triple.os, null)) + Files.deleteIfExists(plain) + try { + Files.createSymbolicLink(plain, target.product.fileName) + } catch (_: Exception) { + Files.copy(target.product, plain, StandardCopyOption.REPLACE_EXISTING) + } + } + + private fun copyData(target: PlannedTarget) { + val settings = target.target.settings + if (settings.data.isEmpty()) return + for (file in fileSets.expand(target.packageDirectory, settings.data)) { + val destination = + target.product.resolveSibling( + target.packageDirectory.relativize(file).toString().replace("..", "__") + ) + Files.createDirectories(destination.parent) + Files.copy( + file, + destination, + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.COPY_ATTRIBUTES, + ) + } + } + + private fun run( + verb: String, + what: String, + command: List<String>, + directory: Path, + output: Path, + ) { + synchronized(out) { out.appendLine("%12s %s".format(verb, what)) } + UpToDate.forget(output) + commandsRun.incrementAndGet() + val result = processes.runGathered(command, directory) + if (result.output.isNotBlank()) + synchronized(out) { out.append(result.output.trimEnd()).appendLine() } + if (result.exitCode != 0) { + throw BuildFailure( + "$what: ${Path.of(command.first()).fileName} exited with ${result.exitCode}" + ) + } + UpToDate.record(output, command) + } +} diff --git a/sources/main/kotlin/com/progmasoft/tazel/driver/CcCommands.kt b/sources/main/kotlin/com/progmasoft/tazel/driver/CcCommands.kt new file mode 100644 index 0000000..c05a9c3 --- /dev/null +++ b/sources/main/kotlin/com/progmasoft/tazel/driver/CcCommands.kt @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com> + * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 + */ + +package com.progmasoft.tazel.driver + +import com.progmasoft.tazel.dsl.BuildMode +import com.progmasoft.tazel.dsl.CStandard +import com.progmasoft.tazel.dsl.CppStandard +import com.progmasoft.tazel.dsl.DebugInfo +import com.progmasoft.tazel.dsl.Language +import com.progmasoft.tazel.dsl.Lto +import com.progmasoft.tazel.dsl.Optimize +import com.progmasoft.tazel.dsl.Os +import com.progmasoft.tazel.dsl.ProjectException +import com.progmasoft.tazel.dsl.Subsystem +import com.progmasoft.tazel.dsl.TargetKind +import com.progmasoft.tazel.dsl.Warnings +import java.nio.file.Files +import java.nio.file.Path + +/** The extensions each C-family language compiles; headers among the sources are not compiled. */ +private val EXTENSIONS = + mapOf( + Language.C to setOf("c"), + Language.CPP to setOf("cpp", "cc", "cxx", "c++"), + Language.OBJC to setOf("m"), + Language.OBJCPP to setOf("mm"), + ) + +private val HEADERS = setOf("h", "hh", "hpp", "hxx", "h++", "inc", "inl", "ipp", "tpp") + +/** The compile and link command lines of C, C++, Objective-C and Objective-C++ targets. */ +class CcCommands(private val plan: BuildPlan, private val compiler: CcCompiler) { + private val os = plan.triple.os + private val mode = plan.mode + + /** The files of [target] to compile; a file of another language is an error. */ + fun translationUnits(target: PlannedTarget, files: List<Path>): List<Path> = + files.filter { file -> + val extension = file.fileName.toString().substringAfterLast('.', "").lowercase() + when { + extension in HEADERS -> false + extension in EXTENSIONS.getValue(target.language) -> true + else -> + throw ProjectException( + "${target.label}: ${target.language.keyword} targets compile " + + "${EXTENSIONS.getValue(target.language).joinToString { ".$it" }} files, not $file" + ) + } + } + + /** Where the object of [source] goes. */ + fun objectOf(target: PlannedTarget, source: Path): Path { + val relative = + target.packageDirectory.relativize(source).joinToString("/") { + if (it.toString() == "..") "__" else it.toString() + } + return target.outputDirectory.resolve("obj").resolve(relative + OutputNames.objectSuffix(os)) + } + + /** The include directories [target] gives its dependents. */ + fun publicIncludes(target: PlannedTarget): List<Path> { + val settings = target.target.settings + if (settings.includePrefix == null && settings.stripIncludePrefix == null) { + return settings.headers.map { target.packageDirectory.resolve(it).normalize() } + } + return listOf(stagedIncludes(target)) + } + + /** + * Copies the public headers of [target] to where `includePrefix` and `stripIncludePrefix` place + * them: `include/core.h` with the prefix `greeter` and `include` stripped is `greeter/core.h`. + */ + fun stageHeaders(target: PlannedTarget) { + val settings = target.target.settings + if (settings.includePrefix == null && settings.stripIncludePrefix == null) return + val staged = stagedIncludes(target) + val strip = settings.stripIncludePrefix?.let { target.packageDirectory.resolve(it).normalize() } + for (directory in settings.headers) { + val start = target.packageDirectory.resolve(directory).normalize() + if (!Files.isDirectory(start)) throw ProjectException("${target.label}: no directory $start") + Files.walk(start).use { files -> + files.filter(Files::isRegularFile).forEach { file -> + val base = if (strip != null && file.startsWith(strip)) strip else target.packageDirectory + val relative = base.relativize(file).toString() + val destination = + staged.resolve(settings.includePrefix?.let { "$it/$relative" } ?: relative) + Files.createDirectories(destination.parent) + if ( + !Files.exists(destination) || + Files.getLastModifiedTime(destination) != Files.getLastModifiedTime(file) + ) { + Files.copy( + file, + destination, + java.nio.file.StandardCopyOption.REPLACE_EXISTING, + java.nio.file.StandardCopyOption.COPY_ATTRIBUTES, + ) + } + } + } + } + } + + private fun stagedIncludes(target: PlannedTarget) = target.outputDirectory.resolve("include") + + /** The command that compiles [source] of [target] into [objectFile] and writes [depfile]. */ + fun compile(target: PlannedTarget, source: Path, objectFile: Path, depfile: Path): List<String> { + val settings = target.target.settings + val language = target.language + val cpp = language == Language.CPP || language == Language.OBJCPP + val command = mutableListOf<String>() + command += if (cpp) compiler.cxx else compiler.c + command += compiler.targetFlags + command += listOf("-c", source.toString(), "-o", objectFile.toString()) + command += listOf("-MD", "-MF", depfile.toString()) + if (cpp) { + settings.cppStandard?.let { command += "-std=" + cppStandard(it, settings.gnuExtensions) } + } else { + settings.cStandard?.let { command += "-std=" + cStandard(it, settings.gnuExtensions) } + } + command += optimizeFlags(settings.optimize ?: defaultOptimize()) + command += debugFlags(settings.debugInfo ?: defaultDebugInfo()) + command += + when (settings.warnings) { + Warnings.ALL -> listOf("-Wall", "-Wextra") + Warnings.DEFAULT -> emptyList() + Warnings.NONE -> listOf("-w") + } + if (settings.warningsAsErrors) command += "-Werror" + if (os != Os.WINDOWS && positionIndependent(target)) command += "-fPIC" + if (language == Language.OBJC || language == Language.OBJCPP) { + if (settings.arc) command += "-fobjc-arc" + } + command += sanitizeFlags(settings.sanitizers) + command += ltoFlags(settings.lto) + val defines = + settings.defines + + settings.privateDefines + + target |