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 /sources/test | |
| 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.
Diffstat (limited to 'sources/test')
| -rw-r--r-- | sources/test/kotlin/com/progmasoft/tazel/cli/BuildCommandTest.kt | 165 | ||||
| -rw-r--r-- | sources/test/kotlin/com/progmasoft/tazel/driver/DriverTest.kt | 113 |
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)) + } +} |