diff --git a/Help/manual/cmake.1.rst b/Help/manual/cmake.1.rst index 996603aeb0..61e2bc2d24 100644 --- a/Help/manual/cmake.1.rst +++ b/Help/manual/cmake.1.rst @@ -869,6 +869,16 @@ following options: Some native build tools always build in parallel. The use of ```` value of ``1`` can be used to limit to a single job. + .. versionadded:: 4.5 + When CMake is the one passing the parallel flag to the build tool (through + this option or the :envvar:`CMAKE_BUILD_PARALLEL_LEVEL` environment + variable) and that tool is GNU Make 4.0 or newer, the output of each + recipe is grouped so that concurrent jobs do not interleave. Request + parallelism natively, e.g. ``cmake --build . -- -j``, to opt out and + stream output as it is produced. Since + :envvar:`CMAKE_BUILD_PARALLEL_LEVEL` likewise enables grouping, unset it + to opt out when it is set. + .. option:: -t ..., --target ... Build ```` instead of the default target. Multiple targets may be diff --git a/Help/release/dev/cmake-build-output-sync.rst b/Help/release/dev/cmake-build-output-sync.rst new file mode 100644 index 0000000000..39accaa001 --- /dev/null +++ b/Help/release/dev/cmake-build-output-sync.rst @@ -0,0 +1,9 @@ +cmake-build-output-sync +----------------------- + +* The :option:`cmake --build` command now groups the output of each recipe in + a parallel build so that concurrent jobs do not interleave, when CMake is + the one passing the parallel flag to the build tool (via :option:`--parallel + ` or :envvar:`CMAKE_BUILD_PARALLEL_LEVEL`) and that + tool is GNU Make 4.0 or newer. Request parallelism natively, e.g. + ``cmake --build . -- -j``, to opt out and stream output as it is produced. diff --git a/Source/cmGlobalUnixMakefileGenerator3.cxx b/Source/cmGlobalUnixMakefileGenerator3.cxx index d649d5dfc2..703b6501f2 100644 --- a/Source/cmGlobalUnixMakefileGenerator3.cxx +++ b/Source/cmGlobalUnixMakefileGenerator3.cxx @@ -11,6 +11,9 @@ #include #include +#include "cmsys/RegularExpression.hxx" + +#include "cmDuration.h" #include "cmGeneratedFileStream.h" #include "cmGeneratorTarget.h" #include "cmGlobalGenerator.h" @@ -589,6 +592,21 @@ cmGlobalUnixMakefileGenerator3::GenerateBuildCommand( } else { makeCommand.Add(cmStrCat("-j", jobs)); } + + // Group each recipe's output so parallel jobs do not interleave. Only + // done when CMake is the one passing the parallel flag to make; a native + // `-- -j` leaves jobs unset and thus opts out. Grouping requires GNU + // Make 4.0+ (its --output-sync); IsGNUMakeJobServerAware() excludes + // non-GNU makes such as JOM before the version probe runs. Emitted + // before the user's make options so a native `-- -O` overrides it. + if (this->IsGNUMakeJobServerAware() && + this->MakeSupportsOutputSync(makeProgram)) { + makeCommand.Add("-Otarget"); + // Set the (empty by default) USES_TERMINAL recipe prefix to "+" so + // GNU Make leaves those recipes unbuffered, keeping interactive + // commands able to reach the terminal under -Otarget. + makeCommand.Add("CMAKE_USES_TERMINAL_PREFIX=+"); + } } makeCommand.Add(makeOptions.begin(), makeOptions.end()); @@ -604,6 +622,35 @@ cmGlobalUnixMakefileGenerator3::GenerateBuildCommand( return { std::move(makeCommand) }; } +bool cmGlobalUnixMakefileGenerator3::MakeSupportsOutputSync( + std::string const& makeProgram) +{ + if (this->OutputSyncSupportState == OutputSyncSupport::Unknown) { + // Any probe failure (spawn error, timeout, non-GNU or old make) leaves + // grouping off; it is a convenience and must never fail the build. + this->OutputSyncSupportState = OutputSyncSupport::No; + + std::string version; + std::string error; + int retVal = 1; + std::vector command{ this->SelectMakeProgram(makeProgram), + "--version" }; + if (cmSystemTools::RunSingleCommand(command, &version, &error, &retVal, + nullptr, cmSystemTools::OUTPUT_NONE, + cmDuration(30)) && + retVal == 0) { + // GNU Make's version line is not localized, so no LC_ALL=C is needed. + cmsys::RegularExpression versionRegex("GNU Make ([0-9]+(\\.[0-9]+)*)"); + if (versionRegex.find(version) && + !cmSystemTools::VersionCompare(cmSystemTools::OP_LESS, + versionRegex.match(1), "4.0")) { + this->OutputSyncSupportState = OutputSyncSupport::Yes; + } + } + } + return this->OutputSyncSupportState == OutputSyncSupport::Yes; +} + void cmGlobalUnixMakefileGenerator3::WriteConvenienceRules( std::ostream& ruleFileStream, std::set& emitted) { diff --git a/Source/cmGlobalUnixMakefileGenerator3.h b/Source/cmGlobalUnixMakefileGenerator3.h index df48c384cf..76ea0b9678 100644 --- a/Source/cmGlobalUnixMakefileGenerator3.h +++ b/Source/cmGlobalUnixMakefileGenerator3.h @@ -309,6 +309,18 @@ protected: private: char const* GetBuildIgnoreErrorsFlag() const override { return "-i"; } + // Probe the make tool (once, memoized) for whether it is GNU Make 4.0 or + // newer and thus supports grouped output via --output-sync. + bool MakeSupportsOutputSync(std::string const& makeProgram); + + enum class OutputSyncSupport + { + Unknown, + Yes, + No + }; + OutputSyncSupport OutputSyncSupportState = OutputSyncSupport::Unknown; + std::map, cmStateSnapshot::StrictWeakOrder> DirectoryTargetsMap; diff --git a/Source/cmLocalUnixMakefileGenerator3.cxx b/Source/cmLocalUnixMakefileGenerator3.cxx index c140de5281..eac59f7f92 100644 --- a/Source/cmLocalUnixMakefileGenerator3.cxx +++ b/Source/cmLocalUnixMakefileGenerator3.cxx @@ -674,6 +674,13 @@ void cmLocalUnixMakefileGenerator3::WriteMakeVariables( "NULL=nul\n" "!ENDIF\n"; } + if (gg->IsGNUMakeJobServerAware()) { + // Toggle for USES_TERMINAL recipes: empty here, set to "+" by + // "cmake --build" under --output-sync (see AppendCustomCommand). + makefileStream << "# Prefix for USES_TERMINAL recipes under output sync.\n" + "CMAKE_USES_TERMINAL_PREFIX =\n" + "\n"; + } if (this->IsWindowsShell()) { makefileStream << "SHELL = cmd.exe\n" "\n"; @@ -1135,6 +1142,16 @@ void cmLocalUnixMakefileGenerator3::AppendCustomCommand( if (ccg.GetCC().GetJobserverAware() && gg->IsGNUMakeJobServerAware()) { std::transform(commands1.begin(), commands1.end(), commands1.begin(), [](std::string const& cmd) { return cmStrCat('+', cmd); }); + } else if (ccg.GetCC().GetUsesTerminal() && gg->IsGNUMakeJobServerAware()) { + // Prefix USES_TERMINAL recipes with $(CMAKE_USES_TERMINAL_PREFIX): empty + // by default, but set to "+" by "cmake --build" under --output-sync so + // GNU Make leaves the recipe unbuffered and an interactive command keeps + // the terminal. Jobserver-aware commands already carry "+" from the + // branch above and are excluded here to avoid a doubled prefix. + std::transform(commands1.begin(), commands1.end(), commands1.begin(), + [](std::string const& cmd) { + return cmStrCat("$(CMAKE_USES_TERMINAL_PREFIX)", cmd); + }); } // push back the custom commands diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt index 42f60f3f6c..7ca2d43592 100644 --- a/Tests/RunCMake/CMakeLists.txt +++ b/Tests/RunCMake/CMakeLists.txt @@ -201,7 +201,8 @@ if(NOT CMAKE_GENERATOR MATCHES "Visual Studio|Xcode") endif() add_executable(detect_jobserver detect_jobserver.c) if(CMAKE_GENERATOR MATCHES "Make") - add_RunCMake_test(Make -DMAKE_IS_GNU=${MAKE_IS_GNU} -DDETECT_JOBSERVER=$) + add_executable(fake_make Make/fake_make.c) + add_RunCMake_test(Make -DMAKE_IS_GNU=${MAKE_IS_GNU} -DDETECT_JOBSERVER=$ -DFAKE_MAKE=$) endif() unset(ninja_test_with_qt_version) unset(ninja_qt_args) diff --git a/Tests/RunCMake/Make/OutputSync-check.cmake b/Tests/RunCMake/Make/OutputSync-check.cmake new file mode 100644 index 0000000000..003a1d050f --- /dev/null +++ b/Tests/RunCMake/Make/OutputSync-check.cmake @@ -0,0 +1,77 @@ +# Shared check for the OutputSync argv/probe cases. The calling test sets +# expectation variables (all optional) before each build command: +# expect_otarget 0/1 -- "-Otarget" must be absent/present +# expect_otarget_count N -- exact number of "-Otarget" occurrences +# expect_uses_terminal_flag 0/1 -- "CMAKE_USES_TERMINAL_PREFIX=+" absent/present +# expect_probe 0/1 -- the make tool must not/must be probed +# expect_probe_count N -- exact number of "--version" probes +# expect_order_onone 0/1 -- "-Otarget" must appear before "-Onone" + +set(record "${RunCMake_TEST_BINARY_DIR}/fake_make_record.txt") +set(marker "${RunCMake_TEST_BINARY_DIR}/fake_make_probe.txt") + +if(EXISTS "${record}") + file(READ "${record}" record_content) +else() + set(record_content "") +endif() + +if(EXISTS "${marker}") + file(READ "${marker}" marker_content) +else() + set(marker_content "") +endif() + +string(REGEX MATCHALL "-Otarget" _otarget_matches "${record_content}") +list(LENGTH _otarget_matches _otarget_count) + +string(REGEX MATCHALL "probe" _probe_matches "${marker_content}") +list(LENGTH _probe_matches _probe_count) + +if(DEFINED expect_otarget) + if(expect_otarget AND _otarget_count EQUAL 0) + string(APPEND RunCMake_TEST_FAILED + "Expected '-Otarget' in the build command but recorded:\n${record_content}\n") + elseif(NOT expect_otarget AND _otarget_count GREATER 0) + string(APPEND RunCMake_TEST_FAILED + "Did not expect '-Otarget' in the build command but recorded:\n${record_content}\n") + endif() +endif() + +if(DEFINED expect_otarget_count AND NOT _otarget_count EQUAL expect_otarget_count) + string(APPEND RunCMake_TEST_FAILED + "Expected ${expect_otarget_count} '-Otarget' occurrence(s) but found ${_otarget_count}:\n${record_content}\n") +endif() + +if(DEFINED expect_uses_terminal_flag) + string(FIND "${record_content}" "CMAKE_USES_TERMINAL_PREFIX=+" _ut_pos) + if(expect_uses_terminal_flag AND _ut_pos EQUAL -1) + string(APPEND RunCMake_TEST_FAILED + "Expected 'CMAKE_USES_TERMINAL_PREFIX=+' in the build command but recorded:\n${record_content}\n") + elseif(NOT expect_uses_terminal_flag AND NOT _ut_pos EQUAL -1) + string(APPEND RunCMake_TEST_FAILED + "Did not expect 'CMAKE_USES_TERMINAL_PREFIX=+' in the build command but recorded:\n${record_content}\n") + endif() +endif() + +if(DEFINED expect_probe) + if(expect_probe AND _probe_count EQUAL 0) + string(APPEND RunCMake_TEST_FAILED + "Expected the make tool to be probed with '--version' but it was not.\n") + elseif(NOT expect_probe AND _probe_count GREATER 0) + string(APPEND RunCMake_TEST_FAILED + "Did not expect a '--version' probe but it ran ${_probe_count} time(s).\n") + endif() +endif() + +if(DEFINED expect_probe_count AND NOT _probe_count EQUAL expect_probe_count) + string(APPEND RunCMake_TEST_FAILED + "Expected ${expect_probe_count} probe(s) but found ${_probe_count}.\n") +endif() + +if(DEFINED expect_order_onone AND expect_order_onone) + if(NOT record_content MATCHES "-Otarget[^\n]*-Onone") + string(APPEND RunCMake_TEST_FAILED + "Expected '-Otarget' to appear before '-Onone' but recorded:\n${record_content}\n") + endif() +endif() diff --git a/Tests/RunCMake/Make/OutputSync.cmake b/Tests/RunCMake/Make/OutputSync.cmake new file mode 100644 index 0000000000..03df50f108 --- /dev/null +++ b/Tests/RunCMake/Make/OutputSync.cmake @@ -0,0 +1 @@ +add_custom_target(drive ALL COMMAND $(CMAKE_COMMAND) -E true) diff --git a/Tests/RunCMake/Make/OutputSyncUsesTerminal-check.cmake b/Tests/RunCMake/Make/OutputSyncUsesTerminal-check.cmake new file mode 100644 index 0000000000..1d6c9fa697 --- /dev/null +++ b/Tests/RunCMake/Make/OutputSyncUsesTerminal-check.cmake @@ -0,0 +1,37 @@ +set(BUILD_DIR "${RunCMake_BINARY_DIR}/OutputSyncUsesTerminal-build") + +function(check_has target regex) + file(STRINGS "${BUILD_DIR}/${target}" lines REGEX "${regex}") + list(LENGTH lines len) + if(len EQUAL 0) + set(RunCMake_TEST_FAILED + "${RunCMake_TEST_FAILED}Expected to find '${regex}' in ${target}\n" + PARENT_SCOPE) + endif() +endfunction() + +function(check_missing target regex) + file(STRINGS "${BUILD_DIR}/${target}" lines REGEX "${regex}") + list(LENGTH lines len) + if(NOT len EQUAL 0) + set(RunCMake_TEST_FAILED + "${RunCMake_TEST_FAILED}Did not expect to find '${regex}' in ${target}: ${lines}\n" + PARENT_SCOPE) + endif() +endfunction() + +# The USES_TERMINAL recipe is prefixed with the build-time toggle variable. +check_has("CMakeFiles/term.dir/build.make" + [[\$\(CMAKE_USES_TERMINAL_PREFIX\)\$\(CMAKE_COMMAND\) -E true]]) + +# A plain recipe references no prefix. +check_missing("CMakeFiles/plain.dir/build.make" [[\$\(CMAKE_USES_TERMINAL_PREFIX\)]]) + +# A jobserver-aware recipe keeps the literal '+' and is not given the toggle. +check_has("CMakeFiles/jsa.dir/build.make" [[\+\$\(CMAKE_COMMAND\) -E true]]) +check_missing("CMakeFiles/jsa.dir/build.make" [[\$\(CMAKE_USES_TERMINAL_PREFIX\)]]) + +# A recipe that is both jobserver-aware and USES_TERMINAL uses only '+' +# (no doubled prefix). +check_has("CMakeFiles/both.dir/build.make" [[\+\$\(CMAKE_COMMAND\) -E true]]) +check_missing("CMakeFiles/both.dir/build.make" [[\$\(CMAKE_USES_TERMINAL_PREFIX\)]]) diff --git a/Tests/RunCMake/Make/OutputSyncUsesTerminal.cmake b/Tests/RunCMake/Make/OutputSyncUsesTerminal.cmake new file mode 100644 index 0000000000..0c80c9d794 --- /dev/null +++ b/Tests/RunCMake/Make/OutputSyncUsesTerminal.cmake @@ -0,0 +1,20 @@ +# Recipe that wants interactive terminal access: prefixed with the toggle. +add_custom_target(term ALL + COMMAND $(CMAKE_COMMAND) -E true + USES_TERMINAL) + +# Ordinary recipe: no prefix at all. +add_custom_target(plain ALL + COMMAND $(CMAKE_COMMAND) -E true) + +# Jobserver-aware recipe: keeps the literal '+' prefix. +add_custom_target(jsa ALL + COMMAND $(CMAKE_COMMAND) -E true + JOB_SERVER_AWARE ON) + +# Both jobserver-aware and interactive: the '+' branch wins, so the toggle +# prefix must not be added (no doubled prefix). +add_custom_target(both ALL + COMMAND $(CMAKE_COMMAND) -E true + USES_TERMINAL + JOB_SERVER_AWARE ON) diff --git a/Tests/RunCMake/Make/RunCMakeTest.cmake b/Tests/RunCMake/Make/RunCMakeTest.cmake index 28301994b0..032a0c9653 100644 --- a/Tests/RunCMake/Make/RunCMakeTest.cmake +++ b/Tests/RunCMake/Make/RunCMakeTest.cmake @@ -135,3 +135,130 @@ if(MAKE_IS_GNU) # commands with the '+' operator. run_cmake(GNUMakeJobServerAware) endif() + +# Output synchronization (-Otarget) is emitted only for the GNU Make family of +# generators. Drive these cases with a fake "make" so the behavior can be +# asserted independently of the host's real make tool. +if(FAKE_MAKE AND RunCMake_GENERATOR MATCHES "Unix Makefiles|MinGW Makefiles|MSYS Makefiles") + function(run_OutputSync) + # Use the fake make for the configure step so the build steps inherit it + # from the cache. + set(RunCMake_MAKE_PROGRAM "${FAKE_MAKE}") + set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/OutputSync-build) + set(RunCMake_TEST_NO_CLEAN 1) + file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}") + file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}") + run_cmake(OutputSync) + + set(RunCMake-check-file OutputSync-check.cmake) + set(record "${RunCMake_TEST_BINARY_DIR}/fake_make_record.txt") + set(marker "${RunCMake_TEST_BINARY_DIR}/fake_make_probe.txt") + set(ENV{FAKE_MAKE_RECORD} "${record}") + set(ENV{FAKE_MAKE_PROBE_MARKER} "${marker}") + + # CMake-driven parallel build with GNU Make >= 4.0: group the output. + file(REMOVE "${record}" "${marker}") + set(ENV{FAKE_MAKE_VERSION} "GNU Make 4.4.1") + set(expect_otarget 1) + set(expect_uses_terminal_flag 1) + set(expect_probe 1) + run_cmake_command(OutputSync-parallel ${CMAKE_COMMAND} --build . --parallel 2) + + # Serial build: no grouping and no probe. + file(REMOVE "${record}" "${marker}") + set(expect_otarget 0) + set(expect_uses_terminal_flag 0) + set(expect_probe 0) + run_cmake_command(OutputSync-serial ${CMAKE_COMMAND} --build .) + + # Native "-- -j" opt-out: CMake adds neither -j nor -Otarget, no probe. + file(REMOVE "${record}" "${marker}") + set(expect_otarget 0) + set(expect_uses_terminal_flag 0) + set(expect_probe 0) + run_cmake_command(OutputSync-native-j ${CMAKE_COMMAND} --build . -- -j) + + # A user's native "-O" overrides CMake's: -Otarget appears before -Onone. + file(REMOVE "${record}" "${marker}") + set(expect_otarget 1) + set(expect_uses_terminal_flag 1) + set(expect_probe 1) + set(expect_order_onone 1) + run_cmake_command(OutputSync-override ${CMAKE_COMMAND} --build . --parallel 2 -- -Onone) + unset(expect_order_onone) + + # GNU Make < 4.0 does not support --output-sync. + file(REMOVE "${record}" "${marker}") + set(ENV{FAKE_MAKE_VERSION} "GNU Make 3.81") + set(expect_otarget 0) + set(expect_uses_terminal_flag 0) + set(expect_probe 1) + run_cmake_command(OutputSync-old ${CMAKE_COMMAND} --build . --parallel 2) + + # 4.0 is the first release to support --output-sync; verify the lower + # boundary and the rest of the 4.0-4.2 series are classified as supported. + foreach(v IN ITEMS 4.0 4.1 4.2) + file(REMOVE "${record}" "${marker}") + set(ENV{FAKE_MAKE_VERSION} "GNU Make ${v}") + set(expect_otarget 1) + set(expect_uses_terminal_flag 1) + set(expect_probe 1) + run_cmake_command(OutputSync-v${v} ${CMAKE_COMMAND} --build . --parallel 2) + endforeach() + + # A non-GNU make is not grouped. + file(REMOVE "${record}" "${marker}") + set(ENV{FAKE_MAKE_VERSION} "bmake version 20200710") + set(expect_otarget 0) + set(expect_uses_terminal_flag 0) + set(expect_probe 1) + run_cmake_command(OutputSync-nongnu ${CMAKE_COMMAND} --build . --parallel 2) + + # Unparsable "--version" output: probe fails gracefully, build still runs. + file(REMOVE "${record}" "${marker}") + set(ENV{FAKE_MAKE_VERSION} "garbage output") + set(expect_otarget 0) + set(expect_uses_terminal_flag 0) + set(expect_probe 1) + run_cmake_command(OutputSync-garbage ${CMAKE_COMMAND} --build . --parallel 2) + + # Probe exits non-zero: treated as unsupported, build still runs. + file(REMOVE "${record}" "${marker}") + set(ENV{FAKE_MAKE_VERSION} "GNU Make 4.4.1") + set(ENV{FAKE_MAKE_VERSION_RESULT} "2") + set(expect_otarget 0) + set(expect_uses_terminal_flag 0) + set(expect_probe 1) + run_cmake_command(OutputSync-probe-fail ${CMAKE_COMMAND} --build . --parallel 2) + unset(ENV{FAKE_MAKE_VERSION_RESULT}) + + # --clean-first runs two build commands but probes only once (memoized). + file(REMOVE "${record}" "${marker}") + set(ENV{FAKE_MAKE_VERSION} "GNU Make 4.4.1") + set(expect_otarget 1) + set(expect_otarget_count 2) + set(expect_uses_terminal_flag 1) + set(expect_probe 1) + set(expect_probe_count 1) + run_cmake_command(OutputSync-clean-first ${CMAKE_COMMAND} --build . --parallel 2 --clean-first) + unset(expect_otarget_count) + unset(expect_probe_count) + + unset(ENV{FAKE_MAKE_VERSION}) + unset(ENV{FAKE_MAKE_RECORD}) + unset(ENV{FAKE_MAKE_PROBE_MARKER}) + endfunction() + run_OutputSync() + + # USES_TERMINAL recipe prefixing is a generation-time change; inspect the + # generated build.make directly. + function(run_OutputSyncUsesTerminal) + set(RunCMake_MAKE_PROGRAM "${FAKE_MAKE}") + set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/OutputSyncUsesTerminal-build) + set(RunCMake_TEST_NO_CLEAN 1) + file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}") + file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}") + run_cmake(OutputSyncUsesTerminal) + endfunction() + run_OutputSyncUsesTerminal() +endif() diff --git a/Tests/RunCMake/Make/fake_make.c b/Tests/RunCMake/Make/fake_make.c new file mode 100644 index 0000000000..575756ce68 --- /dev/null +++ b/Tests/RunCMake/Make/fake_make.c @@ -0,0 +1,67 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ + +/* Stand-in for "make" used by the RunCMake.Make output-sync tests so they can + assert what "cmake --build" passes to make without a real make tool. + + On "--version" (the output-sync probe): touch $FAKE_MAKE_PROBE_MARKER, print + $FAKE_MAKE_VERSION (default a GNU Make 4.x banner), and exit with + $FAKE_MAKE_VERSION_RESULT (default 0). + Otherwise (the build): append the argument list to $FAKE_MAKE_RECORD and + exit 0. */ + +#ifndef _CRT_SECURE_NO_WARNINGS +# define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#include +#include + +int main(int argc, char** argv) +{ + int i; + int isVersion = 0; + + for (i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--version") == 0) { + isVersion = 1; + break; + } + } + + if (isVersion) { + char const* marker = getenv("FAKE_MAKE_PROBE_MARKER"); + char const* banner = getenv("FAKE_MAKE_VERSION"); + char const* result = getenv("FAKE_MAKE_VERSION_RESULT"); + if (marker) { + FILE* f = fopen(marker, "a"); + if (f) { + fprintf(f, "probe\n"); + fclose(f); + } + } + if (!banner) { + banner = "GNU Make 4.4.1"; + } + if (banner[0] != '\0') { + printf("%s\n", banner); + } + return result ? atoi(result) : 0; + } + + { + char const* record = getenv("FAKE_MAKE_RECORD"); + if (record) { + FILE* f = fopen(record, "a"); + if (f) { + for (i = 1; i < argc; ++i) { + fprintf(f, "%s%s", i > 1 ? " " : "", argv[i]); + } + fprintf(f, "\n"); + fclose(f); + } + } + } + return 0; +}