Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/sources/main/kotlin/com/progmasoft
diff options
context:
space:
mode:
authorLeitwolf2026-11-12 22:36:11 +0300
committerGitHub2026-11-12 22:36:11 +0300
commit86214c96285f73d352f934b0da33d231150898f0 (patch)
treeaffc0bfef110459bc8fcf3e8f197ed95cc46f6bb /sources/main/kotlin/com/progmasoft
parent95cdafcf1aa01b5d32171c56e6a94137402643b9 (diff)
parent7cb809c6560fc4ed29f41e4e4199cd91af6cbed1 (diff)
downloadtazel-main.tar.gz
Merge pull request #5 from Progmasoft/feature/build-c-familyHEADmain
Build and run C, C++, Objective-C, Objective-C++ and Zig targets
Diffstat (limited to 'sources/main/kotlin/com/progmasoft')
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/cli/BuildCommands.kt194
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/cli/Main.kt28
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/BuildGraph.kt52
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/BuildPlan.kt120
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/Builder.kt215
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/CcCommands.kt302
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/CcToolchain.kt126
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/FileSets.kt136
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/Jobs.kt157
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/driver/UpToDate.kt107
10 files changed, 1435 insertions, 2 deletions
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.libraries.flatMap { it.target.settings.defines }
+ command += defines.distinct().map { "-D$it" }
+ val includes =
+ publicIncludes(target) +
+ settings.privateHeaders.map { target.packageDirectory.resolve(it).normalize() } +
+ target.libraries.flatMap(::publicIncludes)
+ command += includes.distinct().map { "-I$it" }
+ settings.precompiledHeader?.let {
+ command += listOf("-include", target.packageDirectory.resolve(it).normalize().toString())
+ }
+ command += settings.flags
+ return command
+ }
+
+ /** The command that archives [objects] into the static library of [target]. */
+ fun archive(target: PlannedTarget, objects: List<Path>): List<String> =
+ compiler.archiver + listOf("rcs", target.product.toString()) + objects.map(Path::toString)
+
+ /** The command that links [objects] and the libraries of [target] into its product. */
+ fun link(target: PlannedTarget, objects: List<Path>): List<String> {
+ val settings = target.target.settings
+ val everything = listOf(target) + target.libraries
+ val cpp = everything.any { it.language == Language.CPP || it.language == Language.OBJCPP }
+ val command = mutableListOf<String>()
+ command += if (cpp) compiler.cxx else compiler.c
+ command += compiler.targetFlags
+ command += objects.map(Path::toString)
+ for (library in target.libraries) {
+ when (library.kind) {
+ TargetKind.STATICLIB ->
+ if (library.target.settings.alwaysLink) {
+ command += wholeArchive(library.product)
+ } else {
+ command += library.product.toString()
+ }
+ TargetKind.SHAREDLIB -> command += library.product.toString()
+ else -> Unit
+ }
+ }
+ command += listOf("-o", target.product.toString())
+ if (target.kind == TargetKind.SHAREDLIB) command += sharedFlags(target)
+ command += sanitizeFlags(settings.sanitizers)
+ comma