#! /usr/bin/env python3 import atexit import argparse import glob import os import re import shutil import subprocess import sys ns3_path = os.path.dirname(os.path.abspath(__file__)) out_dir = os.sep.join([ns3_path, "build"]) lock_file = os.sep.join([ns3_path, ".lock-waf_%s_build" % sys.platform]) print_buffer = "" verbose = True # Prints everything in the print_buffer on exit def exit_handler(dry_run): global print_buffer, verbose # We should not print anything in run except if dry_run or verbose if not dry_run and not verbose: return if print_buffer == "": return if dry_run: print("The following commands would be executed:") elif verbose: print("Finished executing the following commands:") print(print_buffer[1:]) def on_off_argument(parser, option_name, help_on, help_off=None): parser.add_argument('--enable-%s' % option_name, help=('Enable %s' % help_on) if help_off is None else help_on, action="store_true", default=None) parser.add_argument('--disable-%s' % option_name, help=('Disable %s' % help_on) if help_off is None else help_off, action="store_true", default=None) return parser def on_off(condition): return "ON" if condition else "OFF" def on_off_condition(args, cmake_flag, option_name): enable_option = args.__getattribute__("enable_" + option_name) disable_option = args.__getattribute__("disable_" + option_name) cmake_arg = None if enable_option is not None or disable_option is not None: cmake_arg = "-DNS3_%s=%s" % (cmake_flag, on_off(enable_option and not disable_option)) return cmake_arg def parse_args(argv): parser = argparse.ArgumentParser(description="ns-3 wrapper for the CMake build system") sub_parser = parser.add_subparsers() parser_build = sub_parser.add_parser('build', help=('Accepts a list of targets to build,' ' or builds the entire project if no target is given')) parser_build.add_argument('build', help='Build the entire project or the specified target and dependencies', action="store", nargs='*', default=None) parser_build.add_argument('--dry-run', help="Do not execute the commands", action="store_true", default=None, dest="build_dry_run") parser_configure = sub_parser.add_parser('configure', help='Try "./ns3 configure --help" for more configuration options') parser_configure.add_argument('configure', action='store_true', default=False) parser_configure.add_argument('-d', '--build-profile', help='Build profile', dest='build_profile', choices=["debug", "release", "optimized"], action="store", type=str, default=None) parser_configure.add_argument('-G', help=('CMake generator ' '(e.g. https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html)'), action="store", type=str, default=None) parser_configure.add_argument('--cxx-standard', help='Compile NS-3 with the given C++ standard', type=str, default=None) parser_configure = on_off_argument(parser_configure, "asserts", "the asserts regardless of the compile mode") parser_configure = on_off_argument(parser_configure, "des-metrics", "Logging all events in a json file with the name of the executable " "(which must call CommandLine::Parse(argc, argv)") parser_configure = on_off_argument(parser_configure, "build-version", "embedding git changes as a build version during build") parser_configure = on_off_argument(parser_configure, "examples", "the ns-3 examples") parser_configure = on_off_argument(parser_configure, "gcov", "code coverage analysis") parser_configure = on_off_argument(parser_configure, "gtk", "GTK support in ConfigStore") parser_configure = on_off_argument(parser_configure, "logs", "the logs regardless of the compile mode") parser_configure = on_off_argument(parser_configure, "mpi", "the MPI support for distributed simulation") parser_configure = on_off_argument(parser_configure, "python-bindings", "python bindings") parser_configure = on_off_argument(parser_configure, "tests", "the ns-3 tests") parser_configure = on_off_argument(parser_configure, "sanitizers", "address, memory leaks and undefined behavior sanitizers") parser_configure = on_off_argument(parser_configure, "static", "Build a single static library with all ns-3", "Restore the shared libraries") parser_configure = on_off_argument(parser_configure, "sudo", "use of sudo to setup suid bits on ns3 executables.") parser_configure = on_off_argument(parser_configure, "verbose", "printing of additional build system messages") parser_configure = on_off_argument(parser_configure, "warnings", "compiler warnings") parser_configure = on_off_argument(parser_configure, "werror", "Treat compiler warnings as errors", "Treat compiler warnings as warnings") parser_configure.add_argument('--enable-modules', help='List of modules to build (e.g. core;network;internet)', action="store", type=str, default=None) parser_configure.add_argument('--disable-modules', help='List of modules not to build (e.g. lte;wimax)', action="store", type=str, default=None) parser_configure.add_argument('--lcov-report', help=('Generate a code coverage report ' '(use this option after configuring with --enable-gcov and running a program)'), action="store_true", default=None) parser_configure.add_argument('--lcov-zerocounters', help=('Zero the lcov counters' '(use this option before rerunning a program' 'when generating repeated lcov reports)'), action="store_true", default=None) parser_configure.add_argument('--out', '--output-directory', help=('Use BRITE integration support, given by the indicated path,' ' to allow the use of the BRITE topology generator'), type=str, default=None, dest="output_directory") parser_configure.add_argument('--with-brite', help=('Use BRITE integration support, given by the indicated path,' ' to allow the use of the BRITE topology generator'), type=str, default=None) parser_configure.add_argument('--with-click', help='Path to Click source or installation prefix for NS-3 Click Integration support', type=str, default=None) parser_configure.add_argument('--with-openflow', help='Path to OFSID source for NS-3 OpenFlow Integration support', type=str, default=None) parser_configure.add_argument('--force-refresh', help='Force refresh the CMake cache by deleting' 'the cache and reconfiguring the project', action="store_true", default=None) parser_configure.add_argument('--prefix', help='Target output directory to install', action="store", default=None) parser_configure.add_argument('--dry-run', help="Do not execute the commands", action="store_true", default=None, dest="configure_dry_run") parser_clean = sub_parser.add_parser('clean', help='Removes files created by waf and ns3') parser_clean.add_argument('clean', action="store_true", default=False) parser_clean.add_argument('--dry-run', help="Do not execute the commands", action="store_true", default=None, dest="clean_dry_run") parser_install = sub_parser.add_parser('install', help='Install ns-3') parser_install.add_argument('install', action="store_true", default=False) parser_uninstall = sub_parser.add_parser('uninstall', help='Uninstall ns-3') parser_uninstall.add_argument('uninstall', action="store_true", default=False) parser_run = sub_parser.add_parser('run', help='Try "./ns3 run --help" for more runtime options') parser_run.add_argument('run', help='Build and run executable. If --no-build is present, build step is skipped.', type=str, default='') parser_run.add_argument('--no-build', help='Skip build step.', action="store_true", default=False) parser_run.add_argument('--command-template', help=('Template of the command used to run the program given by run;' ' It should be a shell command string containing %%s inside,' ' which will be replaced by the actual program.'), type=str, default=None) parser_run.add_argument('--cwd', help='Set the working directory for a program.', action="store", type=str, default=None) parser_run.add_argument('--gdb', help='Change the default command template to run programs with gdb', action="store_true", default=None) parser_run.add_argument('--valgrind', help='Change the default command template to run programs with valgrind', action="store_true", default=None) parser_run.add_argument('--vis', '--visualize', help='Modify --run arguments to enable the visualizer', action="store_true", dest="visualize", default=None) parser_run.add_argument('--dry-run', help="Do not execute the commands", action="store_true", default=None, dest="run_dry_run") parser_run.add_argument('--enable-sudo', help='Use sudo to setup suid bits on ns3 executables.', dest='enable_sudo', action='store_true', default=False) parser_run.add_argument('-v', '--verbose', help='Print which commands were executed', dest='run_verbose', action='store_true', default=False) parser_shell = sub_parser.add_parser('shell', help='Try "./ns3 shell --help" for more runtime options') parser_shell.add_argument('shell', help='Export necessary environment variables and open a shell', action="store_true", default=False) parser_docs = sub_parser.add_parser('docs', help='Try "./ns3 docs --help" for more documentation options') parser_docs.add_argument('docs', help='Build project documentation', choices=["manual", "models", "tutorial", "contributing", "sphinx", "doxygen-no-build", "doxygen", "all"], action="store", type=str, default=None) parser.add_argument('-j', '--jobs', help='Set number of parallel jobs', action='store', type=int, dest="jobs", default=max(1, os.cpu_count() - 1)) parser.add_argument('--dry-run', help="Do not execute the commands", action="store_true", default=None, dest="dry_run") parser.add_argument('--check-config', help='Print the current configuration.', action="store_true", default=None) parser.add_argument('--quiet', help="Don't print task lines, i.e. messages saying which tasks are being executed.", action="store_true", default=None) parser.add_argument('--check', help='DEPRECATED (run ./test.py)', action='store_true', default=None) parser.add_argument('--check-profile', help='Print out current build profile', action='store_true', default=None) parser.add_argument('--check-version', help='Print the current build version', action='store_true', default=None) # parser.add_argument('--docset', # help=( # 'Create Docset, without building. This requires the docsetutil tool from Xcode 9.2 or earlier.' # 'See Bugzilla 2196 for more details.'), # action="store_true", default=None, # dest="docset_build") # Parse known arguments and separate from unknown arguments args, unknown_args = parser.parse_known_args(argv) # Merge dry_runs dry_run_args = [(args.__getattribute__(name) if name in args else None) for name in ["build_dry_run", "clean_dry_run", "configure_dry_run", "dry_run", "run_dry_run"]] args.dry_run = dry_run_args.count(True) > 0 # If some positional options are not in args, set them to false. for option in ["clean", "configure", "docs", "install", "run", "shell", "uninstall"]: if option not in args: setattr(args, option, False) if args.run and args.enable_sudo is None: args.enable_sudo = True # Filter arguments before -- setattr(args, "program_args", []) if unknown_args: try: args_separator_index = argv.index('--') args.program_args = argv[args_separator_index + 1:] except ValueError: msg = "Unknown options were given: {options}.\n" \ "To see the allowed options add the `--help` option.\n" \ "To forward configuration or runtime options, put them after '--'.\n" if args.run: msg += "Try: ./ns3 run {target} -- {options}\n" if args.configure: msg += "Try: ./ns3 configure -- {options}\n" msg = msg.format(options=", ".join(unknown_args), target=args.run) raise Exception(msg) return args def check_build_profile(output_directory): # Check the c4cache for the build type (in case there are multiple cmake cache folders c4che_path = os.sep.join([output_directory, "c4che", "_cache.py"]) build_profile = None ns3_version = None ns3_modules = None ns3_modules_tests = [] ns3_modules_apiscan = [] ns3_modules_bindings = [] enable_sudo = False if output_directory and os.path.exists(c4che_path): c4che_info = {} exec(open(c4che_path).read(), globals(), c4che_info) build_profile = c4che_info["BUILD_PROFILE"] ns3_version = c4che_info["VERSION"] ns3_modules = c4che_info["NS3_ENABLED_MODULES"] if "ENABLE_SUDO" in c4che_info: enable_sudo = c4che_info["ENABLE_SUDO"] if ns3_modules: if c4che_info["ENABLE_TESTS"]: ns3_modules_tests = [x + "-test" for x in ns3_modules] if c4che_info["ENABLE_PYTHON_BINDINGS"]: ns3_modules_bindings = [x + "-bindings" for x in ns3_modules] if "ENABLE_SCAN_PYTHON_BINDINGS" in c4che_info and c4che_info["ENABLE_SCAN_PYTHON_BINDINGS"]: ns3_modules_apiscan = [x + "-apiscan" for x in ns3_modules] ns3_modules = ns3_modules + ns3_modules_tests + ns3_modules_apiscan + ns3_modules_bindings return build_profile, ns3_version, ns3_modules, enable_sudo def print_and_buffer(message): global print_buffer # print(message) print_buffer += "\n" + message def clean_cmake_artifacts(dry_run=False): print_and_buffer("rm -R %s" % os.path.relpath(out_dir, ns3_path)) if not dry_run: shutil.rmtree(out_dir, ignore_errors=True) cmake_cache_files = glob.glob("%s/**/CMakeCache.txt" % ns3_path, recursive=True) for cmake_cache_file in cmake_cache_files: dirname = os.path.dirname(cmake_cache_file) print_and_buffer("rm -R %s" % os.path.relpath(dirname, ns3_path)) if not dry_run: shutil.rmtree(dirname, ignore_errors=True) if os.path.exists(lock_file): print_and_buffer("rm %s" % os.path.relpath(lock_file, ns3_path)) if not dry_run: os.remove(lock_file) def search_cmake_cache(build_profile): # Search for the CMake cache cmake_cache_files = glob.glob("%s/**/CMakeCache.txt" % ns3_path, recursive=True) current_cmake_cache_folder = None current_cmake_generator = None if cmake_cache_files: # In case there are multiple cache files, get the correct one for cmake_cache_file in cmake_cache_files: # We found the right cache folder if current_cmake_cache_folder and current_cmake_generator: break # Still looking for it current_cmake_cache_folder = None current_cmake_generator = None with open(cmake_cache_file, "r") as f: lines = f.read().split("\n") while len(lines): line = lines[0] lines.pop(0) # Check for EOF if current_cmake_cache_folder and current_cmake_generator: break # Check the build profile if "build_profile:INTERNAL" in line: if build_profile: if build_profile == line.split("=")[-1]: current_cmake_cache_folder = os.path.dirname(cmake_cache_file) else: current_cmake_cache_folder = os.path.dirname(cmake_cache_file) # Check the generator if "CMAKE_GENERATOR:" in line: current_cmake_generator = line.split("=")[-1] if not current_cmake_generator: # Search for available generators cmake_generator_map = {"make": "Unix Makefiles", "ninja": "Ninja", "xcodebuild": "Xcode" } available_generators = [] for generator in cmake_generator_map.keys(): if shutil.which(generator): available_generators.append(generator) # Select the first one if len(available_generators) == 0: raise Exception("No generator available.") current_cmake_generator = cmake_generator_map[available_generators[0]] return current_cmake_cache_folder, current_cmake_generator def project_not_configured(config_msg=""): print("You need to configure ns-3 first: try ./ns3 configure%s" % config_msg) exit(1) def check_config(current_cmake_cache_folder): if current_cmake_cache_folder is None: project_not_configured() waf_like_config_table = current_cmake_cache_folder + os.sep + "ns3wafconfig.txt" if not os.path.exists(waf_like_config_table): project_not_configured() with open(waf_like_config_table, "r") as f: print(f.read()) def project_configured(current_cmake_cache_folder): if not current_cmake_cache_folder: return False if not os.path.exists(current_cmake_cache_folder): return False if not os.path.exists(os.sep.join([current_cmake_cache_folder, "CMakeCache.txt"])): return False return True def configure_cmake(cmake, args, current_cmake_cache_folder, current_cmake_generator, output, dry_run=False): # Aggregate all flags to configure CMake cmake_args = [cmake] if not project_configured(current_cmake_cache_folder): # Create a new cmake_cache folder if one does not exist current_cmake_cache_folder = os.sep.join([ns3_path, "cmake_cache"]) if not os.path.exists(current_cmake_cache_folder): print_and_buffer("mkdir %s" % os.path.relpath(current_cmake_cache_folder, ns3_path)) if not dry_run: os.makedirs(current_cmake_cache_folder, exist_ok=True) # Set default build type to default if a previous cache doesn't exist if args.build_profile is None: args.build_profile = "debug" # Set generator if a previous cache doesn't exist if args.G is None: args.G = current_cmake_generator # C++ standard if args.cxx_standard is not None: cmake_args.append("-DCMAKE_CXX_STANDARD=%s" % args.cxx_standard) # Build type if args.build_profile is not None: args.build_profile = args.build_profile.lower() if args.build_profile not in ["debug", "release", "optimized"]: raise Exception("Unknown build type") else: if args.build_profile == "debug": cmake_args.append("-DCMAKE_BUILD_TYPE=debug") else: cmake_args.append("-DCMAKE_BUILD_TYPE=release") cmake_args.append("-DNS3_NATIVE_OPTIMIZATIONS=%s" % on_off((args.build_profile == "optimized"))) options = (("ASSERT", "asserts"), ("COVERAGE", "gcov"), ("DES_METRICS", "des_metrics"), ("ENABLE_BUILD_VERSION", "build_version"), ("ENABLE_SUDO", "sudo"), ("EXAMPLES", "examples"), ("GTK3", "gtk"), ("LOG", "logs"), ("MPI", "mpi"), ("PYTHON_BINDINGS", "python_bindings"), ("SANITIZE", "sanitizers"), ("STATIC", "static"), ("TESTS", "tests"), ("VERBOSE", "verbose"), ("WARNINGS", "warnings"), ("WARNINGS_AS_ERRORS", "werror"), ) for (cmake_flag, option_name) in options: arg = on_off_condition(args, cmake_flag, option_name) if arg: cmake_args.append(arg) if args.lcov_zerocounters is not None: cmake_args.append("-DNS3_COVERAGE_ZERO_COUNTERS=%s" % on_off(args.lcov_zerocounters)) # Output, Brite, Click and Openflow directories if args.output_directory is not None: cmake_args.append("-DNS3_OUTPUT_DIRECTORY=%s" % args.output_directory) if args.with_brite is not None: cmake_args.append("-DNS3_WITH_BRITE=%s" % args.with_brite) if args.with_click is not None: cmake_args.append("-DNS3_WITH_CLICK=%s" % args.with_click) if args.with_openflow is not None: cmake_args.append("-DNS3_WITH_OPENFLOW=%s" % args.with_openflow) if args.prefix is not None: cmake_args.append("-DCMAKE_INSTALL_PREFIX=%s" % args.prefix) # Process enabled/disabled modules if args.enable_modules: cmake_args.append("-DNS3_ENABLED_MODULES=%s" % args.enable_modules) if args.disable_modules: cmake_args.append("-DNS3_DISABLED_MODULES=%s" % args.disable_modules) # Try to set specified generator (will probably fail if there is an old cache) if args.G: cmake_args.append("-G") cmake_args.append(args.G) # Append CMake flags passed using the -- separator cmake_args.extend(args.program_args) # Configure cmake cmake_args.append("..") # for now, assuming the cmake_cache directory is inside the ns-3-dev folder # Echo out the configure command print_and_buffer("cd %s; %s ; cd %s" % (os.path.relpath(current_cmake_cache_folder, ns3_path), " ".join(cmake_args), os.path.relpath(ns3_path, current_cmake_cache_folder) ) ) # Run cmake if not dry_run: ret = subprocess.run(cmake_args, cwd=current_cmake_cache_folder, stdout=output) if ret.returncode != 0: exit(ret.returncode) def get_program_shortcuts(build_profile, ns3_version): build_status_file = os.sep.join([out_dir, "build-status.py"]) # Import programs from build-status.py programs_dict = {} exec(open(build_status_file).read(), globals(), programs_dict) # We can now build a map to simplify things for users (at this point we could remove versioning prefix/suffix) ns3_program_map = {} for program in programs_dict["ns3_runnable_programs"]: if "pch_exec" in program: continue temp_path = program.replace(out_dir, "").split(os.sep) # Remove version prefix and build type suffix from shortcuts (or keep them too?) temp_path[-1] = temp_path[-1].replace("-" + build_profile, "").replace("ns" + ns3_version + "-", "") # Deal with scratch subdirs if "scratch" in temp_path and len(temp_path) > 3: subdir = "_".join([*temp_path[2:-1], ""]) temp_path[-1] = temp_path[-1].replace(subdir, "") # Check if there is a .cc file for that specific program source_file_path = os.sep.join(temp_path) + ".cc" source_shortcut = False if os.path.exists(ns3_path + source_file_path): source_shortcut = True program = program.strip() while len(temp_path): # Shortcuts: /src/aodv/examples/aodv can be accessed with aodv/examples/aodv, examples/aodv, aodv shortcut_path = os.sep.join(temp_path) ns3_program_map[shortcut_path] = program if source_shortcut: ns3_program_map[shortcut_path + ".cc"] = program temp_path.pop(0) if programs_dict["ns3_runnable_scripts"]: scratch_scripts = glob.glob(os.path.join(ns3_path, "scratch", "*.py"), recursive=True) programs_dict["ns3_runnable_scripts"].extend(scratch_scripts) for program in programs_dict["ns3_runnable_scripts"]: temp_path = program.replace(ns3_path, "").split(os.sep) program = program.strip() while len(temp_path): shortcut_path = os.sep.join(temp_path) ns3_program_map[shortcut_path] = program temp_path.pop(0) return ns3_program_map def parse_version(version_str): version = version_str.split(".") version = tuple(map(int, version)) return version def cmake_check_version(): # Check CMake version cmake = shutil.which("cmake") if not cmake: print("Error: CMake not found; please install version 3.10 or greater, or modify $PATH") exit(-1) cmake_output = subprocess.check_output([cmake, "--version"]).decode("utf-8") version = re.findall("version (.*)", cmake_output)[0] if parse_version(version) < parse_version("3.10.0"): print("Error: CMake found at %s but version %s is older than 3.10" % (cmake, version)) exit(-1) return cmake, version def cmake_build(current_cmake_cache_folder, output, jobs, target=None, dry_run=False): _, version = cmake_check_version() # Older CMake versions don't accept the number of jobs directly jobs_part = ("-j %d" % jobs) if parse_version(version) >= parse_version("3.12.0") else "" target_part = (" --target %s" % target) if target else "" cmake_build_command = "cmake --build . %s%s" % (jobs_part, target_part) print_and_buffer("cd %s; %s ; cd %s" % (os.path.relpath(current_cmake_cache_folder, ns3_path), cmake_build_command, os.path.relpath(ns3_path, current_cmake_cache_folder) ) ) if not dry_run: ret = subprocess.run(cmake_build_command.split(), cwd=current_cmake_cache_folder, stdout=output) # In case of failure, exit prematurely with the return code from the build if ret.returncode != 0: exit(ret.returncode) def extract_cmakecache_settings(current_cmake_cache_folder): try: with open(current_cmake_cache_folder + os.sep + "CMakeCache.txt", "r", encoding="utf-8") as f: contents = f.read() except FileNotFoundError as e: raise e current_settings = re.findall("(NS3_.*):.*=(.*)", contents) # extract NS3 specific settings current_settings.extend(re.findall("(CMAKE_BUILD_TYPE):.*=(.*)", contents)) # extract build type current_settings.extend(re.findall("(CMAKE_GENERATOR):.*=(.*)", contents)) # extract generator return dict(current_settings) def reconfigure_cmake_to_force_refresh(cmake, current_cmake_cache_folder, output, dry_run=False): import json settings_bak_file = "settings.json" # Extract settings or recover from the backup if not os.path.exists(settings_bak_file): settings = extract_cmakecache_settings(current_cmake_cache_folder) else: with open(settings_bak_file, "r", encoding="utf-8") as f: settings = json.load(f) # Delete cache folder and then recreate it cache_path = os.path.relpath(current_cmake_cache_folder, ns3_path) print_and_buffer("rm -R %s; mkdir %s" % (cache_path, cache_path)) if not dry_run: shutil.rmtree(current_cmake_cache_folder) os.mkdir(current_cmake_cache_folder) # Save settings backup to prevent loss with open(settings_bak_file, "w", encoding="utf-8") as f: json.dump(settings, f, indent=2) # Reconfigure CMake preserving previous NS3 settings cmake_args = [cmake] for setting in settings.items(): if setting[1]: cmake_args.append("-D%s=%s" % setting) cmake_args.append("..") # Echo out the configure command print_and_buffer("cd %s; %s ; cd %s" % (os.path.relpath(ns3_path, current_cmake_cache_folder), " ".join(cmake_args), os.path.relpath(current_cmake_cache_folder, ns3_path) ) ) # Call cmake if not dry_run: ret = subprocess.run(cmake_args, cwd=current_cmake_cache_folder, stdout=output) # If it succeeds, delete backup, otherwise raise exception if ret.returncode == 0: os.remove(settings_bak_file) else: raise Exception("Reconfiguring CMake to force refresh failed. " "A backup of the settings was saved in %s" % settings_bak_file) def get_target_to_build(program_path, ns3_version, build_profile): if ".py" in program_path: return None build_profile_suffix = "" if build_profile in ["release"] else "-" + build_profile program_name = "".join(*re.findall("(.*)ns%s-(.*)%s" % (ns3_version, build_profile_suffix), program_path)) if "scratch" in program_path: # Get the path to the program and replace slashes with underlines # to get unique targets for CMake, preventing collisions with modules examples return program_name.replace(out_dir, "").replace("/", "_")[1:] else: # Other programs just use their normal names (without version prefix and build_profile suffix) as targets return program_name.split("/")[-1] def configuration_step(current_cmake_cache_folder, current_cmake_generator, args, output, dry_run=False): # Search for the CMake binary cmake, _ = cmake_check_version() # If --force-refresh, we load settings from the CMakeCache, delete it, then reconfigure CMake to # force refresh cached packages/libraries that were installed/removed, without losing the current settings if args.force_refresh: reconfigure_cmake_to_force_refresh(cmake, current_cmake_cache_folder, output, dry_run) exit(0) # Call cmake to configure/reconfigure/refresh the project configure_cmake(cmake, args, current_cmake_cache_folder, current_cmake_generator, output, dry_run ) # If manually configuring, we end the script earlier exit(0) def build_step(args, build_and_run, target_to_run, current_cmake_cache_folder, ns3_modules, ns3_version, build_profile, output): # There are two scenarios where we build everything: ./ns3 build and ./ns3 --check if args.check or ("build" in args and len(args.build) == 0): cmake_build(current_cmake_cache_folder, jobs=args.jobs, output=output, dry_run=args.dry_run ) # If we are building specific targets, we build them one by one if "build" in args: non_executable_targets = ["apiscan-all", "check-version", "cmake-format", "docs", "doxygen", "doxygen-no-build", "sphinx", "manual", "models", "tutorial", "contributing", "install", "uninstall", ] # Build targets in the list for target in args.build: if target in ns3_modules: target = "lib" + target elif target not in non_executable_targets: target = get_target_to_build(target, ns3_version, build_profile) else: # Sphinx target should have the sphinx prefix if target in ["manual", "models", "tutorial"]: target = "sphinx_%s" % target # Docs should build both doxygen and sphinx based docs if target == "docs": target = "sphinx" args.build.append("doxygen") cmake_build(current_cmake_cache_folder, jobs=args.jobs, target=target, output=output, dry_run=args.dry_run) # The remaining case is when we want to build something to run if build_and_run: cmake_build(current_cmake_cache_folder, jobs=args.jobs, target=get_target_to_build(target_to_run, ns3_version, build_profile), output=output, dry_run=args.dry_run ) def run_step(args, target_to_run, target_args): libdir = "%s/lib" % out_dir path_sep = ";" if sys.platform == "win32" else ":" custom_env = {"PATH": libdir, "PYTHONPATH": "%s/bindings/python" % out_dir, } if sys.platform != "win32": custom_env["LD_LIBRARY_PATH"] = libdir proc_env = os.environ.copy() for (key, value) in custom_env.items(): if key in proc_env: proc_env[key] += path_sep + value else: proc_env[key] = value debugging_software = [] working_dir = ns3_path use_shell = False target_args += args.program_args # running test.py/check? if args.check: target_to_run = os.sep.join([ns3_path, "test.py"]) target_args = ["--no-build", "--jobs=%d" % args.jobs] elif args.shell: target_to_run = "bash" use_shell = True else: # running a python script? if ".py" in target_to_run: target_args = [target_to_run] + target_args target_to_run = "python3" # running from ns-3-dev (ns3_path) or cwd if args.cwd: working_dir = args.cwd # running valgrind? if args.valgrind: debugging_software.append(shutil.which("valgrind")) # running gdb? if args.gdb: debugging_software.extend([shutil.which("gdb"), "--args"]) # running with the visualizer? if args.visualize: target_args.append("--SimulatorImplementationType=ns3::VisualSimulatorImpl") # running with command template? if args.command_template: commands = (args.command_template % target_to_run).split() target_to_run = commands[0] target_args = commands[1:] + target_args # running mpi on the CI? if target_to_run in ["mpiexec", "mpirun"] and os.getenv("MPI_CI"): if shutil.which("ompi_info"): target_args = ["--oversubscribe"] + target_args target_args = ["--allow-run-as-root"] + target_args program_arguments = [*debugging_software, target_to_run, *target_args] if verbose or args.dry_run: exported_variables = "export " for (variable, value) in custom_env.items(): if variable == "PATH": value = "$PATH" + path_sep + libdir exported_variables += "%s=%s " % (variable, value) print_and_buffer("cd %s; %s; %s" % (os.path.relpath(ns3_path, working_dir), exported_variables, " ".join(program_arguments) ) ) if not args.dry_run: try: ret = subprocess.run(program_arguments, env=proc_env, cwd=working_dir, shell=use_shell) exit(ret.returncode) except KeyboardInterrupt: print("Process was interrupted by the user") # Debugging this with PyCharm is a no no. It refuses to work hanging indefinitely def sudo_command(command: list, password: str): # Run command and feed the sudo password proc = subprocess.Popen(['sudo', '-S', *command], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ).communicate(input=password.encode() + b'\n') stdout, stderr = proc[0].decode(), proc[1].decode() # Clean sudo password after each command subprocess.Popen(["sudo", "-k"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() # Check if the password is wrong if "try again" in stderr: raise Exception("Incorrect sudo password") return stdout, stderr def sudo_step(args, target_to_run, configure_post_build: set): # Check if sudo exists sudo = shutil.which("sudo") if not sudo: raise Exception("Sudo is required by --enable-sudo, but it was not found") # We do this for specified targets if --enable-sudo was set in the run sub-parser # And to all executables if set in the configure sub-parser targets_to_sudo = configure_post_build if target_to_run: targets_to_sudo.add(target_to_run) password = os.getenv("SUDO_PASSWORD", None) if not args.dry_run: if password is None: from getpass import getpass password = getpass(prompt="Sudo password:") import stat for target in targets_to_sudo: # Check if the file was already built if not os.path.exists(target): continue # Check if we need to set anything fstat = os.stat(target) if (fstat.st_mode & stat.S_ISUID) == stat.S_ISUID: continue # Log commands relative_path_to_target = os.path.relpath(target, ns3_path) chown_command = "chown root {}".format(relative_path_to_target) chmod_command = "chmod u+s {}".format(relative_path_to_target) print_and_buffer("; ".join([chown_command, chmod_command])) # Change permissions if not args.dry_run: out, err = sudo_command(chown_command.split(), password) if len(out) > 0: raise Exception("Failed to chown: ", relative_path_to_target) out, err = sudo_command(chmod_command.split(), password) if len(out) > 0: raise Exception("Failed to chmod: ", relative_path_to_target) return def refuse_run_as_root(): # Check if the user is root and refuse to run username = os.getenv("USER", "") if username == "root": raise Exception("Refusing to run as root. --enable-sudo will request your password when needed") def main(): global out_dir, verbose # Refuse to run with sudo refuse_run_as_root() # Parse arguments args = parse_args(sys.argv[1:]) atexit.register(exit_handler, dry_run=args.dry_run) output = subprocess.DEVNULL if args.quiet else None # no arguments were passed, so can't possibly be reconfiguring anything, then we refresh and rebuild if len(sys.argv) == 1: args.build = [] # Read contents from lock (output directory is important) if os.path.exists(lock_file): exec(open(lock_file).read(), globals()) # Clean project if needed if args.clean: clean_cmake_artifacts(dry_run=args.dry_run) # We end things earlier when cleaning return # Docs options become cmake targets if args.docs: args.build = [args.docs] if args.docs != "all" else ["sphinx", "doxygen"] # Installation and uninstallation options become cmake targets if args.install: args.build = ['install'] if args.uninstall: args.build = ['uninstall'] # Get build profile build_profile, ns3_version, ns3_modules, enable_sudo = check_build_profile(out_dir) if args.check_profile: if build_profile: print("Build profile: %s" % build_profile) else: project_not_configured() if args.check_version: args.build = ["check-version"] # Check if running something or reconfiguring ns-3 run_only = False build_and_run = False if args.run: # When running, default to not verbose verbose = args.run_verbose # If not verbose, silence the rest of the script if not verbose: output = subprocess.DEVNULL # Check whether we are only running or we need to build first if not args.no_build: build_and_run = True else: run_only = True target_to_run = None target_args = [] current_cmake_cache_folder = None if not args.check and (run_only or build_and_run): target_to_run = args.run if len(target_to_run) > 0: # While testing a weird case appeared where the target to run is between quotes, # so we remove in case they exist if target_to_run[0] in ["\"", "'"] and target_to_run[-1] in ["\"", "'"]: target_to_run = target_to_run[1:-1] target_to_run = target_to_run.split() target_to_run, target_args = target_to_run[0], target_to_run[1:] else: raise Exception("You need to specify a program to run") if not run_only: # Get current CMake cache folder and CMake generator (used when reconfiguring) current_cmake_cache_folder, current_cmake_generator = search_cmake_cache(build_profile) if args.check_config: check_config(current_cmake_cache_folder) # We end things earlier if only checking the current project configuration return if args.configure: configuration_step(current_cmake_cache_folder, current_cmake_generator, args, output, args.dry_run ) if not project_configured(current_cmake_cache_folder): project_not_configured() if ns3_modules is None: project_not_configured() # We could also replace the "ns3-" prefix used in c4che with the "lib" prefix currently used in cmake ns3_modules = [module.replace("ns3-", "") for module in ns3_modules] # Now that CMake is configured, we can look for c++ targets in build-status.py ns3_programs = get_program_shortcuts(build_profile, ns3_version) # If we have a target to run, replace shortcut with full path or raise exception if run_only or build_and_run: if target_to_run in ns3_programs: target_to_run = ns3_programs[target_to_run] else: raise Exception("Couldn't find the specified program: %s" % target_to_run) if "build" in args: complete_targets = [] for target in args.build: complete_targets.append(ns3_programs[target] if target in ns3_programs else target) args.build = complete_targets del complete_targets if not run_only: build_step(args, build_and_run, target_to_run, current_cmake_cache_folder, ns3_modules, ns3_version, build_profile, output ) def remove_overlapping_path(base_path, target_path): """ Remove overlapping paths from output directory and target_to_run :param base_path: output path of the ns-3 build :param target_path: path to the executable to run :return: target_path without the overlapping parts """ target_path = target_path.split(os.sep) base_path = base_path.split(os.sep) while target_path[0] in base_path: target_path = target_path[1:] target_path = os.sep.join(target_path) return target_path if not args.check and not args.shell and target_to_run and not ".py" in target_to_run: target_to_run = remove_overlapping_path(out_dir, target_to_run) # Waf doesn't add version prefix and build type suffix to the scratches, so we remove them if current_cmake_cache_folder is None: if "scratch" in target_to_run and run_only: waf_target_to_run = target_to_run.replace(os.path.basename(target_to_run), args.run) if os.path.exists(os.sep.join([out_dir, waf_target_to_run])): target_to_run = waf_target_to_run target_to_run = os.sep.join([out_dir, target_to_run]) # If we're only trying to run the target, we need to check if it actually exists first if (run_only or build_and_run) and ".py" not in target_to_run and not os.path.exists(target_to_run): raise Exception("Executable has not been built yet") # Setup program as sudo if enable_sudo or (args.run and args.enable_sudo): sudo_step(args, target_to_run, set(ns3_programs.values()) if enable_sudo else set()) # Finally, we try to run it if args.check or args.shell or run_only or build_and_run: run_step(args, target_to_run, target_args) return main()