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

summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorLeitwolf112026-11-12 07:46:46 +0300
committerLeitwolf112026-11-12 07:46:46 +0300
commit3bffdc1d747a73ff83fab83f1a51b53c0ab507d9 (patch)
tree7bf8f8b6faa18f8df25af263e22f3c2346d3bba3
parentf7545228848e250856b9cdf2cf92538dfda0eb4f (diff)
downloadtazel-3bffdc1d747a73ff83fab83f1a51b53c0ab507d9.tar.gz
Read MODULE.kts and BUILD.kts, and resolve labelsfeature/labels-and-project-files
Tazel now reads the project files of the accepted DSL: - Labels: //:x for the root BUILD.kts, //dir/:x for dir/BUILD.kts, :x for the same file, @name//:x for a dependency, and the patterns //dir/... and //... . The //dir shorthand is refused with a hint. - MODULE.kts: module (name, SemVer version, description, authors, license), dependencies (implementation, testImplementation, localImplementation, localTestImplementation, with alias), viget (push, pushTarget, exclude), cache and execution. Duplicate dependency names, a push without version, description or author, and a pushTarget in a private BUILD.kts are errors. - BUILD.kts: isPublic per file and c, cpp, objc, objcpp and zig targets, each with bin, staticlib, sharedlib and test. The scopes carry type markers of their language and kind, so a setting that one does not have (exports on a bin, args on a staticlib, a C standard on C++, arc on C, compiler on Zig, test settings on a bin, bundle on C++) is a compile error at its line in the project file. - A target block runs for each triple and build mode it is built for; OS and ARCH come from the LLVM triple, BUILD_MODE from the command. Its depends are resolved: libraries only, visibility through isPublic, test dependencies for test targets only, paths kept inside the module. - Directories starting with a dot and nested modules are not packages; file names are compared case-sensitively. The tests load a reference module that uses every setting of the DSL and configure each of its eleven targets for each triple and mode.
-rw-r--r--CHANGELOG.md5
-rw-r--r--build.gradle.kts5
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/dsl/BuildDsl.kt109
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/dsl/Choices.kt135
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/dsl/ModuleDsl.kt345
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/dsl/TargetOptions.kt166
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/dsl/Targets.kt405
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/dsl/Triple.kt78
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/dsl/Values.kt73
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/label/Label.kt122
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/project/Module.kt243
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/project/ModuleLoader.kt136
-rw-r--r--sources/main/kotlin/com/progmasoft/tazel/project/ScriptRunner.kt88
-rw-r--r--sources/test/kotlin/com/progmasoft/tazel/label/LabelTest.kt82
-rw-r--r--sources/test/kotlin/com/progmasoft/tazel/project/ModuleLoaderTest.kt323
-rw-r--r--sources/test/resources/reference/MODULE.kts65
-rw-r--r--sources/test/resources/reference/app/BUILD.kts51
-rw-r--r--sources/test/resources/reference/core/BUILD.kts48
-rw-r--r--sources/test/resources/reference/mac/BUILD.kts28
-rw-r--r--sources/test/resources/reference/tests/BUILD.kts36
-rw-r--r--sources/test/resources/reference/tools/BUILD.kts14
21 files changed, 2556 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16d50dc..532172f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,3 +9,8 @@ Versions follow [Semantic Versioning](https://semver.org). The earlier Tazel, 0.
## Unreleased
- A Kotlin program for the JVM that prints its version and help.
+- Labels: `//:x`, `//dir/:x`, `:x`, `@name//:x`, and the patterns `//dir/...` and `//...`.
+- `MODULE.kts` and `BUILD.kts` are read: the module, its dependencies, `viget`, `cache` and `execution`;
+ `c`, `cpp`, `objc`, `objcpp` and `zig` targets of the four kinds, with `isPublic` per file. A setting that a
+ language or kind does not have is a compile error of the project file. A target block runs for each triple and
+ build mode; its labels are resolved and visibility is checked.
diff --git a/build.gradle.kts b/build.gradle.kts
index 761d4ef..8d3d62b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -55,7 +55,10 @@ sourceSets {
kotlin.srcDir("sources/main/kotlin")
resources.srcDir("sources/main/resources")
}
- test { kotlin.srcDir("sources/test/kotlin") }
+ test {
+ kotlin.srcDir("sources/test/kotlin")
+ resources.srcDir("sources/test/resources")
+ }
}
spotless {
diff --git a/sources/main/kotlin/com/progmasoft/tazel/dsl/BuildDsl.kt b/sources/main/kotlin/com/progmasoft/tazel/dsl/BuildDsl.kt
new file mode 100644
index 0000000..7adba4e
--- /dev/null
+++ b/sources/main/kotlin/com/progmasoft/tazel/dsl/BuildDsl.kt
@@ -0,0 +1,109 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+ * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+ */
+
+package com.progmasoft.tazel.dsl
+
+import com.progmasoft.tazel.label.Labels
+
+/**
+ * A target as its `BUILD.kts` declares it. Its block is not run when the file runs: it runs for
+ * each triple and build mode the target is built for, see [configure].
+ */
+class TargetDeclaration
+internal constructor(
+ val name: String,
+ val language: Language,
+ val kind: TargetKind,
+ private val block: (TargetTriple, BuildMode) -> TargetSettings,
+) {
+ /** What the block sets for [triple] in [mode]. */
+ fun configure(triple: TargetTriple, mode: BuildMode): TargetSettings = block(triple, mode)
+}
+
+/** A `BUILD.kts` after it ran. */
+data class BuildFile(val isPublic: Boolean, val targets: List<TargetDeclaration>)
+
+/** The top level of a `BUILD.kts`. */
+@TazelDsl
+class BuildFileScope internal constructor() {
+ /** false: only targets of this file may depend on its targets; true: every package and module. */
+ var isPublic: Boolean = false
+
+ private val targets = mutableListOf<TargetDeclaration>()
+
+ val c = CcTargets<LanguageMark.C>(Language.C, ::declare)
+ val cpp = CcTargets<LanguageMark.Cpp>(Language.CPP, ::declare)
+ val objc = CcTargets<LanguageMark.ObjC>(Language.OBJC, ::declare)
+ val objcpp = CcTargets<LanguageMark.ObjCpp>(Language.OBJCPP, ::declare)
+ val zig = ZigTargets(::declare)
+
+ private fun declare(declaration: TargetDeclaration) {
+ Labels.requireName(declaration.name, "target name")
+ if (targets.any { it.name == declaration.name }) {
+ throw ProjectException("the target :${declaration.name} is declared twice")
+ }
+ targets += declaration
+ }
+
+ internal fun build() = BuildFile(isPublic, targets.toList())
+}
+
+/** `c`, `cpp`, `objc`, `objcpp`: the four kinds of target of one C-family language. */
+class CcTargets<L : LanguageMark>
+internal constructor(
+ private val language: Language,
+ private val declare: (TargetDeclaration) -> Unit,
+) {
+ fun bin(name: String, block: CcTargetScope<L, KindMark.Bin>.() -> Unit) =
+ add(name, TargetKind.BIN, block)
+
+ fun staticlib(name: String, block: CcTargetScope<L, KindMark.StaticLib>.() -> Unit) =
+ add(name, TargetKind.STATICLIB, block)
+
+ fun sharedlib(name: String, block: CcTargetScope<L, KindMark.SharedLib>.() -> Unit) =
+ add(name, TargetKind.SHAREDLIB, block)
+
+ fun test(name: String, block: CcTargetScope<L, KindMark.Test>.() -> Unit) =
+ add(name, TargetKind.TEST, block)
+
+ private fun <K : KindMark> add(
+ name: String,
+ kind: TargetKind,
+ block: CcTargetScope<L, K>.() -> Unit,
+ ) {
+ declare(
+ TargetDeclaration(name, language, kind) { triple, mode ->
+ CcTargetScope<L, K>(triple, mode).apply(block).settings
+ }
+ )
+ }
+}
+
+/** `zig`: the four kinds of Zig target. */
+class ZigTargets internal constructor(private val declare: (TargetDeclaration) -> Unit) {
+ fun bin(name: String, block: ZigTargetScope<KindMark.Bin>.() -> Unit) =
+ add(name, TargetKind.BIN, block)
+
+ fun staticlib(name: String, block: ZigTargetScope<KindMark.StaticLib>.() -> Unit) =
+ add(name, TargetKind.STATICLIB, block)
+
+ fun sharedlib(name: String, block: ZigTargetScope<KindMark.SharedLib>.() -> Unit) =
+ add(name, TargetKind.SHAREDLIB, block)
+
+ fun test(name: String, block: ZigTargetScope<KindMark.Test>.() -> Unit) =
+ add(name, TargetKind.TEST, block)
+
+ private fun <K : KindMark> add(
+ name: String,
+ kind: TargetKind,
+ block: ZigTargetScope<K>.() -> Unit,
+ ) {
+ declare(
+ TargetDeclaration(name, Language.ZIG, kind) { triple, mode ->
+ ZigTargetScope<K>(triple, mode).apply(block).settings
+ }
+ )
+ }
+}
diff --git a/sources/main/kotlin/com/progmasoft/tazel/dsl/Choices.kt b/sources/main/kotlin/com/progmasoft/tazel/dsl/Choices.kt
new file mode 100644
index 0000000..238ff65
--- /dev/null
+++ b/sources/main/kotlin/com/progmasoft/tazel/dsl/Choices.kt
@@ -0,0 +1,135 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+ * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+ */
+
+package com.progmasoft.tazel.dsl
+
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+
+/** The operating system of a target triple. */
+enum class Os {
+ WINDOWS,
+ LINUX,
+ MACOS,
+ BSD,
+ UNIX,
+}
+
+/** The processor of a target triple. */
+enum class Arch {
+ X86,
+ X86_64,
+ ARM,
+ ARM64,
+ RISCV32,
+ RISCV64,
+ POWERPC64,
+ S390X,
+ LOONGARCH64,
+}
+
+/** Which build a command makes; `tazel build` makes [DEBUG] unless told otherwise. */
+enum class BuildMode {
+ DEBUG,
+ RELEASE,
+}
+
+/** The compiler of a C, C++, Objective-C or Objective-C++ target. */
+enum class Compiler {
+ CLANG,
+ GCC,
+ MSVC,
+ CLANG_CL,
+ ZIG_CC,
+}
+
+/** The language standard of a C or Objective-C target. */
+enum class CStandard {
+ C89,
+ C99,
+ C11,
+ C17,
+ C23,
+}
+
+/** The language standard of a C++ or Objective-C++ target. */
+enum class CppStandard {
+ CPP11,
+ CPP14,
+ CPP17,
+ CPP20,
+ CPP23,
+ CPP26,
+}
+
+/** How many warnings the compiler gives. */
+enum class Warnings {
+ ALL,
+ DEFAULT,
+ NONE,
+}
+
+/** Debug information: none, in the objects, or in separate files. */
+enum class DebugInfo {
+ NONE,
+ FULL,
+ SPLIT,
+}
+
+/** What the optimizer of a C-family compiler aims at. */
+enum class Optimize {
+ NONE,
+ SIZE,
+ SPEED,
+}
+
+/** The release mode of a Zig target: ReleaseSafe, ReleaseFast, ReleaseSmall. */
+enum class ZigOptimize {
+ SAFE,
+ SPEED,
+ SIZE,
+}
+
+/** Link-time optimization. */
+enum class Lto {
+ NONE,
+ THIN,
+ FULL,
+}
+
+/** A sanitizer the target is compiled and linked with. */
+enum class Sanitizer {
+ ADDRESS,
+ UNDEFINED,
+ THREAD,
+ MEMORY,
+ LEAK,
+}
+
+/** The Windows subsystem of a program. */
+enum class Subsystem {
+ CONSOLE,
+ WINDOWS,
+}
+
+/** How big a test is; it chooses the timeout a test has when it sets none. */
+enum class TestSize(val timeout: Duration) {
+ SMALL(1.minutes),
+ MEDIUM(5.minutes),
+ LARGE(15.minutes),
+ ENORMOUS(60.minutes),
+}
+
+/** How `tazel test` treats a test. */
+enum class TestTag {
+ /** Only run when its label is named, never through a pattern. */
+ MANUAL,
+ /** Runs alone, with no other test at the same time. */
+ EXCLUSIVE,
+ /** Runs on this machine, never on remote execution. */
+ LOCAL,
+ /** Its result is never taken from the cache. */
+ NO_CACHE,
+}
diff --git a/sources/main/kotlin/com/progmasoft/tazel/dsl/ModuleDsl.kt b/sources/main/kotlin/com/progmasoft/tazel/dsl/ModuleDsl.kt
new file mode 100644
index 0000000..a4baced
--- /dev/null
+++ b/sources/main/kotlin/com/progmasoft/tazel/dsl/ModuleDsl.kt
@@ -0,0 +1,345 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+ * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+ */
+
+package com.progmasoft.tazel.dsl
+
+import com.progmasoft.tazel.label.Labels
+
+@DslMarker annotation class TazelDsl
+
+/** What `module { }` says about the module. */
+data class ModuleInfo(
+ val name: String,
+ val version: String?,
+ val description: String?,
+ val authors: List<String>,
+ val license: String?,
+)
+
+/** A dependency from ViGet: [name] of [account], at [version], with the label `@[labelName]//`. */
+data class RemoteDependency(
+ val account: String,
+ val name: String,
+ val version: String,
+ val alias: String?,
+ val testOnly: Boolean,
+) {
+ val labelName: String
+ get() = alias ?: name
+}
+
+/** Local `.vipkg` files under one label name, `@[name]//`. */
+data class LocalDependency(
+ val name: String,
+ val packages: List<FileSelection>,
+ val testOnly: Boolean,
+)
+
+/** What `viget { }` publishes. */
+data class VigetSettings(
+ val push: Boolean = false,
+ val pushTargets: List<String> = emptyList(),
+ val excludes: List<FileSelection> = emptyList(),
+)
+
+/** A remote cache. */
+data class RemoteCache(
+ val url: String,
+ val read: Boolean,
+ val write: Boolean,
+ val token: String?,
+ val publicKey: String?,
+ val signingKey: String?,
+)
+
+/** What `cache { }` says. */
+data class CacheSettings(
+ val local: Boolean = true,
+ val keep: Int = 8,
+ val remotes: List<RemoteCache> = emptyList(),
+)
+
+/** A remote execution service. */
+data class RemoteExecution(val url: String, val token: String?)
+
+/** What `execution { }` says; [jobs] `null` means one per processor. */
+data class ExecutionSettings(
+ val jobs: Int? = null,
+ val memory: ByteSize? = null,
+ val keepGoing: Boolean = false,
+ val remotes: List<RemoteExecution> = emptyList(),
+)
+
+/** A `MODULE.kts` after it ran. */
+data class ModuleFile(
+ val info: ModuleInfo,
+ val remoteDependencies: List<RemoteDependency>,
+ val localDependencies: List<LocalDependency>,
+ val viget: VigetSettings,
+ val cache: CacheSettings,
+ val execution: ExecutionSettings,
+) {
+ /** The label names of the dependencies, each with whether only test targets may use it. */
+ val dependencyNames: Map<String, Boolean>
+ get() =
+ remoteDependencies.associate { it.labelName to it.testOnly } +
+ localDependencies.associate { it.name to it.testOnly }
+}
+
+/** The top level of a `MODULE.kts`. */
+@TazelDsl
+class ModuleFileScope internal constructor() {
+ private var info: ModuleInfo? = null
+ private var dependencies: DependenciesScope? = null
+ private var viget: VigetSettings? = null
+ private var cache: CacheSettings? = null
+ private var execution: ExecutionSettings? = null
+
+ fun module(block: ModuleScope.() -> Unit) {
+ once(info, "module")
+ info = ModuleScope().apply(block).build()
+ }
+
+ fun dependencies(block: DependenciesScope.() -> Unit) {
+ once(dependencies, "dependencies")
+ dependencies = DependenciesScope().apply(block)
+ }
+
+ fun viget(block: VigetScope.() -> Unit) {
+ once(viget, "viget")
+ viget = VigetScope().apply(block).build()
+ }
+
+ fun cache(block: CacheScope.() -> Unit) {
+ once(cache, "cache")
+ cache = CacheScope().apply(block).build()
+ }
+
+ fun execution(block: ExecutionScope.() -> Unit) {
+ once(execution, "execution")
+ execution = ExecutionScope().apply(block).build()
+ }
+
+ internal fun build(): ModuleFile {
+ val info = info ?: throw ProjectException("MODULE.kts has no module { name = ... }")
+ val dependencies = dependencies ?: DependenciesScope()
+ val viget = viget ?: VigetSettings()
+ val file =
+ ModuleFile(
+ info,
+ dependencies.remote,
+ dependencies.local,
+ viget,
+ cache ?: CacheSettings(),
+ execution ?: ExecutionSettings(),
+ )
+ val names =
+ file.remoteDependencies.map { it.labelName } + file.localDependencies.map { it.name }
+ names
+ .groupBy { it }
+ .filterValues { it.size > 1 }
+ .keys
+ .firstOrNull()
+ ?.let { throw ProjectException("two dependencies are called @$it; give one an alias") }
+ if (viget.push) {
+ if (info.version == null) throw ProjectException("viget { push = true } needs module.version")
+ if (info.description == null) {
+ throw ProjectException("viget { push = true } needs module.description")
+ }
+ if (info.authors.isEmpty()) {
+ throw ProjectException("viget { push = true } needs at least one module author")
+ }
+ if (viget.pushTargets.isEmpty()) {
+ throw ProjectException("viget { push = true } needs pushTarget(...)")
+ }
+ }
+ return file
+ }
+
+ private fun once(previous: Any?, block: String) {
+ if (previous != null) throw ProjectException("MODULE.kts has $block { } twice")
+ }
+}
+
+@TazelDsl
+class ModuleScope internal constructor() {
+ var name: String? = null
+ var version: String? = null
+ var description: String? = null
+ var license: String? = null
+ private val authors = mutableListOf<String>()
+
+ fun authors(vararg names: String) {
+ names.forEach { authors += requireText(it, "an author") }
+ }
+
+ internal fun build(): ModuleInfo =
+ ModuleInfo(
+ name = Labels.requireName(name ?: throw ProjectException("module { } has no name"), "module"),
+ version = version?.let { requireSemVer(it, "module.version") },
+ description = description?.let { requireText(it, "module.description") },
+ authors = authors.toList(),
+ license = license?.let { requireText(it, "module.license") },
+ )
+}
+
+@TazelDsl
+class DependenciesScope internal constructor() {
+ internal val remote = mutableListOf<RemoteDependency>()
+ internal val local = mutableListOf<LocalDependency>()
+
+ /** A package of [account] on ViGet that every target may depend on. */
+ fun implementation(account: String, block: RemoteDependencyScope.() -> Unit) {
+ remote += RemoteDependencyScope().apply(block).build(account, testOnly = false)
+ }
+
+ /** A package of [account] on ViGet that only test targets may depend on. */
+ fun testImplementation(account: String, block: RemoteDependencyScope.() -> Unit) {
+ remote += RemoteDependencyScope().apply(block).build(account, testOnly = true)
+ }
+
+ /** Local `.vipkg` files that every target may depend on. */
+ fun localImplementation(block: LocalDependencyScope.() -> Unit) {
+ local += LocalDependencyScope().apply(block).build(testOnly = false)
+ }
+
+ /** Local `.vipkg` files that only test targets may depend on. */
+ fun localTestImplementation(block: LocalDependencyScope.() -> Unit) {
+ local += LocalDependencyScope().apply(block).build(testOnly = true)
+ }
+}
+
+@TazelDsl
+class RemoteDependencyScope internal constructor() {
+ var name: String? = null
+ var version: String? = null
+ /** The label name instead of [name]: `@alias//:target`. */
+ var alias: String? = null
+
+ internal fun build(account: String, testOnly: Boolean): RemoteDependency {
+ val name =
+ Labels.requireName(
+ name ?: throw ProjectException("a dependency of $account has no name"),
+ "dependency",
+ )
+ return RemoteDependency(
+ account = Labels.requireName(account, "account"),
+ name = name,
+ version =
+ requireSemVer(
+ version ?: throw ProjectException("the dependency $account/$name has no version"),
+ "the version of $account/$name",
+ ),
+ alias = alias?.let { Labels.requireName(it, "alias") },
+ testOnly = testOnly,
+ )
+ }
+}
+
+@TazelDsl
+class LocalDependencyScope internal constructor() {
+ var name: String? = null
+ private val packages = mutableListOf<FileSelection>()
+
+ fun include(vararg paths: String) {
+ paths.forEach { packages += FilePath(requireRelativePath(it, "local package")) }
+ }
+
+ fun include(vararg globs: Glob) {
+ packages += globs
+ }
+
+ internal fun build(testOnly: Boolean): LocalDependency {
+ val name =
+ Labels.requireName(
+ name ?: throw ProjectException("a local dependency has no name"),
+ "dependency",
+ )
+ if (packages.isEmpty()) throw ProjectException("the local dependency $name includes nothing")
+ return LocalDependency(name, packages.toList(), testOnly)
+ }
+}
+
+@TazelDsl
+class VigetScope internal constructor() {
+ var push: Boolean = false
+ private val targets = mutableListOf<String>()
+ private val excludes = mutableListOf<FileSelection>()
+
+ /** The targets the package on ViGet holds; their `BUILD.kts` must be public. */
+ fun pushTarget(vararg labels: String) {
+ targets += labels
+ }
+
+ fun exclude(vararg paths: String) {
+ paths.forEach { excludes += FilePath(requireRelativePath(it, "viget exclude")) }
+ }
+
+ fun exclude(vararg globs: Glob) {
+ excludes += globs
+ }
+
+ internal fun build() = VigetSettings(push, targets.toList(), excludes.toList())
+}
+
+@TazelDsl
+class CacheScope internal constructor() {
+ var local: Boolean = true
+ /** How many results each target keeps in the local cache. */
+ var keep: Int = 8
+ private val remotes = mutableListOf<RemoteCache>()
+
+ fun remote(url: String, block: RemoteCacheScope.() -> Unit = {}) {
+ remotes += RemoteCacheScope().apply(block).build(requireUrl(url))
+ }
+
+ internal fun build(): CacheSettings {
+ if (keep < 1) throw ProjectException("cache.keep is $keep; it keeps at least 1")
+ return CacheSettings(local, keep, remotes.toList())
+ }
+}
+
+@TazelDsl
+class RemoteCacheScope internal constructor() {
+ var read: Boolean = true
+ var write: Boolean = false
+ var token: String? = null
+ var publicKey: String? = null
+ var signingKey: String? = null
+
+ internal fun build(url: String) = RemoteCache(url, read, write, token, publicKey, signingKey)
+}
+
+@TazelDsl
+class ExecutionScope internal constructor() {
+ var jobs: Int? = null
+ var memory: ByteSize? = null
+ var keepGoing: Boolean = false
+ private val remotes = mutableListOf<RemoteExecution>()
+
+ fun remote(url: String, block: RemoteExecutionScope.() -> Unit = {}) {
+ remotes += RemoteExecutionScope().apply(block).build(requireUrl(url))
+ }
+
+ internal fun build(): ExecutionSettings {
+ jobs?.let { if (it < 1) throw ProjectException("execution.jobs is $it; it runs at least 1") }
+ memory?.let {
+ if (it.bytes <= 0) throw ProjectException("execution.memory must be more than 0 bytes")
+ }
+ return ExecutionSettings(jobs, memory, keepGoing, remotes.toList())
+ }
+}
+
+@TazelDsl
+class RemoteExecutionScope internal constructor() {
+ var token: String? = null
+
+ internal fun build(url: String) = RemoteExecution(url, token)
+}
+
+private fun requireUrl(url: String): String {
+ if (!url.startsWith("https://")) throw ProjectException("'$url' is not an https:// address")
+ return url
+}
diff --git a/sources/main/kotlin/com/progmasoft/tazel/dsl/TargetOptions.kt b/sources/main/kotlin/com/progmasoft/tazel/dsl/TargetOptions.kt
new file mode 100644
index 0000000..b713005
--- /dev/null
+++ b/sources/main/kotlin/com/progmasoft/tazel/dsl/TargetOptions.kt
@@ -0,0 +1,166 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+ * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+ */
+
+// Settings only some languages or kinds have. Each is an extension on the target scope of exactly
+// those, so writing one in another target is a compile error of the project file.
+
+package com.progmasoft.tazel.dsl
+
+import kotlin.time.Duration
+
+// ----- By language -----
+
+/** Default: the newest standard the compiler supports. */
+var CcTargetScope<LanguageMark.CStandardFamily, KindMark>.standard: CStandard?
+ @JvmName("getCStandard") get() = settings.cStandard
+ @JvmName("setCStandard")
+ set(value) {
+ settings = settings.copy(cStandard = value)
+ }
+
+/** Default: the newest standard the compiler supports. */
+var CcTargetScope<LanguageMark.CppStandardFamily, KindMark>.standard: CppStandard?
+ @JvmName("getCppStandard") get() = settings.cppStandard
+ @JvmName("setCppStandard")
+ set(value) {
+ settings = settings.copy(cppStandard = value)
+ }
+
+/** Automatic reference counting; default: true. */
+var CcTargetScope<LanguageMark.Objective, KindMark>.arc: Boolean
+ get() = settings.arc
+ set(value) {
+ settings = settings.copy(arc = value)
+ }
+
+/** Makes the program a `.app` bundle; default: a plain program. */
+fun CcTargetScope<LanguageMark.Objective, KindMark.Bin>.bundle(block: BundleScope.() -> Unit) {
+ if (settings.bundle != null) throw ProjectException("bundle { } is written twice")
+ settings = settings.copy(bundle = BundleScope().apply(block).build())
+}
+
+/** The C header a Zig library offers. */
+fun ZigTargetScope<KindMark.Library>.headers(vararg directories: String) {
+ settings =
+ settings.copy(
+ headers = settings.headers + directories.map { requireRelativePath(it, "header directory") }
+ )
+}
+
+// ----- By kind -----
+
+/** What `tazel run` and `tazel test` pass to the program. */
+fun TargetScope<KindMark.Runnable>.args(vararg arguments: String) {
+ settings = settings.copy(args = settings.args + arguments)
+}
+
+/** The environment `tazel run` and `tazel test` give the program. */
+fun TargetScope<KindMark.Runnable>.env(vararg variables: Pair<String, String>) {
+ variables.forEach { (name, _) -> requireText(name, "an environment variable name") }
+ settings = settings.copy(env = settings.env + variables)
+}
+
+/** Directories searched for shared libraries when the output runs, such as `$ORIGIN/../lib`. */
+fun CcTargetScope<LanguageMark, KindMark.Linked>.rpath(vararg directories: String) {
+ settings = settings.copy(rpath = settings.rpath + directories.map { requireText(it, "an rpath") })
+}
+
+var CcTargetScope<LanguageMark, KindMark.Linked>.linkerScript: String?
+ get() = settings.linkerScript
+ set(value) {
+ settings = settings.copy(linkerScript = value?.let { requireRelativePath(it, "linkerScript") })
+ }
+
+/** Windows resource scripts (`.rc`): icon, version, manifest. */
+fun CcTargetScope<LanguageMark, KindMark.Linked>.resources(vararg scripts: String) {
+ settings =
+ settings.copy(
+ resources = settings.resources + scripts.map { requireRelativePath(it, "resource script") }
+ )
+}
+
+/** The Windows subsystem; default: CONSOLE. */
+var CcTargetScope<LanguageMark, KindMark.Bin>.subsystem: Subsystem
+ get() = settings.subsystem
+ set(value) {
+ settings = settings.copy(subsystem = value)
+ }
+
+/** Position-independent code; default: true when a sharedlib depends on it. */
+var CcTargetScope<LanguageMark, KindMark.StaticLib>.pic: Boolean?
+ get() = settings.pic
+ set(value) {
+ settings = settings.copy(pic = value)
+ }
+
+/** Links every object of the archive (whole-archive); default: false. */
+var CcTargetScope<LanguageMark, KindMark.StaticLib>.alwaysLink: Boolean
+ get() = settings.alwaysLink
+ set(value) {
+ settings = settings.copy(alwaysLink = value)
+ }
+
+/** Symbol patterns the library exports; default: every symbol that is not hidden. */
+fun CcTargetScope<LanguageMark, KindMark.SharedLib>.exports(vararg patterns: String) {
+ settings =
+ settings.copy(exports = settings.exports + patterns.map { requireText(it, "an export") })
+}
+
+/** `libname.so.1`; default: none. */
+var CcTargetScope<LanguageMark, KindMark.SharedLib>.soVersion: String?
+ get() = settings.soVersion
+ set(value) {
+ settings = settings.copy(soVersion = value?.let { requireText(it, "soVersion") })
+ }
+
+/** The Windows module-definition file. */
+var CcTargetScope<LanguageMark, KindMark.SharedLib>.defFile: String?
+ get() = settings.defFile
+ set(value) {
+ settings = settings.copy(defFile = value?.let { requireRelativePath(it, "defFile") })
+ }
+
+// ----- Tests -----
+
+/** Default: MEDIUM. */
+var TargetScope<KindMark.Test>.size: TestSize
+ get() = settings.size
+ set(value) {
+ settings = settings.copy(size = value)
+ }
+
+/** Default: the timeout of [size]. */
+var TargetScope<KindMark.Test>.timeout: Duration?
+ get() = settings.timeout
+ set(value) {
+ if (value != null && !value.isPositive()) throw ProjectException("a test timeout is positive")
+ settings = settings.copy(timeout = value)
+ }
+
+/** A failing flaky test is run again, up to 3 times. */
+var TargetScope<KindMark.Test>.flaky: Boolean
+ get() = settings.flaky
+ set(value) {
+ settings = settings.copy(flaky = value)
+ }
+
+/** Parts the test is split into (`TEST_TOTAL_SHARDS`, `TEST_SHARD_INDEX`); default: 1. */
+var TargetScope<KindMark.Test>.shards: Int
+ get() = settings.shards
+ set(value) {
+ if (value < 1) throw ProjectException("shards is $value; a test has at least 1")
+ settings = settings.copy(shards = value)
+ }
+
+fun TargetScope<KindMark.Test>.tags(vararg tags: TestTag) {
+ settings = settings.copy(tags = settings.tags + tags)
+}
+
+/** Links the libraries of the test statically; default: true. */
+var CcTargetScope<LanguageMark, KindMark.Test>.linkStatic: Boolean
+ get() = settings.linkStatic
+ set(value) {
+ settings = settings.copy(linkStatic = value)
+ }
diff --git a/sources/main/kotlin/com/progmasoft/tazel/dsl/Targets.kt b/sources/main/kotlin/com/progmasoft/tazel/dsl/Targets.kt
new file mode 100644
index 0000000..439cda6
--- /dev/null
+++ b/sources/main/kotlin/com/progmasoft/tazel/dsl/Targets.kt
@@ -0,0 +1,405 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
+ * SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+ */
+
+package com.progmasoft.tazel.dsl
+
+import kotlin.time.Duration
+
+/** The language a target compiles. */
+enum class Language(val keyword: String) {
+ C("c"),
+ CPP("cpp"),
+ OBJC("objc"),
+ OBJCPP("objcpp"),
+ ZIG("zig"),
+}
+
+/** What a target produces. */
+enum class TargetKind(val keyword: String) {
+ BIN("bin"),
+ STATICLIB("staticlib"),
+ SHAREDLIB("sharedlib"),
+ TEST("test");
+
+ val isLibrary: Boolean
+ get() = this == STATICLIB || this == SHAREDLIB
+}
+
+/**
+ * Type markers of the language of a target scope. They are never instantiated; the scopes carry
+ * them so that a setting a language does not have is a compile error in the project file.
+ */
+sealed interface LanguageMark {
+ sealed interface CStandardFamily : LanguageMark
+
+ sealed interface CppStandardFamily : LanguageMark
+
+ sealed interface Objective : LanguageMark
+
+ interface C : CStandardFamily
+
+ interface Cpp : CppStandardFamily
+
+ interface ObjC : CStandardFamily, Objective
+
+ interface ObjCpp : CppStandardFamily, Objective
+
+ interface Zig : LanguageMark
+}
+
+/** Type markers of the kind of a target scope, like [LanguageMark]. */
+sealed interface KindMark {
+ /** bin and test: Tazel runs them. */
+ sealed interface Runnable : Ki