diff options
| author | Leitwolf11 | 2026-11-12 22:19:46 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-12 22:19:46 +0300 |
| commit | 7cb809c6560fc4ed29f41e4e4199cd91af6cbed1 (patch) | |
| tree | affc0bfef110459bc8fcf3e8f197ed95cc46f6bb | |
| parent | 95cdafcf1aa01b5d32171c56e6a94137402643b9 (diff) | |
| download | tazel-7cb809c6560fc4ed29f41e4e4199cd91af6cbed1.tar.gz | |
Build and run C, C++, Objective-C, Objective-C++ and Zig targetsfeature/build-c-family
Tazel gets its first build commands:
tazel build [--release] [--triple <LLVM triple>] [labels...]
tazel run [--release] [--triple <LLVM triple>] [label] [-- program arguments]
Without a label they take the package of the current directory. A
pattern skips targets not built for the triple; a label names one, or
it is an error. tazel run builds and runs one bin target with its args
and env from BUILD.kts, then the arguments after --.
- Plan (driver.BuildPlan): the selected targets and every library they
depend on, configured once each for one triple and build mode, with
link order (dependents before dependencies) and cycle detection.
Dependencies from ViGet and local .vipkg files are refused with a
message, since they are not built yet.
- Compilers (driver.CcToolchain): Clang, then GCC, then zig cc on PATH
unless the target chooses one. Clang builds for other triples with
--target, zig cc with its own triple spelling; GCC only for the host.
MSVC and clang-cl are refused with a message for now.
- Commands (driver.CcCommands): standard, gnuExtensions, optimize and
debugInfo with their mode defaults, warnings, warningsAsErrors,
position-independent code (sharedlibs, and staticlibs a sharedlib
links unless pic says otherwise), arc, sanitizers, LTO, public and
private defines and headers with those of the libraries, headers
staged under includePrefix/stripIncludePrefix, precompiledHeader as a
forced include, flags; archives with llvm-ar or ar; links with
alwaysLink as whole-archive, links, frameworks on macOS, rpath (and
the directories of shared libraries), linkerScript, exports (version
script or exported symbols), soVersion (soname and the unversioned
name), defFile and subsystem on Windows, strip and linkFlags.
Windows resource scripts are refused with a message for now.
- Zig targets run zig build-exe, build-lib (-dynamic) or test
--test-no-exec on their root file, with the libraries they depend on.
- Incremental (driver.UpToDate): every compile writes a depfile (-MD
-MF); an object is current while its command line is the same and no
input in its depfile is newer. Archives and programs are remade when
their command or an input changed. A failed command is redone.
- Jobs: the job pool and build graph of the legacy Tazel run the
translation units in parallel (execution.jobs, keepGoing).
- data files are copied beside the output.
Tests: depfile reading, up-to-date decisions, globs and excludes, Zig
triples and output names, and a real build with the C compiler of the
machine: a first build, a build with nothing to do, a header change
that recompiles both units, tazel run with args, env and extra
arguments, and a compiler error.
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 |