diff options
| author | Leitwolf11 | 2026-11-12 07:46:46 +0300 |
|---|---|---|
| committer | Leitwolf11 | 2026-11-12 07:46:46 +0300 |
| commit | 3bffdc1d747a73ff83fab83f1a51b53c0ab507d9 (patch) | |
| tree | 7bf8f8b6faa18f8df25af263e22f3c2346d3bba3 | |
| parent | f7545228848e250856b9cdf2cf92538dfda0eb4f (diff) | |
| download | tazel-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.
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 |