//===- llvm/Support/Jobserver.cpp - Jobserver Client Implementation -------===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "llvm/Support/Jobserver.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/Statistic.h" #include "llvm/ADT/StringExtras.h" #include "llvm/Config/llvm-config.h" #include "llvm/Support/Debug.h" #include "llvm/Support/Error.h" #include "llvm/Support/raw_ostream.h" #include #include #include #include #define DEBUG_TYPE "jobserver" using namespace llvm; namespace { struct FdPair { int Read = -1; int Write = -1; bool isValid() const { return Read >= 0 && Write >= 0; } }; struct JobserverConfig { enum Mode { None, PosixFifo, PosixPipe, Win32Semaphore, }; Mode TheMode = None; std::string Path; FdPair PipeFDs; }; /// A helper function that checks if `Input` starts with `Prefix`. /// If it does, it removes the prefix from `Input`, assigns the remainder to /// `Value`, and returns true. Otherwise, it returns false. bool getPrefixedValue(StringRef Input, StringRef Prefix, StringRef &Value) { if (Input.consume_front(Prefix)) { Value = Input; return true; } return false; } /// A helper function to parse a string in the format "R,W" where R and W are /// non-negative integers representing file descriptors. It populates the /// `ReadFD` and `WriteFD` output parameters. Returns true on success. static std::optional getFileDescriptorPair(StringRef Input) { FdPair FDs; if (Input.consumeInteger(10, FDs.Read)) return std::nullopt; if (!Input.consume_front(",")) return std::nullopt; if (Input.consumeInteger(10, FDs.Write)) return std::nullopt; if (!Input.empty() || !FDs.isValid()) return std::nullopt; return FDs; } /// Parses the `MAKEFLAGS` environment variable string to find jobserver /// arguments. It splits the string into space-separated arguments and searches /// for `--jobserver-auth` or `--jobserver-fds`. Based on the value of these /// arguments, it determines the jobserver mode (Pipe, FIFO, or Semaphore) and /// connection details (file descriptors or path). Expected parseNativeMakeFlags(StringRef MakeFlags) { JobserverConfig Config; if (MakeFlags.empty()) return Config; // Split the MAKEFLAGS string into arguments. SmallVector Args; SplitString(MakeFlags, Args); // If '-n' (dry-run) is present as a legacy flag (not starting with '-'), // disable the jobserver. if (!Args.empty() && !Args[0].starts_with("-") && Args[0].contains('n')) return Config; // Iterate through arguments to find jobserver flags. // Note that make may pass multiple --jobserver-auth flags; the last one wins. for (StringRef Arg : Args) { StringRef Value; if (getPrefixedValue(Arg, "--jobserver-auth=", Value)) { // Try to parse as a file descriptor pair first. if (auto FDPair = getFileDescriptorPair(Value)) { Config.TheMode = JobserverConfig::PosixPipe; Config.PipeFDs = *FDPair; } else { StringRef FifoPath; // If not FDs, try to parse as a named pipe (fifo). if (getPrefixedValue(Value, "fifo:", FifoPath)) { Config.TheMode = JobserverConfig::PosixFifo; Config.Path = FifoPath.str(); } else { // Otherwise, assume it's a Windows semaphore. Config.TheMode = JobserverConfig::Win32Semaphore; Config.Path = Value.str(); } } } else if (getPrefixedValue(Arg, "--jobserver-fds=", Value)) { // This is an alternative, older syntax for the pipe-based server. if (auto FDPair = getFileDescriptorPair(Value)) { Config.TheMode = JobserverConfig::PosixPipe; Config.PipeFDs = *FDPair; } else { return createStringError(inconvertibleErrorCode(), "Invalid file descriptor pair in MAKEFLAGS"); } } } // Perform platform-specific validation. #ifdef _WIN32 if (Config.TheMode == JobserverConfig::PosixFifo || Config.TheMode == JobserverConfig::PosixPipe) return createStringError( inconvertibleErrorCode(), "FIFO/Pipe-based jobserver is not supported on Windows"); #else if (Config.TheMode == JobserverConfig::Win32Semaphore) return createStringError( inconvertibleErrorCode(), "Semaphore-based jobserver is not supported on this platform"); #endif return Config; } std::once_flag GJobserverOnceFlag; JobserverClient *GJobserver = nullptr; } // namespace namespace llvm { class JobserverClientImpl : public JobserverClient { bool IsInitialized = false; std::atomic HasImplicitSlot{true}; unsigned NumJobs = 0; public: JobserverClientImpl(const JobserverConfig &Config); ~JobserverClientImpl() override; JobSlot tryAcquire() override; void release(JobSlot Slot) override; unsigned getNumJobs() const override { return NumJobs; } bool isValid() const { return IsInitialized; } private: #if defined(LLVM_ON_UNIX) int ReadFD = -1; int WriteFD = -1; std::string FifoPath; #elif defined(_WIN32) void *Semaphore = nullptr; #endif }; } // namespace llvm // Include the platform-specific parts of the class. #if defined(LLVM_ON_UNIX) #include "Unix/Jobserver.inc" #elif defined(_WIN32) #include "Windows/Jobserver.inc" #else // Dummy implementation for unsupported platforms. JobserverClientImpl::JobserverClientImpl(const JobserverConfig &Config) {} JobserverClientImpl::~JobserverClientImpl() = default; JobSlot JobserverClientImpl::tryAcquire() { return JobSlot(); } void JobserverClientImpl::release(JobSlot Slot) {} #endif namespace llvm { JobserverClient::~JobserverClient() = default; uint8_t JobSlot::getExplicitValue() const { assert(isExplicit() && "Cannot get value of implicit or invalid slot"); return static_cast(Value); } /// This is the main entry point for acquiring a jobserver client. It uses a /// std::call_once to ensure the singleton `GJobserver` instance is created /// safely in a multi-threaded environment. On first call, it reads the /// `MAKEFLAGS` environment variable, parses it, and attempts to construct and /// initialize a `JobserverClientImpl`. If successful, the global instance is /// stored in `GJobserver`. Subsequent calls will return the existing instance. JobserverClient *JobserverClient::getInstance() { std::call_once(GJobserverOnceFlag, []() { LLVM_DEBUG( dbgs() << "JobserverClient::getInstance() called for the first time.\n"); const char *MakeFlagsEnv = getenv("MAKEFLAGS"); if (!MakeFlagsEnv) { errs() << "Warning: failed to create jobserver client due to MAKEFLAGS " "environment variable not found\n"; return; } LLVM_DEBUG(dbgs() << "Found MAKEFLAGS = \"" << MakeFlagsEnv << "\"\n"); auto ConfigOrErr = parseNativeMakeFlags(MakeFlagsEnv); if (Error Err = ConfigOrErr.takeError()) { errs() << "Warning: failed to create jobserver client due to invalid " "MAKEFLAGS environment variable: " << toString(std::move(Err)) << "\n"; return; } JobserverConfig Config = *ConfigOrErr; if (Config.TheMode == JobserverConfig::None) { errs() << "Warning: failed to create jobserver client due to jobserver " "mode missing in MAKEFLAGS environment variable\n"; return; } if (Config.TheMode == JobserverConfig::PosixPipe) { #if defined(LLVM_ON_UNIX) if (!areFdsValid(Config.PipeFDs.Read, Config.PipeFDs.Write)) { errs() << "Warning: failed to create jobserver client due to invalid " "Pipe FDs in MAKEFLAGS environment variable\n"; return; } #endif } auto Client = std::make_unique(Config); if (Client->isValid()) { LLVM_DEBUG(dbgs() << "Jobserver client created successfully!\n"); GJobserver = Client.release(); } else errs() << "Warning: jobserver client initialization failed.\n"; }); return GJobserver; } /// For testing purposes only. This function resets the singleton instance by /// destroying the existing client and re-initializing the `std::once_flag`. /// This allows tests to simulate the first-time initialization of the /// jobserver client multiple times. void JobserverClient::resetForTesting() { delete GJobserver; GJobserver = nullptr; // Re-construct the std::once_flag in place to reset the singleton state. new (&GJobserverOnceFlag) std::once_flag(); } } // namespace llvm