diff options
author | Ryan Egesdahl <ryan.egesdahl@mongodb.com> | 2020-10-12 08:43:16 -0700 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2020-10-13 06:06:23 +0000 |
commit | 031ce11e88e9a98136eb99943162809554981dbb (patch) | |
tree | 257629e60c7fb70004b6e7e7708366c3c049d63c /site_scons | |
parent | 1357b2601db6c6358410933a3ad42200732187a2 (diff) | |
download | mongo-031ce11e88e9a98136eb99943162809554981dbb.tar.gz |
Revert "SERVER-50363 Merge --build-tools=next into stable"
This reverts commit 7b93770e0835087c023ae3e3f36eeb46d204767f.
Diffstat (limited to 'site_scons')
-rw-r--r-- | site_scons/site_tools/ccache.py | 84 | ||||
-rw-r--r-- | site_scons/site_tools/icecream.py | 576 | ||||
-rw-r--r-- | site_scons/site_tools/ninja.py | 229 |
3 files changed, 248 insertions, 641 deletions
diff --git a/site_scons/site_tools/ccache.py b/site_scons/site_tools/ccache.py index a4abcd79e14..76fef32d9b7 100644 --- a/site_scons/site_tools/ccache.py +++ b/site_scons/site_tools/ccache.py @@ -22,20 +22,23 @@ from pkg_resources import parse_version # This is the oldest version of ccache that offers support for -gsplit-dwarf _ccache_version_min = parse_version("3.2.3") +_ccache_version_found = None def exists(env): """Look for a viable ccache implementation that meets our version requirements.""" - if not env.subst("$CCACHE"): - return False - ccache = env.WhereIs("$CCACHE") + # If we already generated, we definitely exist + if "CCACHE_VERSION" in env: + return True + + ccache = env.get("CCACHE", False) if not ccache: - print(f"Error: ccache not found at {env['CCACHE']}") return False - if 'CCACHE_VERSION' in env and env['CCACHE_VERSION'] >= _ccache_version_min: - return True + ccache = env.WhereIs(ccache) + if not ccache: + return False pipe = SCons.Action._subproc( env, @@ -46,7 +49,6 @@ def exists(env): ) if pipe.wait() != 0: - print(f"Error: failed to execute '{env['CCACHE']}'") return False validated = False @@ -60,23 +62,25 @@ def exists(env): ccache_version = re.split("ccache version (.+)", line) if len(ccache_version) < 2: continue - ccache_version = parse_version(ccache_version[1]) - if ccache_version >= _ccache_version_min: + global _ccache_version_found + _ccache_version_found = parse_version(ccache_version[1]) + if _ccache_version_found >= _ccache_version_min: validated = True - if validated: - env['CCACHE_VERSION'] = ccache_version - else: - print(f"Error: failed to verify ccache version >= {_ccache_version_min}, found {ccache_version}") - return validated def generate(env): """Add ccache support.""" - # Absoluteify - env["CCACHE"] = env.WhereIs("$CCACHE") + # If we have already generated the tool, don't generate it again. + if "CCACHE_VERSION" in env: + return + + # If we can't find ccache, or it is too old a version, don't + # generate. + if not exists(env): + return # Propagate CCACHE related variables into the command environment for var, host_value in os.environ.items(): @@ -92,53 +96,23 @@ def generate(env): if env.ToolchainIs("clang"): env.AppendUnique(CCFLAGS=["-Qunused-arguments"]) - # Check whether icecream is requested and is a valid tool. - if "ICECC" in env: - icecream = SCons.Tool.Tool('icecream') - icecream_enabled = bool(icecream) and icecream.exists(env) - else: - icecream_enabled = False + # Record our found CCACHE_VERSION. Other tools that need to know + # about ccache (like iecc) should query this variable to determine + # if ccache is active. Looking at the CCACHE variable in the + # environment is not sufficient, since the user may have set it, + # but it doesn't work or is out of date. + env["CCACHE_VERSION"] = _ccache_version_found # Set up a performant ccache configuration. Here, we don't use a second preprocessor and # pass preprocessor arguments that deterministically expand source files so a stable # hash can be calculated on them. This both reduces the amount of work ccache needs to # do and increases the likelihood of a cache hit. + env["ENV"]["CCACHE_NOCPP2"] = 1 if env.ToolchainIs("clang"): - env["ENV"].pop("CCACHE_CPP2", None) - env["ENV"]["CCACHE_NOCPP2"] = "1" env.AppendUnique(CCFLAGS=["-frewrite-includes"]) elif env.ToolchainIs("gcc"): - if icecream_enabled: - # Newer versions of Icecream will drop -fdirectives-only from - # preprocessor and compiler flags if it does not find a remote - # build host to build on. ccache, on the other hand, will not - # pass the flag to the compiler if CCACHE_NOCPP2=1, but it will - # pass it to the preprocessor. The combination of setting - # CCACHE_NOCPP2=1 and passing the flag can lead to build - # failures. - - # See: https://jira.mongodb.org/browse/SERVER-48443 - # We have an open issue with Icecream and ccache to resolve the - # cause of these build failures. Once the bug is resolved and - # the fix is deployed, we can remove this entire conditional - # branch and make it like the one for clang. - # TODO: https://github.com/icecc/icecream/issues/550 - env["ENV"].pop("CCACHE_CPP2", None) - env["ENV"]["CCACHE_NOCPP2"] = "1" - else: - env["ENV"].pop("CCACHE_NOCPP2", None) - env["ENV"]["CCACHE_CPP2"] = "1" - env.AppendUnique(CCFLAGS=["-fdirectives-only"]) - - # Ensure ccache accounts for any extra files in use that affects the generated object - # file. This can be used for situations where a file is passed as an argument to a - # compiler parameter and differences in the file need to be accounted for in the - # hash result to prevent erroneous cache hits. - if "CCACHE_EXTRAFILES" in env and env["CCACHE_EXTRAFILES"]: - env["ENV"]["CCACHE_EXTRAFILES"] = ":".join([ - blackfile.path - for blackfile in env["CCACHE_EXTRAFILES"] - ]) + env.AppendUnique(CCFLAGS=["-fdirectives-only"]) + # Make a generator to expand to CCACHE in the case where we are # not a conftest. We don't want to use ccache for configure tests diff --git a/site_scons/site_tools/icecream.py b/site_scons/site_tools/icecream.py index 9cb57571725..6482c500c5a 100644 --- a/site_scons/site_tools/icecream.py +++ b/site_scons/site_tools/icecream.py @@ -12,15 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import SCons + import os import re import subprocess -import urllib from pkg_resources import parse_version -import SCons - _icecream_version_min = parse_version("1.1rc2") _icecream_version_gcc_remote_cpp = parse_version("1.2") @@ -43,44 +42,34 @@ class _BoundSubstitution: def icecc_create_env(env, target, source, for_signature): # Safe to assume unix here because icecream only works on Unix - mkdir = "mkdir -p ${TARGET.dir}" + mkdir = "mkdir -p ${ICECC_VERSION.Dir('').abspath}" # Create the env, use awk to get just the tarball name and we store it in # the shell variable $ICECC_VERSION_TMP so the subsequent mv command and # store it in a known location. Add any files requested from the user environment. - create_env = "ICECC_VERSION_TMP=$$(${SOURCES[0]} --$ICECC_COMPILER_TYPE ${SOURCES[1]} ${SOURCES[2]}" - - # TODO: It would be a little more elegant if things in - # ICECC_CREATE_ENV_ADDFILES were handled as sources, because we - # would get automatic dependency tracking. However, there are some - # wrinkles around the mapped case so we have opted to leave it as - # just interpreting the env for now. + create_env = "ICECC_VERSION_TMP=$$($ICECC_CREATE_ENV --$ICECC_COMPILER_TYPE $CC $CXX" for addfile in env.get('ICECC_CREATE_ENV_ADDFILES', []): - if isinstance(addfile, tuple): - if len(addfile) == 2: - if env['ICECREAM_VERSION'] > parse_version('1.1'): - raise Exception("This version of icecream does not support addfile remapping.") - create_env += " --addfile {}={}".format( - env.File(addfile[0]).srcnode().abspath, - env.File(addfile[1]).srcnode().abspath) - env.Depends(target, addfile[1]) - else: - raise Exception(f"Found incorrect icecream addfile format: {str(addfile)}" + - f"\ntuple must two elements of the form" + - f"\n('chroot dest path', 'source file path')") + if (type(addfile) == tuple + and len(addfile) == 2): + if env['ICECREAM_VERSION'] > parse_version('1.1'): + raise Exception("This version of icecream does not support addfile remapping.") + create_env += " --addfile {}={}".format( + env.File(addfile[0]).srcnode().abspath, + env.File(addfile[1]).srcnode().abspath) + env.Depends('$ICECC_VERSION', addfile[1]) + elif type(addfile) == str: + create_env += " --addfile {}".format(env.File(addfile).srcnode().abspath) + env.Depends('$ICECC_VERSION', addfile) else: - try: - create_env += f" --addfile {env.File(addfile).srcnode().abspath}" - env.Depends(target, addfile) - except: - # NOTE: abspath is required by icecream because of - # this line in icecc-create-env: - # https://github.com/icecc/icecream/blob/10b9468f5bd30a0fdb058901e91e7a29f1bfbd42/client/icecc-create-env.in#L534 - # which cuts out the two files based off the equals sign and - # starting slash of the second file - raise Exception(f"Found incorrect icecream addfile format: {type(addfile)}" + - f"\nvalue provided cannot be converted to a file path") - + # NOTE: abspath is required by icecream because of + # this line in icecc-create-env: + # https://github.com/icecc/icecream/blob/10b9468f5bd30a0fdb058901e91e7a29f1bfbd42/client/icecc-create-env.in#L534 + # which cuts out the two files based off the equals sign and + # starting slash of the second file + raise Exception("Found incorrect icecream addfile format: {}" + + "\nicecream addfiles must be a single path or tuple path format: " + + "('chroot dest path', 'source file path')".format( + str(addfile))) create_env += " | awk '/^creating .*\\.tar\\.gz/ { print $$2 }')" # Simply move our tarball to the expected locale. @@ -92,6 +81,10 @@ def icecc_create_env(env, target, source, for_signature): def generate(env): + + if not exists(env): + return + # icecc lower then 1.1 supports addfile remapping accidentally # and above it adds an empty cpuinfo so handle cpuinfo issues for icecream # below version 1.1 @@ -100,56 +93,77 @@ def generate(env): and os.path.exists('/proc/cpuinfo')): env.AppendUnique(ICECC_CREATE_ENV_ADDFILES=[('/proc/cpuinfo', '/dev/null')]) + env["ICECCENVCOMSTR"] = env.get("ICECCENVCOMSTR", "Generating environment: $TARGET") + env["ICECC_COMPILER_TYPE"] = env.get( + "ICECC_COMPILER_TYPE", os.path.basename(env.WhereIs("${CC}")) + ) + env.Append( + BUILDERS={ + "IcecreamEnv": SCons.Builder.Builder( + action=SCons.Action.CommandGeneratorAction( + icecc_create_env, {"comstr": "$ICECCENVCOMSTR"}, + ) + ) + } + ) + + # If we are going to load the ccache tool, but we haven't done so + # yet, then explicitly do it now. We need the ccache tool to be in + # place before we setup icecream because we need to do things a + # little differently if ccache is in play. If you don't use the + # TOOLS variable to configure your tools, you should explicitly + # load the ccache tool before you load icecream. + ccache_enabled = "CCACHE_VERSION" in env + if "ccache" in env["TOOLS"] and not ccache_enabled: + env.Tool("ccache") + # Absoluteify, so we can derive ICERUN env["ICECC"] = env.WhereIs("$ICECC") - if "ICERUN" in env: - # Absoluteify, for parity with ICECC - icerun = env.WhereIs("$ICERUN") - else: - icerun = env.File("$ICECC").File("icerun") - env["ICERUN"] = icerun + if not "ICERUN" in env: + env["ICERUN"] = env.File("$ICECC").File("icerun") - if "ICECC_CREATE_ENV" in env: - icecc_create_env_bin = env.WhereIs("$ICECC_CREATE_ENV") - else: - icecc_create_env_bin = env.File("ICECC").File("icecc-create-env") - env["ICECC_CREATE_ENV"] = icecc_create_env_bin - - # Make CC and CXX absolute paths too. This ensures the correct paths to - # compilers get passed to icecc-create-env rather than letting it - # potentially discover something we don't expect via PATH. - env["CC"] = env.WhereIs("$CC") - env["CXX"] = env.WhereIs("$CXX") + # Absoluteify, for parity with ICECC + env["ICERUN"] = env.WhereIs("$ICERUN") - # Set up defaults for configuration options - env['ICECREAM_TARGET_DIR'] = env.Dir( - env.get('ICECREAM_TARGET_DIR', '#./.icecream') - ) - verbose = env.get('ICECREAM_VERBOSE', False) - env['ICECREAM_DEBUG'] = env.get('ICECREAM_DEBUG', False) - - # We have a lot of things to build and run that the final user - # environment doesn't need to see or know about. Make a custom env - # that we use consistently from here to where we end up setting - # ICECREAM_RUN_ICECC in the user env. - setupEnv = env.Clone( - NINJA_SKIP=True + env["ICECC_CREATE_ENV"] = env.WhereIs( + env.get("ICECC_CREATE_ENV", "icecc-create-env") ) - if 'ICECC_VERSION' in setupEnv and bool(setupEnv['ICECC_VERSION']): + # Make CC and CXX absolute paths too. It is better for icecc. + env["CC"] = env.WhereIs("$CC") + env["CXX"] = env.WhereIs("$CXX") - if setupEnv["ICECC_VERSION"].startswith("http"): + have_explicit_icecc_version = 'ICECC_VERSION' in env and bool(env['ICECC_VERSION']) + have_icecc_version_url = have_explicit_icecc_version and env["ICECC_VERSION"].startswith("http") - quoted = urllib.parse.quote(setupEnv['ICECC_VERSION'], safe=[]) + if have_explicit_icecc_version and not have_icecc_version_url: + icecc_version_file = env.File('$ICECC_VERSION') + if not icecc_version_file.exists(): + raise Exception( + 'The ICECC_VERSION variable set set to {}, but this file does not exist'.format(icecc_version_file) + ) + env['ICECC_VERSION'] = icecc_version_file + else: + # Generate the deterministic name for our tarball + icecc_version_target_filename = env.subst("${CC}${CXX}.tar.gz").replace("/", "_")[ + 1: + ] + icecc_version_dir = env.Dir("$BUILD_ROOT/scons/icecc") + icecc_known_version = icecc_version_dir.File(icecc_version_target_filename) + + if have_icecc_version_url: + # We do the above weaker validation as opposed to + # urllib.urlparse (or similar). We really only support http + # URLs here and any other validation either requires a third + # party module or accepts things we don't. + env["ICECC_VERSION_URL"] = env["ICECC_VERSION"] + env["ICECC_VERSION"] = icecc_known_version # Use curl / wget to download the toolchain because SCons (and ninja) # are better at running shell commands than Python functions. - # - # TODO: This all happens SCons side now. Should we just use python to - # fetch instead? - curl = setupEnv.WhereIs("curl") - wget = setupEnv.WhereIs("wget") + curl = env.WhereIs("curl") + wget = env.WhereIs("wget") if curl: cmdstr = "curl -L" @@ -160,288 +174,99 @@ def generate(env): "You have specified an ICECC_VERSION that is a URL but you have neither wget nor curl installed." ) - # Copy ICECC_VERSION into ICECC_VERSION_URL so that we can - # change ICECC_VERSION without perturbing the effect of - # the action. - setupEnv['ICECC_VERSION_URL'] = setupEnv['ICECC_VERSION'] - setupEnv['ICECC_VERSION'] = icecc_version_file = setupEnv.Command( - target=f"$ICECREAM_TARGET_DIR/{quoted}", - source=[setupEnv.Value(quoted)], - action=SCons.Action.Action( - f"{cmdstr} -o $TARGET $ICECC_VERSION_URL", - "Downloading compiler package from $ICECC_VERSION_URL" if not verbose else str(), - ), - )[0] - + env.Command( + target="$ICECC_VERSION", + source=["$CC", "$CXX"], + action=[ + cmdstr + " -o $TARGET $ICECC_VERSION_URL", + ], + ) else: - # Convert the users selection into a File node and do some basic validation - setupEnv['ICECC_VERSION'] = icecc_version_file = setupEnv.File('$ICECC_VERSION') - - if not icecc_version_file.exists(): - raise Exception( - 'The ICECC_VERSION variable set set to {}, but this file does not exist'.format(icecc_version_file) - ) - - # This is what we are going to call the file names as known to SCons on disk - setupEnv["ICECC_VERSION_ID"] = "user_provided." + icecc_version_file.name - - else: - - setupEnv["ICECC_COMPILER_TYPE"] = setupEnv.get( - "ICECC_COMPILER_TYPE", os.path.basename(setupEnv.WhereIs("${CC}")) - ) - - # This is what we are going to call the file names as known to SCons on disk. We do the - # subst early so that we can call `replace` on the result. - setupEnv["ICECC_VERSION_ID"] = setupEnv.subst( - "icecc-create-env.${CC}${CXX}.tar.gz").replace("/", "_" - ) - - setupEnv["ICECC_VERSION"] = icecc_version_file = setupEnv.Command( - target="$ICECREAM_TARGET_DIR/$ICECC_VERSION_ID", - source=[ - "$ICECC_CREATE_ENV", - "$CC", - "$CXX" - ], - action=SCons.Action.Action( - icecc_create_env, - "Generating icecream compiler package: $TARGET" if not verbose else str(), - generator=True, + # Make a predictable name for the toolchain + env["ICECC_VERSION"] = env.File(icecc_known_version) + env.IcecreamEnv( + target="$ICECC_VERSION", + source=["$ICECC_CREATE_ENV", "$CC", "$CXX"], ) - )[0] - - # At this point, all paths above have produced a file of some sort. We now move on - # to producing our own signature for this local file. - setupEnv.Append( - ICECREAM_TARGET_BASE_DIR='$ICECREAM_TARGET_DIR', - ICECREAM_TARGET_BASE_FILE='$ICECC_VERSION_ID', - ICECREAM_TARGET_BASE='$ICECREAM_TARGET_BASE_DIR/$ICECREAM_TARGET_BASE_FILE', - ) - - # If the file we are planning to use is not within - # ICECREAM_TARGET_DIR then make a local copy of it that is. - if icecc_version_file.dir != env['ICECREAM_TARGET_DIR']: - setupEnv["ICECC_VERSION"] = icecc_version_file = setupEnv.Command( - target=[ - '${ICECREAM_TARGET_BASE}.local', - ], - source=icecc_version_file, - action=SCons.Defaults.Copy('$TARGET', '$SOURCE'), - ) - - # There is no point caching the copy. - setupEnv.NoCache(icecc_version_file) - - # Now, we compute our own signature of the local compiler package, - # and create yet another link to the compiler package with a name - # containing our computed signature. Now we know that we can give - # this filename to icecc and it will be assured to really reflect - # the contents of the package, and not the arbitrary naming of the - # file as found on the users filesystem or from - # icecc-create-env. We put the absolute path to that filename into - # a file that we can read from. - icecc_version_info = setupEnv.File(setupEnv.Command( - target=[ - '${ICECREAM_TARGET_BASE}.sha256', - '${ICECREAM_TARGET_BASE}.sha256.path', - ], - source=icecc_version_file, - action=SCons.Action.ListAction( - [ - - # icecc-create-env run twice with the same input will - # create files with identical contents, and identical - # filenames, but with different hashes because it - # includes timestamps. So we compute a new hash based - # on the actual stream contents of the file by - # untarring it into shasum. - SCons.Action.Action( - "tar xfO ${SOURCES[0]} | shasum -b -a 256 - | awk '{ print $1 }' > ${TARGETS[0]}", - "Calculating sha256 sum of ${SOURCES[0]}" if not verbose else str(), - ), - - SCons.Action.Action( - "ln -f ${SOURCES[0]} ${TARGETS[0].dir}/icecream_py_sha256_$$(cat ${TARGETS[0]}).tar.gz", - "Linking ${SOURCES[0]} to its sha256 sum name" if not verbose else str(), - ), - - SCons.Action.Action( - "echo ${TARGETS[0].dir.abspath}/icecream_py_sha256_$$(cat ${TARGETS[0]}).tar.gz > ${TARGETS[1]}", - "Storing sha256 sum name for ${SOURCES[0]} to ${TARGETS[1]}" if not verbose else str(), + # Our ICECC_VERSION isn't just a file, so we need to make + # things depend on it to ensure that it comes into being at + # the right time. Don't do that for conftests though: we never + # want to run them remote. + def icecc_toolchain_dependency_emitter(target, source, env): + if "conftest" not in str(target[0]): + env.Requires(target, "$ICECC_VERSION") + return target, source + + # Cribbed from Tool/cc.py and Tool/c++.py. It would be better if + # we could obtain this from SCons. + _CSuffixes = [".c"] + if not SCons.Util.case_sensitive_suffixes(".c", ".C"): + _CSuffixes.append(".C") + + _CXXSuffixes = [".cpp", ".cc", ".cxx", ".c++", ".C++"] + if SCons.Util.case_sensitive_suffixes(".c", ".C"): + _CXXSuffixes.append(".C") + + suffixes = _CSuffixes + _CXXSuffixes + for object_builder in SCons.Tool.createObjBuilders(env): + emitterdict = object_builder.builder.emitter + for suffix in emitterdict.keys(): + if not suffix in suffixes: + continue + base = emitterdict[suffix] + emitterdict[suffix] = SCons.Builder.ListEmitter( + [base, icecc_toolchain_dependency_emitter] ) - ], - ) - )) - - # We can't allow these to interact with the cache because the - # second action produces a file unknown to SCons. If caching were - # permitted, the other two files could be retrieved from cache but - # the file produced by the second action could not (and would not) - # be. We would end up with a broken setup. - setupEnv.NoCache(icecc_version_info) - - # Create a value node that, when built, contains the result of - # reading the contents of the sha256.path file. This way we can - # pull the value out of the file and substitute it into our - # wrapper script. - icecc_version_string_value = setupEnv.Command( - target=setupEnv.Value(None), - source=[ - icecc_version_info[1] - ], - action=SCons.Action.Action( - lambda env, target, source: target[0].write(source[0].get_text_contents()), - "Reading compiler package sha256 sum path from $SOURCE" if not verbose else str(), - ) - )[0] - - def icecc_version_string_generator(source, target, env, for_signature): - if for_signature: - return icecc_version_string_value.get_csig() - return icecc_version_string_value.read() - - # Set the values that will be interpolated into the run-icecc script. - setupEnv['ICECC_VERSION'] = icecc_version_string_generator - - # If necessary, we include the users desired architecture in the - # interpolated file. - icecc_version_arch_string = str() - if "ICECC_VERSION_ARCH" in setupEnv: - icecc_version_arch_string = "${ICECC_VERSION_ARCH}:" - - # Finally, create the run-icecc wrapper script. The contents will - # re-invoke icecc with our sha256 sum named file, ensuring that we - # trust the signature to be appropriate. In a pure SCons build, we - # actually wouldn't need this Substfile, we could just set - # env['ENV]['ICECC_VERSION'] to the Value node above. But that - # won't work for Ninja builds where we can't ask for the contents - # of such a node easily. Creating a Substfile means that SCons - # will take care of generating a file that Ninja can use. - run_icecc = setupEnv.Textfile( - target="$ICECREAM_TARGET_DIR/run-icecc.sh", - source=[ - '#!/bin/sh', - 'ICECC_VERSION=@icecc_version_arch@@icecc_version@ exec @icecc@ "$@"', - '', - ], - SUBST_DICT={ - '@icecc@' : '$ICECC', - '@icecc_version@' : '$ICECC_VERSION', - '@icecc_version_arch@' : icecc_version_arch_string, - }, - - # Don't change around the suffixes - TEXTFILEPREFIX=str(), - TEXTFILESUFFIX=str(), - - # Somewhat surprising, but even though Ninja will defer to - # SCons to invoke this, we still need ninja to be aware of it - # so that it knows to invoke SCons to produce it as part of - # TEMPLATE expansion. Since we have set NINJA_SKIP=True for - # setupEnv, we need to reverse that here. - NINJA_SKIP=False - ) - - setupEnv.AddPostAction( - run_icecc, - action=SCons.Defaults.Chmod('$TARGET', "u+x"), - ) - - setupEnv.Depends( - target=run_icecc, - dependency=[ - - # TODO: Without the ICECC dependency, changing ICECC doesn't cause the Substfile - # to regenerate. Why is this? - '$ICECC', - - # This dependency is necessary so that we build into this - # string before we create the file. - icecc_version_string_value, - - # TODO: SERVER-50587 We need to make explicit depends here because of NINJA_SKIP. Any - # dependencies in the nodes created in setupEnv with NINJA_SKIP would have - # that dependency chain hidden from ninja, so they won't be rebuilt unless - # added as dependencies here on this node that has NINJA_SKIP=False. - '$CC', - '$CXX', - icecc_version_file, - ], - ) - - # From here out, we make changes to the users `env`. - setupEnv = None - - env['ICECREAM_RUN_ICECC'] = run_icecc[0] - - def icecc_toolchain_dependency_emitter(target, source, env): - if "conftest" not in str(target[0]): - # Requires or Depends? There are trade-offs: - # - # If it is `Depends`, then enabling or disabling icecream - # will cause a global recompile. But, if you regenerate a - # new compiler package, you will get a rebuild. If it is - # `Requires`, then enabling or disabling icecream will not - # necessarily cause a global recompile (it depends if - # C[,C,XX]FLAGS get changed when you do so), but on the - # other hand if you regenerate a new compiler package you - # will *not* get a rebuild. - # - # For now, we are opting for `Requires`, because it seems - # preferable that opting in or out of icecream shouldn't - # force a rebuild. - env.Requires(target, "$ICECREAM_RUN_ICECC") - return target, source - - # Cribbed from Tool/cc.py and Tool/c++.py. It would be better if - # we could obtain this from SCons. - _CSuffixes = [".c"] - if not SCons.Util.case_sensitive_suffixes(".c", ".C"): - _CSuffixes.append(".C") - - _CXXSuffixes = [".cpp", ".cc", ".cxx", ".c++", ".C++"] - if SCons.Util.case_sensitive_suffixes(".c", ".C"): - _CXXSuffixes.append(".C") - - suffixes = _CSuffixes + _CXXSuffixes - for object_builder in SCons.Tool.createObjBuilders(env): - emitterdict = object_builder.builder.emitter - for suffix in emitterdict.keys(): - if not suffix in suffixes: - continue - base = emitterdict[suffix] - emitterdict[suffix] = SCons.Builder.ListEmitter( - [base, icecc_toolchain_dependency_emitter] - ) - - # Check whether ccache is requested and is a valid tool. - if "CCACHE" in env: - ccache = SCons.Tool.Tool('ccache') - ccache_enabled = bool(ccache) and ccache.exists(env) - else: - ccache_enabled = False if env.ToolchainIs("clang"): env["ENV"]["ICECC_CLANG_REMOTE_CPP"] = 1 elif env.ToolchainIs("gcc"): - if env["ICECREAM_VERSION"] < _icecream_version_gcc_remote_cpp: - # We aren't going to use ICECC_REMOTE_CPP because icecc - # 1.1 doesn't offer it. We disallow fallback to local - # builds because the fallback is serial execution. - env["ENV"]["ICECC_CARET_WORKAROUND"] = 0 - elif not ccache_enabled: - # If we can, we should make Icecream do its own preprocessing - # to reduce concurrency on the local host. We should not do - # this when ccache is in use because ccache will execute - # Icecream to do its own preprocessing and then execute - # Icecream as the compiler on the preprocessed source. - env["ENV"]["ICECC_REMOTE_CPP"] = 1 + if env["ICECREAM_VERSION"] >= _icecream_version_gcc_remote_cpp: + if ccache_enabled: + # Newer versions of Icecream will drop -fdirectives-only from + # preprocessor and compiler flags if it does not find a remote + # build host to build on. ccache, on the other hand, will not + # pass the flag to the compiler if CCACHE_NOCPP2=1, but it will + # pass it to the preprocessor. The combination of setting + # CCACHE_NOCPP2=1 and passing the flag can lead to build + # failures. + + # See: https://jira.mongodb.org/browse/SERVER-48443 + + # We have an open issue with Icecream and ccache to resolve the + # cause of these build failures. Once the bug is resolved and + # the fix is deployed, we can remove this entire conditional + # branch and make it like the one for clang. + # TODO: https://github.com/icecc/icecream/issues/550 + env["ENV"].pop("CCACHE_NOCPP2", None) + env["ENV"]["CCACHE_CPP2"] = 1 + try: + env["CCFLAGS"].remove("-fdirectives-only") + except ValueError: + pass + else: + # If we can, we should make Icecream do its own preprocessing + # to reduce concurrency on the local host. We should not do + # this when ccache is in use because ccache will execute + # Icecream to do its own preprocessing and then execute + # Icecream as the compiler on the preprocessed source. + env["ENV"]["ICECC_REMOTE_CPP"] = 1 if "ICECC_SCHEDULER" in env: env["ENV"]["USE_SCHEDULER"] = env["ICECC_SCHEDULER"] + # Build up the string we will set in the environment to tell icecream + # about the compiler package. + icecc_version_string = "${ICECC_VERSION.abspath}" + if "ICECC_VERSION_ARCH" in env: + icecc_version_string = "${ICECC_VERSION_ARCH}:" + icecc_version_string + + # Use our BoundSubstitition class to put ICECC_VERSION into env['ENV'] with + # substitution in play. This avoids an early subst which can behave + # strangely. + env["ENV"]["ICECC_VERSION"] = _BoundSubstitution(env, icecc_version_string) + # If ccache is in play we actually want the icecc binary in the # CCACHE_PREFIX environment variable, not on the command line, per # the ccache documentation on compiler wrappers. Otherwise, just @@ -452,22 +277,19 @@ def generate(env): # compiler flags (things like -fdirectives-only), but we still try # to do the right thing. if ccache_enabled: - # If the path to CCACHE_PREFIX isn't absolute, then it will - # look it up in PATH. That isn't what we want here, we make - # the path absolute. - env['ENV']['CCACHE_PREFIX'] = _BoundSubstitution(env, "${ICECREAM_RUN_ICECC.abspath}") + env["ENV"]["CCACHE_PREFIX"] = _BoundSubstitution(env, "$ICECC") else: # Make a generator to expand to ICECC in the case where we are - # not a conftest. We never want to run conftests remotely. - # Ideally, we would do this for the CCACHE_PREFIX case above, - # but unfortunately if we did we would never actually see the - # conftests, because the BoundSubst means that we will never - # have a meaningful `target` variable when we are in ENV. - # Instead, rely on the ccache.py tool to do it's own filtering - # out of conftests. + # not a conftest. We never want to run conftests + # remotely. Ideally, we would do this for the CCACHE_PREFIX + # case above, but unfortunately if we did we would never + # actually see the conftests, because the BoundSubst means + # that we will never have a meaningful `target` variable when + # we are in ENV. Instead, rely on the ccache.py tool to do + # it's own filtering out of conftests. def icecc_generator(target, source, env, for_signature): if "conftest" not in str(target[0]): - return '$ICECREAM_RUN_ICECC' + return '$ICECC' return '' env['ICECC_GENERATOR'] = icecc_generator @@ -497,28 +319,21 @@ def generate(env): env[command] = " ".join(["$( $ICERUN $)", env[command]]) # Uncomment these to debug your icecc integration - if env['ICECREAM_DEBUG']: - env['ENV']['ICECC_DEBUG'] = 'debug' - env['ENV']['ICECC_LOGFILE'] = 'icecc.log' + # env['ENV']['ICECC_DEBUG'] = 'debug' + # env['ENV']['ICECC_LOGFILE'] = 'icecc.log' def exists(env): - if not env.subst("$ICECC"): - return False + # Assume the tool has run if we already know the version. + if "ICECREAM_VERSION" in env: + return True - icecc = env.WhereIs("$ICECC") + icecc = env.get("ICECC", False) + if not icecc: + return False + icecc = env.WhereIs(icecc) if not icecc: - # TODO: We should not be printing here because we don't always know the - # use case for loading this tool. It may be that the user desires - # writing this output to a log file or not even displaying it at all. - # We should instead be invoking a callback to SConstruct that it can - # interpret as needed. Or better yet, we should use some SCons logging - # and error API, if and when one should emerge. - print(f"Error: icecc not found at {env['ICECC']}") return False - - if 'ICECREAM_VERSION' in env and env['ICECREAM_VERSION'] >= _icecream_version_min: - return True pipe = SCons.Action._subproc( env, @@ -529,26 +344,9 @@ def exists(env): ) if pipe.wait() != 0: - print(f"Error: failed to execute '{env['ICECC']}'") return False validated = False - - if "ICERUN" in env: - # Absoluteify, for parity with ICECC - icerun = env.WhereIs("$ICERUN") - else: - icerun = env.File("$ICECC").File("icerun") - if not icerun: - print(f"Error: the icerun wrapper does not exist at {icerun} as expected") - - if "ICECC_CREATE_ENV" in env: - icecc_create_env_bin = env.WhereIs("$ICECC_CREATE_ENV") - else: - icecc_create_env_bin = env.File("ICECC").File("icecc-create-env") - if not icecc_create_env_bin: - print(f"Error: the icecc-create-env utility does not exist at {icecc_create_env_bin} as expected") - for line in pipe.stdout: line = line.decode("utf-8") if validated: @@ -565,7 +363,5 @@ def exists(env): if validated: env['ICECREAM_VERSION'] = icecc_version - else: - print(f"Error: failed to verify icecream version >= {_icecream_version_min}, found {icecc_version}") return validated diff --git a/site_scons/site_tools/ninja.py b/site_scons/site_tools/ninja.py index 31aacf81534..7e777f5b686 100644 --- a/site_scons/site_tools/ninja.py +++ b/site_scons/site_tools/ninja.py @@ -19,7 +19,6 @@ import importlib import io import shutil import shlex -import textwrap from glob import glob from os.path import join as joinpath @@ -130,7 +129,7 @@ def get_order_only(node): """Return a list of order only dependencies for node.""" if node.prerequisites is None: return [] - return [get_path(src_file(prereq)) for prereq in node.prerequisites if is_valid_dependent_node(prereq)] + return [get_path(src_file(prereq)) for prereq in node.prerequisites] def get_dependencies(node, skip_sources=False): @@ -139,44 +138,21 @@ def get_dependencies(node, skip_sources=False): return [ get_path(src_file(child)) for child in node.children() - if child not in node.sources and is_valid_dependent_node(child) + if child not in node.sources ] - return [get_path(src_file(child)) for child in node.children() if is_valid_dependent_node(child)] + return [get_path(src_file(child)) for child in node.children()] -def get_inputs(node, skip_unknown_types=False): - """ - Collect the Ninja inputs for node. - - If the given node has inputs which can not be converted into something - Ninja can process, this will throw an exception. Optionally, those nodes - that are not processable can be skipped as inputs with the - skip_unknown_types keyword arg. - """ +def get_inputs(node): + """Collect the Ninja inputs for node.""" executor = node.get_executor() if executor is not None: inputs = executor.get_all_sources() else: inputs = node.sources - # Some Nodes (e.g. Python.Value Nodes) won't have files associated. We allow these to be - # optionally skipped to enable the case where we will re-invoke SCons for things - # like TEMPLATE. Otherwise, we have no direct way to express the behavior for such - # Nodes in Ninja, so we raise a hard error - ninja_nodes = [] - for input_node in inputs: - if isinstance(input_node, (SCons.Node.FS.Base, SCons.Node.Alias.Alias)): - ninja_nodes.append(input_node) - else: - if skip_unknown_types: - continue - raise Exception("Can't process {} node '{}' as an input for '{}'".format( - type(input_node), - str(input_node), - str(node))) - - # convert node items into raw paths/aliases for ninja - return [get_path(src_file(o)) for o in ninja_nodes] + inputs = [get_path(src_file(o)) for o in inputs] + return inputs def get_outputs(node): @@ -194,40 +170,6 @@ def get_outputs(node): return outputs -def generate_depfile(env, node, dependencies): - """ - Ninja tool function for writing a depfile. The depfile should include - the node path followed by all the dependent files in a makefile format. - - dependencies arg can be a list or a subst generator which returns a list. - """ - - depfile = os.path.join(get_path(env['NINJA_BUILDDIR']), str(node) + '.depfile') - - # subst_list will take in either a raw list or a subst callable which generates - # a list, and return a list of CmdStringHolders which can be converted into raw strings. - # If a raw list was passed in, then scons_list will make a list of lists from the original - # values and even subst items in the list if they are substitutable. Flatten will flatten - # the list in that case, to ensure for either input we have a list of CmdStringHolders. - deps_list = env.Flatten(env.subst_list(dependencies)) - - # Now that we have the deps in a list as CmdStringHolders, we can convert them into raw strings - # and make sure to escape the strings to handle spaces in paths. We also will sort the result - # keep the order of the list consistent. - escaped_depends = sorted([dep.escape(env.get("ESCAPE", lambda x: x)) for dep in deps_list]) - depfile_contents = str(node) + ": " + ' '.join(escaped_depends) - - need_rewrite = False - try: - with open(depfile, 'r') as f: - need_rewrite = (f.read() != depfile_contents) - except FileNotFoundError: - need_rewrite = True - - if need_rewrite: - os.makedirs(os.path.dirname(depfile) or '.', exist_ok=True) - with open(depfile, 'w') as f: - f.write(depfile_contents) class SConsToNinjaTranslator: """Translates SCons Actions into Ninja build objects.""" @@ -299,14 +241,11 @@ class SConsToNinjaTranslator: # dependencies don't really matter when we're going to shove these to # the bottom of ninja's DAG anyway and Textfile builders can have text # content as their source which doesn't work as an implicit dep in - # ninja. We suppress errors on input Nodes types that we cannot handle - # since we expect that the re-invocation of SCons will handle dependency - # tracking for those Nodes and their dependents. + # ninja. if name == "_action": return { "rule": "TEMPLATE", "outputs": get_outputs(node), - "inputs": get_inputs(node, skip_unknown_types=True), "implicit": get_dependencies(node, skip_sources=True), } @@ -477,16 +416,8 @@ class NinjaState: "rspfile_content": "$rspc", "pool": "local_pool", }, - # Ninja does not automatically delete the archive before - # invoking ar. The ar utility will append to an existing archive, which - # can cause duplicate symbols if the symbols moved between object files. - # Native SCons will perform this operation so we need to force ninja - # to do the same. See related for more info: - # https://jira.mongodb.org/browse/SERVER-49457 "AR": { - "command": "{}$env$AR @$out.rsp".format( - '' if sys.platform == "win32" else "rm -f $out && " - ), + "command": "$env$AR @$out.rsp", "description": "Archiving $out", "rspfile": "$out.rsp", "rspfile_content": "$rspc", @@ -546,7 +477,6 @@ class NinjaState: "command": "$SCONS_INVOCATION_W_TARGETS", "description": "Regenerating $out", "generator": 1, - "depfile": os.path.join(get_path(env['NINJA_BUILDDIR']), '$out.depfile'), # Console pool restricts to 1 job running at a time, # it additionally has some special handling about # passing stdin, stdout, etc to process in this pool @@ -625,8 +555,6 @@ class NinjaState: ninja.comment("Generated by scons. DO NOT EDIT.") - ninja.variable("builddir", get_path(self.env['NINJA_BUILDDIR'])) - for pool_name, size in self.pools.items(): ninja.pool(pool_name, size) @@ -736,16 +664,6 @@ class NinjaState: build["outputs"] = first_output - # Optionally a rule can specify a depfile, and SCons can generate implicit - # dependencies into the depfile. This allows for dependencies to come and go - # without invalidating the ninja file. The depfile was created in ninja specifically - # for dealing with header files appearing and disappearing across rebuilds, but it can - # be repurposed for anything, as long as you have a way to regenerate the depfile. - # More specific info can be found here: https://ninja-build.org/manual.html#_depfile - if rule is not None and rule.get('depfile') and build.get('deps_files'): - path = build['outputs'] if SCons.Util.is_List(build['outputs']) else [build['outputs']] - generate_depfile(self.env, path[0], build.pop('deps_files', [])) - if "inputs" in build: build["inputs"].sort() @@ -756,13 +674,13 @@ class NinjaState: # Special handling for outputs and implicit since we need to # aggregate not replace for each builder. - for agg_key in ["outputs", "implicit", "inputs"]: + for agg_key in ["outputs", "implicit"]: new_val = template_builds.get(agg_key, []) # Use pop so the key is removed and so the update # below will not overwrite our aggregated values. cur_val = template_builder.pop(agg_key, []) - if is_List(cur_val): + if isinstance(cur_val, list): new_val += cur_val else: new_val.append(cur_val) @@ -780,25 +698,19 @@ class NinjaState: # generate this rule even though SCons should know we're # dependent on SCons files. # - # The REGENERATE rule uses depfile, so we need to generate the depfile - # in case any of the SConscripts have changed. The depfile needs to be - # path with in the build and the passed ninja file is an abspath, so - # we will use SCons to give us the path within the build. Normally - # generate_depfile should not be called like this, but instead be called - # through the use of custom rules, and filtered out in the normal - # list of build generation about. However, because the generate rule - # is hardcoded here, we need to do this generate_depfile call manually. - ninja_file_path = self.env.File(ninja_file).path - generate_depfile( - self.env, - ninja_file_path, - self.env['NINJA_REGENERATE_DEPS'] - ) - + # TODO: We're working on getting an API into SCons that will + # allow us to query the actual SConscripts used. Right now + # this glob method has deficiencies like skipping + # jstests/SConscript and being specific to the MongoDB + # repository layout. ninja.build( - ninja_file_path, + self.env.File(ninja_file).path, rule="REGENERATE", - implicit=[__file__], + implicit=[ + self.env.File("#SConstruct").path, + __file__, + ] + + sorted(glob("src/**/SConscript", recursive=True)), ) # If we ever change the name/s of the rules that include @@ -810,8 +722,8 @@ class NinjaState: pool="console", implicit=[ninja_file], variables={ - "cmd": "ninja -f {} -t compdb {}CC CXX > compile_commands.json".format( - ninja_file, '-x ' if self.env.get('NINJA_COMPDB_EXPAND') else '' + "cmd": "ninja -f {} -t compdb CC CXX > compile_commands.json".format( + ninja_file ) }, ) @@ -927,13 +839,7 @@ def get_command_env(env): if windows: command_env += "set '{}={}' && ".format(key, value) else: - # We address here *only* the specific case that a user might have - # an environment variable which somehow gets included and has - # spaces in the value. These are escapes that Ninja handles. This - # doesn't make builds on paths with spaces (Ninja and SCons issues) - # nor expanding response file paths with spaces (Ninja issue) work. - value = value.replace(r' ', r'$ ') - command_env += "{}='{}' ".format(key, value) + command_env += "{}={} ".format(key, value) env["NINJA_ENV_VAR_CACHE"] = command_env return command_env @@ -987,7 +893,7 @@ def gen_get_response_file_command(env, rule, tool, tool_is_dynamic=False): variables[rule] = cmd if use_command_env: variables["env"] = get_command_env(env) - return rule, variables, [tool_command] + return rule, variables return get_response_file_command @@ -1017,21 +923,13 @@ def generate_command(env, node, action, targets, sources, executor=None): return cmd.replace("$", "$$") -def get_generic_shell_command(env, node, action, targets, sources, executor=None): +def get_shell_command(env, node, action, targets, sources, executor=None): return ( "CMD", { "cmd": generate_command(env, node, action, targets, sources, executor=None), "env": get_command_env(env), }, - # Since this function is a rule mapping provider, it must return a list of dependencies, - # and usually this would be the path to a tool, such as a compiler, used for this rule. - # However this function is to generic to be able to reliably extract such deps - # from the command, so we return a placeholder empty list. It should be noted that - # generally this function will not be used soley and is more like a template to generate - # the basics for a custom provider which may have more specific options for a provier - # function for a custom NinjaRuleMapping. - [] ) @@ -1067,39 +965,12 @@ def get_command(env, node, action): # pylint: disable=too-many-branches if not comstr: return None - provider = __NINJA_RULE_MAPPING.get(comstr, get_generic_shell_command) - rule, variables, provider_deps = provider(sub_env, node, action, tlist, slist, executor=executor) + provider = __NINJA_RULE_MAPPING.get(comstr, get_shell_command) + rule, variables = provider(sub_env, node, action, tlist, slist, executor=executor) # Get the dependencies for all targets implicit = list({dep for tgt in tlist for dep in get_dependencies(tgt)}) - # Now add in the other dependencies related to the command, - # e.g. the compiler binary. The ninja rule can be user provided so - # we must do some validation to resolve the dependency path for ninja. - for provider_dep in provider_deps: - - provider_dep = sub_env.subst(provider_dep) - if not provider_dep: - continue - - # If the tool is a node, then SCons will resolve the path later, if its not - # a node then we assume it generated from build and make sure it is existing. - if isinstance(provider_dep, SCons.Node.Node) or os.path.exists(provider_dep): - implicit.append(provider_dep) - continue - - # Many commands will assume the binary is in the path, so - # we accept this as a possible input from a given command. - provider_dep_abspath = sub_env.WhereIs(provider_dep) - if provider_dep_abspath: - implicit.append(provider_dep_abspath) - continue - - # Possibly these could be ignore and the build would still work, however it may not always - # rebuild correctly, so we hard stop, and force the user to fix the issue with the provided - # ninja rule. - raise Exception(f"Could not resolve path for {provider_dep} dependency on node '{node}'") - ninja_build = { "order_only": get_order_only(node), "outputs": get_outputs(node), @@ -1162,21 +1033,18 @@ def register_custom_handler(env, name, handler): def register_custom_rule_mapping(env, pre_subst_string, rule): - """Register a function to call for a given rule.""" + """Register a custom handler for SCons function actions.""" global __NINJA_RULE_MAPPING __NINJA_RULE_MAPPING[pre_subst_string] = rule -def register_custom_rule(env, rule, command, description="", deps=None, pool=None, use_depfile=False): +def register_custom_rule(env, rule, command, description="", deps=None, pool=None): """Allows specification of Ninja rules from inside SCons files.""" rule_obj = { "command": command, "description": description if description else "{} $out".format(rule), } - if use_depfile: - rule_obj["depfile"] = os.path.join(get_path(env['NINJA_BUILDDIR']), '$out.depfile') - if deps is not None: rule_obj["deps"] = deps @@ -1214,27 +1082,6 @@ def ninja_contents(original): return wrapper -def CheckNinjaCompdbExpand(env, context): - """ Configure check testing if ninja's compdb can expand response files""" - - context.Message('Checking if ninja compdb can expand response files... ') - ret, output = context.TryAction( - action='ninja -f $SOURCE -t compdb -x CMD_RSP > $TARGET', - extension='.ninja', - text=textwrap.dedent(""" - rule CMD_RSP - command = $cmd @$out.rsp > fake_output.txt - description = Building $out - rspfile = $out.rsp - rspfile_content = $rspc - build fake_output.txt: CMD_RSP fake_input.txt - cmd = echo - pool = console - rspc = "test" - """)) - result = '@fake_output.txt.rsp' not in output - context.Result(result) - return result def ninja_stat(_self, path): """ @@ -1345,20 +1192,12 @@ def generate(env): env["NINJA_PREFIX"] = env.get("NINJA_PREFIX", "build") env["NINJA_SUFFIX"] = env.get("NINJA_SUFFIX", "ninja") env["NINJA_ALIAS_NAME"] = env.get("NINJA_ALIAS_NAME", "generate-ninja") - env['NINJA_BUILDDIR'] = env.get("NINJA_BUILDDIR", env.Dir(".ninja").path) + ninja_file_name = env.subst("${NINJA_PREFIX}.${NINJA_SUFFIX}") ninja_file = env.Ninja(target=ninja_file_name, source=[]) env.AlwaysBuild(ninja_file) env.Alias("$NINJA_ALIAS_NAME", ninja_file) - # TODO: API for getting the SConscripts programmatically - # exists upstream: https://github.com/SCons/scons/issues/3625 - def ninja_generate_deps(env): - return sorted([env.File("#SConstruct").path] + glob("**/SConscript", recursive=True)) - env['_NINJA_REGENERATE_DEPS_FUNC'] = ninja_generate_deps - - env['NINJA_REGENERATE_DEPS'] = env.get('NINJA_REGENERATE_DEPS', '${_NINJA_REGENERATE_DEPS_FUNC(__env__)}') - # This adds the required flags such that the generated compile # commands will create depfiles as appropriate in the Ninja file. if env["PLATFORM"] == "win32": @@ -1366,11 +1205,9 @@ def generate(env): else: env.Append(CCFLAGS=["-MMD", "-MF", "${TARGET}.d"]) - env.AddMethod(CheckNinjaCompdbExpand, "CheckNinjaCompdbExpand") - # Provide a way for custom rule authors to easily access command # generation. - env.AddMethod(get_generic_shell_command, "NinjaGetGenericShellCommand") + env.AddMethod(get_shell_command, "NinjaGetShellCommand") env.AddMethod(gen_get_response_file_command, "NinjaGenResponseFileProvider") # Provides a way for users to handle custom FunctionActions they |