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

summaryrefslogtreecommitdiffhomepage
path: root/sources/test
diff options
context:
space:
mode:
authorLeitwolf112026-11-12 22:19:46 +0300
committerLeitwolf112026-11-12 22:19:46 +0300
commit7cb809c6560fc4ed29f41e4e4199cd91af6cbed1 (patch)
treeaffc0bfef110459bc8fcf3e8f197ed95cc46f6bb /sources/test
parent95cdafcf1aa01b5d32171c56e6a94137402643b9 (diff)
downloadtazel-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.
Diffstat (limited to 'sources/test')
-rw-r--r--sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt165
-rw-r--r--sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt113
2 files changed, 278 insertions, 0 deletions
diff --git a/sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt b/sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt
new file mode 100644
index 0000000..36e1323
--- /dev/null
+++ b/sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt
@@ -0,0 +1,165 @@
+/*
+ * 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.GatheredRun
+import com.progmasoft.tazel.driver.Processes
+import com.progmasoft.tazel.driver.Programs
+import com.progmasoft.tazel.driver.SystemProcesses
+import com.progmasoft.tazel.dsl.TargetTriple
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.test.Test
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+/**
+ * Builds and runs a small module with the C compiler of this machine. Without Clang or GCC on PATH
+ * there is nothing to build with, and the tests return early.
+ */
+class BuildCommandTest {
+ private val os = TargetTriple.host().os
+ private val programs = Programs(os)
+ private val compiler = programs.find("clang") ?: programs.find("gcc")
+
+ /** Real processes; what a program `tazel run` starts prints is kept instead of shown. */
+ private class Recording : Processes by SystemProcesses {
+ var printed = ""
+
+ override fun runWith(
+ command: List<String>,
+ directory: Path,
+ environment: Map<String, String>,
+ ): Int {
+ val result: GatheredRun = SystemProcesses.runGatheredWith(command, directory, environment)
+ printed += result.output
+ return result.exitCode
+ }
+ }
+
+ private fun module(): Path {
+ val root = Files.createTempDirectory("tazel-build-").toRealPath()
+ fun write(path: String, text: String) {
+ val file = root.resolve(path)
+ Files.createDirectories(file.parent)
+ Files.writeString(file, text.trimIndent() + "\n")
+ }
+ write("MODULE.kts", """module { name = "sample" }""")
+ write(
+ "core/BUILD.kts",
+ """
+ isPublic = true
+ c.staticlib("core") {
+ sources(glob("src/*.c"))
+ headers("include")
+ stripIncludePrefix = "include"
+ includePrefix = "sample"
+ defines("SAMPLE_OFFSET=1")
+ }
+ """,
+ )
+ write("core/include/core.h", "int sample_add(int a, int b);")
+ write(
+ "core/src/core.c",
+ "#include <sample/core.h>\nint sample_add(int a, int b) { return a + b + SAMPLE_OFFSET - 1; }",
+ )
+ write(
+ "app/BUILD.kts",
+ """
+ c.bin("app") {
+ sources("main.c")
+ depends("//core/:core")
+ args("--given")
+ env("SAMPLE_GREETING" to "hi")
+ }
+ """,
+ )
+ write(
+ "app/main.c",
+ """
+ #include <stdio.h>
+ #include <stdlib.h>
+ #include <sample/core.h>
+ int main(int argc, char **argv) {
+ printf("%s %d", getenv("SAMPLE_GREETING"), sample_add(2, 3));
+ for (int i = 1; i < argc; ++i) printf(" %s", argv[i]);
+ printf("\n");
+ return 0;
+ }
+ """,
+ )
+ return root
+ }
+
+ private fun tazel(
+ root: Path,
+ directory: String,
+ processes: Processes,
+ vararg arguments: String,
+ ): Pair<Int, Recorder> {
+ val recorder = Recorder()
+ val code =
+ run(
+ arguments.toList(),
+ recorder.out,
+ recorder.err,
+ Environment(root.resolve(directory), os, programs, processes),
+ )
+ return code to recorder
+ }
+
+ @Test
+ fun buildsRunsAndRebuildsOnlyWhatChanged() {
+ if (compiler == null) return
+ val root = module()
+ val processes = Recording()
+
+ val (first, firstOutput) = tazel(root, ".", processes, "build", "//...")
+ assertEquals(ExitCode.SUCCESS, first, firstOutput.stderr() + firstOutput.stdout())
+ assertContains(firstOutput.stdout(), "4 command(s) run")
+
+ val (again, againOutput) = tazel(root, ".", processes, "build", "//...")
+ assertEquals(ExitCode.SUCCESS, again, againOutput.stderr())
+ assertContains(againOutput.stdout(), "0 command(s) run")
+
+ // A header change recompiles what includes it, and relinks.
+ val header = root.resolve("core/include/core.h")
+ Files.writeString(header, "int sample_add(int a, int b); /* changed */\n")
+ Files.setLastModifiedTime(
+ header,
+ java.nio.file.attribute.FileTime.fromMillis(System.currentTimeMillis() + 5_000),
+ )
+ val (changed, changedOutput) = tazel(root, ".", processes, "build", "//...")
+ assertEquals(ExitCode.SUCCESS, changed, changedOutput.stderr())
+ assertContains(changedOutput.stdout(), "4 command(s) run")
+
+ val (ran, ranOutput) = tazel(root, "app", processes, "run", "--", "extra")
+ assertEquals(ExitCode.SUCCESS, ran, ranOutput.stderr())
+ assertEquals("hi 5 --given extra", processes.printed.trim())
+ }
+
+ @Test
+ fun aCompilerErrorFailsTheBuildWithItsMessage() {
+ if (compiler == null) return
+ val root = module()
+ Files.writeString(root.resolve("app/main.c"), "int main(void) { return undeclared; }\n")
+ val (code, output) = tazel(root, ".", Recording(), "build", "//app/:app")
+ assertEquals(ExitCode.FAILURE, code)
+ assertContains(output.stdout(), "undeclared")
+ assertContains(output.stderr(), "//app/:app")
+ }
+
+ @Test
+ fun commandLineMistakesAreUsageErrors() {
+ val root = module()
+ assertEquals(ExitCode.USAGE, tazel(root, ".", Recording(), "build", "--fast").first)
+ assertEquals(ExitCode.USAGE, tazel(root, ".", Recording(), "build", "//core").first)
+ val (code, output) = tazel(root, ".", Recording(), "run", "//core/:core")
+ assertEquals(ExitCode.FAILURE, code)
+ assertTrue("no bin target" in output.stderr(), output.stderr())
+ }
+}
diff --git a/sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt b/sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt
new file mode 100644
index 0000000..5f7d831
--- /dev/null
+++ b/sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.FilePath
+import com.progmasoft.tazel.dsl.Glob
+import com.progmasoft.tazel.dsl.ProjectException
+import com.progmasoft.tazel.dsl.TargetTriple
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.attribute.FileTime
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class DriverTest {
+ @Test
+ fun depfilesAreReadWithTheirEscapes() {
+ val text = "obj/a.c.o: src/a.c include/a\\ b.h \\\n include/c.h \\\r\n /usr/include/stdio.h\n"
+ assertEquals(
+ listOf("src/a.c", "include/a b.h", "include/c.h", "/usr/include/stdio.h"),
+ depfileInputs(text),
+ )
+ assertEquals(listOf("C:\\src\\a.c"), depfileInputs("C:\\out\\a.obj: C:\\src\\a.c\n"))
+ }
+
+ @Test
+ fun anOutputIsCurrentUntilItsCommandOrAnInputChanges() {
+ val directory = Files.createTempDirectory("tazel-up-to-date")
+ val input = Files.writeString(directory.resolve("a.c"), "int a;")
+ val output = Files.writeString(directory.resolve("a.o"), "object")
+ Files.setLastModifiedTime(input, FileTime.fromMillis(1_000_000))
+ Files.setLastModifiedTime(output, FileTime.fromMillis(2_000_000))
+ val command = listOf("cc", "-c", "a.c")
+ assertFalse(UpToDate.check(output, command, listOf(input)), "never recorded")
+ UpToDate.record(output, command)
+ assertTrue(UpToDate.check(output, command, listOf(input)))
+ assertFalse(UpToDate.check(output, command + "-O2", listOf(input)), "another command")
+ Files.setLastModifiedTime(input, FileTime.fromMillis(3_000_000))
+ assertFalse(UpToDate.check(output, command, listOf(input)), "a newer input")
+ Files.setLastModifiedTime(input, FileTime.fromMillis(1_000_000))
+ assertFalse(
+ UpToDate.check(output, command, listOf(directory.resolve("gone.h"))),
+ "a missing input",
+ )
+ UpToDate.forget(output)
+ assertFalse(UpToDate.check(output, command, listOf(input)), "forgotten")
+ }
+
+ @Test
+ fun globsTakeFilesBelowThePackageAndExcludesRemoveThem() {
+ val root = Files.createTempDirectory("tazel-globs")
+ for (file in
+ listOf("src/a.c", "src/deep/b.c", "src/experimental/x.c", "src/a.h", "other/c.c", "top.c")) {
+ Files.createDirectories(root.resolve(file).parent)
+ Files.writeString(root.resolve(file), "")
+ }
+ val sets = FileSets(ignoreCase = false)
+ fun names(files: List<Path>) = files.map { root.relativize(it).toString().replace('\\', '/') }
+ assertEquals(
+ listOf("src/a.c", "src/deep/b.c", "src/experimental/x.c"),
+ names(sets.expand(root, listOf(Glob(listOf("src/**/*.c"))))),
+ )
+ assertEquals(
+ listOf("src/a.c", "src/deep/b.c"),
+ names(
+ sets.expand(
+ root,
+ listOf(Glob(listOf("src/**/*.c"))),
+ listOf(Glob(listOf("src/experimental"))),
+ )
+ ),
+ )
+ assertEquals(listOf("src/a.c"), names(sets.expand(root, listOf(Glob(listOf("src/*.c"))))))
+ assertEquals(
+ listOf("top.c", "other/c.c"),
+ names(sets.expand(root, listOf(FilePath("top.c"), Glob(listOf("other/?.c"))))),
+ )
+ val sub = root.resolve("src/deep")
+ assertEquals(listOf("other/c.c"), names(sets.expand(sub, listOf(FilePath("../../other/c.c")))))
+ assertFailsWith<ProjectException> { sets.expand(root, listOf(FilePath("missing.c"))) }
+ assertEquals(emptyList(), sets.expand(root, listOf(Glob(listOf("nowhere/*.c")))))
+ }
+
+ @Test
+ fun zigSpellsTriplesItsOwnWay() {
+ fun zig(text: String) = zigTriple(TargetTriple.parse(text))
+ assertEquals("x86_64-linux-gnu", zig("x86_64-unknown-linux-gnu"))
+ assertEquals("aarch64-macos", zig("aarch64-apple-darwin"))
+ assertEquals("aarch64-macos", zig("arm64-apple-macosx14.0"))
+ assertEquals("x86_64-windows-msvc", zig("x86_64-pc-windows-msvc"))
+ assertEquals("x86_64-freebsd", zig("x86_64-unknown-freebsd14"))
+ }
+
+ @Test
+ fun outputNamesFollowTheSystemOfTheTriple() {
+ val linux = TargetTriple.parse("x86_64-unknown-linux-gnu").os
+ val windows = TargetTriple.parse("x86_64-pc-windows-msvc").os
+ val mac = TargetTriple.parse("aarch64-apple-darwin").os
+ assertEquals("libcore.a", OutputNames.staticLibrary("core", linux))
+ assertEquals("core.lib", OutputNames.staticLibrary("core", windows))
+ assertEquals("libcore.so.1", OutputNames.sharedLibrary("core", linux, "1"))
+ assertEquals("libcore.dylib", OutputNames.sharedLibrary("core", mac, null))
+ assertEquals("core.dll", OutputNames.sharedLibrary("core", windows, "1"))
+ assertEquals("app.exe", OutputNames.executable("app", windows))
+ assertEquals("app", OutputNames.executable("app", mac))
+ }
+}