Skip to content

feat(compilerlib): give each compilation a working directory of its own - #262

Draft
SizzleUnrlsd wants to merge 24 commits into
mainfrom
feat/per-compilation-working-directory
Draft

SizzleUnrlsd wants to merge 24 commits into
mainfrom
feat/per-compilation-working-directory

Conversation

@SizzleUnrlsd

@SizzleUnrlsd SizzleUnrlsd commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

With -working-directory <dir>, compilerlib::compile moved the whole process to <dir> and left it there (#261). A host that compiles the commands of a compilation database, each from its own directory, therefore serialized its compilations under a lock, as coretrace-stack-analyzer does. Each compilation now reads and writes files through a file system of its own, whose working directory -working-directory moves while the process's stays: compilations can run from several threads at once.

Closes #261

Behaviour changes

  • The process stays where it is after a compilation with -working-directory whose jobs all run in this process: a compilation to a file, to memory, instrumented or not. Before, it was left in <dir>. This is what lets compilations run at once.
  • Unchanged: linking and assembling. A job that runs as a program of its own (the linker, the assembler, and every job with -fno-integrated-cc1) starts in the process's working directory. For such a compilation, the process still moves to <dir> and stays there, as clang does. No compilation that worked before is refused.
  • Fixed: a relative <dir>. It failed with error reading 'unit.c': the driver moved the process to it, and the -cc1 job, which received it unchanged, resolved it again from there.
  • Fixed: objects linked alone. -working-directory <dir> unit.o main.o -o program failed with failed to inspect object: unit.o: compilerlib read the objects, to choose between the C and C++ drivers, from the process's directory.

Change

  • test(compilerlib): ... (4 commits): a link, an assembly, the output and dependency file of a plain and of an instrumented compilation, and a sanitizer's ignore list, all with -working-directory. They pass on main and pin what the change must keep.
  • refactor(compilerlib): make the -working-directory arguments absolute and fix(compilerlib): resolve a relative -working-directory once: the option is made absolute before the driver reads it.
  • fix(compilerlib): inspect objects linked alone in -working-directory.
  • refactor(compilerlib): resolve the files a -cc1 job writes in a helper: the output, dependency file, header list, optimization record, split DWARF, stack usage, serialized diagnostics and time trace, which Clang writes relative to the process's directory.
  • feat(compilerlib): give each compilation a working directory of its own:
    • the compilation's file system comes from llvm::vfs::createPhysicalFileSystem, and the driver moves its working directory, not the process's;
    • the -cc1 jobs the driver runs in this process read through it from LLVM 16 on. Before LLVM 22, such a job creates its own file system, the process's, unless it is given one: a sanitizer's ignore list given by a relative path would then be looked up in the process's directory, and the job would end the host process (see the mutations below);
    • the files a -cc1 job writes are made absolute against the directory;
    • a compilation with a job that runs as a program moves the process before the jobs run.
  • docs(compilerlib) and docs(readme): what compile guarantees with threads, and its limits.
  • fix(compilerlib): resolve the statistics and module cache of -cc1 jobs, refactor(compilerlib): make the files the driver writes absolute and fix(compilerlib): write the -MJ fragment in the -working-directory: files written relative to the process's directory, found by testing against main after the first version of this branch. The -cc1 job writes the statistics of -save-stats, the graph of -dependency-dot and its module cache; the driver itself writes the fragment of -MJ and the directory of -gen-cdb-fragment-path. With -working-directory, main wrote them in that directory, since the process had moved there; they are now made absolute against it.
  • ci(build): run the -working-directory tests under ThreadSanitizer: a job that builds compilerlib and its unit tests with ThreadSanitizer.

Limits

  • Compilations that move the process, those with a job that runs as a program, must not run while another compilation does.
  • Options that set LLVM's state for the whole process. A compilation that passes one of them must not run while another one does. In clang 20.1.2:
    • -mllvm is parsed into LLVM's command-line options in ExecuteCompilerInvocation, without a lock;
    • -mdebug-pass and -mlimit-float-precision are parsed the same way in setCommandLineOpts of BackendUtil.cpp, whose comment says that the parser "is not thread-safe";
    • -ftime-report sets llvm::TimePassesIsEnabled and llvm::TimePassesPerRun in the constructor of BackendConsumer (CodeGenAction.cpp).
  • Two writes of every compilation. That constructor writes both globals in every compilation, false without -ftime-report: compilations that run at once write the same value at the same time. This is a data race in Clang's code, benign in effect but undefined by the C++ standard. ThreadSanitizer cannot see it here, since Clang is not instrumented.
  • What ThreadSanitizer covers. The existing job instruments the runtime and its logger only. The new job instruments compilerlib, its unit tests, the logger and GoogleTest. LLVM and Clang come from Ubuntu packages built without ThreadSanitizer: a race between two compilations inside their code goes unseen, unless one side of it is in compilerlib or in a function ThreadSanitizer intercepts. Covering them takes an LLVM and a Clang built with LLVM_USE_SANITIZER=Thread.
  • ThreadSanitizer did not run locally: on macOS 27.0.1, every program built with it, even a ten-line one, crashes at start with Apple clang, LLVM 20 and LLVM 18. The new job is its only run.

Tests

Local runs, on macOS arm64 with AppleClang and LLVM 20.1.2.

The new compile tests against main's compilerlib:

Test main
Link, assembly, output and dependency file (plain, instrumented), ignore list pass
RelativeDirectoryIsRelativeToTheProcess fails: error reading 'unit.c'
ObjectsAreLinkedFromTheDirectory fails: failed to inspect object: unit.o
ProcessStaysInItsDirectory (plain, instrumented) fails: the process is left in the project's directory
ConcurrentCompilationTest (plain, instrumented): 8 threads, 3 rounds each fails: the process is left in one of the projects' directories. Each thread got the IR of its own header in this run.
CompilationDatabaseFragmentIsWrittenInTheDirectory (-MJ), StatisticsAreWrittenInTheDirectory (-save-stats=cwd) pass. Both failed on the first version of this branch, b05bd18: the files landed in the process's directory.

Mutations of this branch, each caught by the tests named:

Mutation Failing tests
The -cc1 job keeps its own file system before LLVM 22 IgnoreListIsReadInTheDirectory ends the test process: LLVM ERROR: can't open file 'ignore.txt'
Objects inspected from the process's directory ObjectsAreLinkedFromTheDirectory
-working-directory passed on as given RelativeDirectoryIsRelativeToTheProcess
The process does not move for jobs that run as programs the link, the assembly, the objects linked alone
The files a -cc1 job writes are not resolved the dependency file of the plain compilation, the object and dependency file of the instrumented one

Each commit builds, and passes the working-directory, concurrency and argument tests it has. ctest -j2 at the head: 277/277, the 257 of main plus 20 new tests.

clang-format 18, scripts/check-license-compliance.sh and the commit checker pass.

Measurements

coretrace-stack-analyzer db7fb25, with coretrace-compiler built from main (f0a22d9), against the same analyzer compiling in parallel through this branch (its draft change passes each compile command's directory as -working-directory). macOS arm64 (M3, 8 cores), LLVM 20.1.2, --format=json --timing, no IR cache, an empty resource summary cache. Medians of 5 runs, 3 for the 53 sources; the builds alternate within each repetition.

Sources --jobs main this branch Loading: main, this branch
Lua 5.4.6, 34 C files 1 7.70 s 7.71 s 1.14 s, 1.17 s
2 7.68 s 7.51 s 1.12 s, 0.90 s
8 C++ files, each including 11 standard headers 1 4.27 s 4.07 s 3.81 s, 3.68 s
2 4.39 s 2.61 s 4.03 s, 2.22 s
The analyzer's 53 C++ sources 1 55.0 s 55.1 s 44.1 s, 43.9 s
2 52.3 s 34.1 s 42.2 s, 23.7 s
  • Reports: the same diagnostics and functions with both builds, at both job counts. The order of Lua's diagnostics, and so their ids, varies from run to run with either build.
  • Peak memory grows with two compilations in flight: at --jobs=2, 239 MB to 328 MB for the 8 C++ files, 1.26 GB to 1.42 GB for the 53 sources.
  • Lua: the analysis takes 6.5 s of the 7.7 s; loading is a small part of the run.
  • More jobs were not measured, to keep this machine usable.

CI

The runs of 8a99cf3, which adds the -MJ and statistics fixes, are under way. At b05bd18:

  • LLVM versions run 43162050148: 15/15 jobs pass. Every test passes in each job: 275 on Windows, 272 on Linux and macOS.
  • Build run 43162050223: passes.
    • Every test passes: 178 without the runtime, 272 with coverage, 273 with AddressSanitizer and UBSan.
    • compilerlib with ThreadSanitizer passes: 13 tests ran, the concurrent compilations included, without a report.
    • The Docker job is skipped, as on every pull request.
  • The new tests: the 15 pass in the 14 jobs of Linux, macOS and the Build workflow. On Windows, 14 pass and RelativeDirectoryIsRelativeToTheProcess skips itself: the runner's working directory and temporary directory are on two drives, so no relative path joins them.
  • Other checks: clang-format, license-compliance and the commit conventions pass, and so do the runs of the push to this branch.
  • At 7aa9024, the run of the push failed once in macOS / LLVM 23, while cloning coretrace-log (could not read Username for 'https://github.com'); run again, it passed.

The linker runs as a program of its own, which starts in the process's
working directory. A link with -working-directory writes the program in
that directory, and leaves the process there, as clang does. The test
pins both, which giving each compilation a working directory of its
own must keep.

Refs #261
The assembler runs as a program of its own too. An empty source, which
every target assembles, gives its object in the directory of
-working-directory.

Refs #261
The directory of -working-directory resolves the source and the include
directory, and receives the output and the dependency file, of a plain
and of an instrumented compilation.

Refs #261
normalizeWorkingDirectoryArgs parses a command line as the driver does,
rewrites each -working-directory, in either spelling, into the
two-argument form with an absolute directory, and returns the last
one. Nothing calls it yet.

Refs #261
The driver moved the process to a relative directory, then gave the
option unchanged to the -cc1 job, which resolved it again from there:
the source was not found. The directory is made absolute before the
driver reads it, so that it names one directory to both.

Refs #261
compilerlib reads the objects of a link without sources, to choose
between the C and C++ drivers, before the driver moves to the directory
of -working-directory: a relative object was looked up in the process's
directory and not found. It is now looked up in that directory.

Refs #261
A -cc1 job reads a sanitizer's ignore list through its file system,
not through its file manager, and the driver reads it first: both must
find it in the directory of -working-directory.

Refs #261
resolveCc1OutputPaths makes absolute, against a directory, the relative
paths of the files a -cc1 job writes: its output, dependency file,
header list, optimization record, split DWARF, stack usage, serialized
diagnostics and time trace. Nothing calls it yet.

Refs #261
compile() read and wrote files through the file system of the process,
so -working-directory moved the whole process there, for good: a host
compiling from several threads had to change the working directory
itself, under a lock that serialized every compilation.

Each compilation now has a file system of its own, whose working
directory -working-directory moves while the process's stays. The -cc1
jobs read through it, LLVM 21 and earlier included, and the files they
write are made absolute against it, since Clang writes them relative to
the process's directory. A job that runs as a program of its own, as
the linker and the assembler do, starts in the process's directory: for
such a compilation, the process still moves to the directory.

Compilations from eight threads, each in a directory of its own with a
header of the same name, read their own header, instrumented or not.

Refs #261
A job builds compilerlib and its unit tests with ThreadSanitizer, and
runs the tests of -working-directory, those of concurrent compilations
included. LLVM and Clang come from packages built without it: a race
inside their code goes unseen.

Refs #261
@SizzleUnrlsd SizzleUnrlsd added the enhancement New feature or request label Nov 12, 2026
@SizzleUnrlsd SizzleUnrlsd self-assigned this Nov 12, 2026
A -cc1 job wrote the statistics file of -save-stats, the dependency
graph of -dependency-dot and its module cache relative to the process's
working directory, which -working-directory no longer moves: the
statistics of -save-stats=cwd landed in the process's directory. Their
paths are now made absolute against the compilation's directory, as
are those of the other files the job writes.

Refs #261
resolveDriverOutputPaths rewrites -MJ <file> and
-gen-cdb-fragment-path <directory>, which the driver itself writes, into
the two-argument form, with a relative path made absolute against a
directory. Nothing calls it yet.

Refs #261
The driver writes the compilation database fragment of -MJ relative to
the process's working directory, which -working-directory no longer
moves: the fragment landed in the process's directory. Its path, and
that of -gen-cdb-fragment-path, are now made absolute against the
compilation's directory before the driver reads them.

Refs #261
SizzleUnrlsd added a commit to CoreTrace/coretrace-stack-analyzer that referenced this pull request Nov 12, 2026
The newer commit of CoreTrace/coretrace-compiler#262 also writes the
fragment of -MJ, the statistics of -save-stats, the graph of
-dependency-dot and the module cache in the directory of
-working-directory, as the analyzer did when it moved the process
there.

Refs #271
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

-working-directory moves the whole process, so a host cannot compile in parallel

1 participant