2 # PYTHON_ARGCOMPLETE_OKAY
4 # SPDX-License-Identifier: BSD-2-Clause-FreeBSD
6 # Copyright (c) 2018 Alex Richardson <arichardson@FreeBSD.org>
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
11 # 1. Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # 2. Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
17 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32 # This script makes it easier to build on non-FreeBSD systems by bootstrapping
33 # bmake and inferring required compiler variables.
35 # On FreeBSD you can use it the same way as just calling make:
36 # `MAKEOBJDIRPREFIX=~/obj ./tools/build/make.py buildworld -DWITH_FOO`
38 # On Linux and MacOS you will either need to set XCC/XCXX/XLD/XCPP or pass
39 # --cross-bindir to specify the path to the cross-compiler bindir:
40 # `MAKEOBJDIRPREFIX=~/obj ./tools/build/make.py
41 # --cross-bindir=/path/to/cross/compiler buildworld -DWITH_FOO TARGET=foo
49 from pathlib import Path
52 def run(cmd, **kwargs):
53 cmd = list(map(str, cmd)) # convert all Path objects to str
55 subprocess.check_call(cmd, **kwargs)
58 def bootstrap_bmake(source_root, objdir_prefix):
59 bmake_source_dir = source_root / "contrib/bmake"
60 bmake_build_dir = objdir_prefix / "bmake-build"
61 bmake_install_dir = objdir_prefix / "bmake-install"
62 bmake_binary = bmake_install_dir / "bin/bmake"
64 if (bmake_install_dir / "bin/bmake").exists():
66 print("Bootstrapping bmake...")
67 # TODO: check if the host system bmake is new enough and use that instead
68 if not bmake_build_dir.exists():
69 os.makedirs(str(bmake_build_dir))
70 env = os.environ.copy()
72 env.update(new_env_vars)
74 if sys.platform.startswith("linux"):
75 # Work around the deleted file bmake/missing/sys/cdefs.h
76 # TODO: bmake should keep the compat sys/cdefs.h
77 env["CFLAGS"] = "-I{src}/tools/build/cross-build/include/common " \
78 "-I{src}/tools/build/cross-build/include/linux " \
79 "-D_GNU_SOURCE=1".format(src=source_root)
81 "--with-default-sys-path=" + str(bmake_install_dir / "share/mk"),
82 "--with-machine=amd64", # TODO? "--with-machine-arch=amd64",
83 "--without-filemon", "--prefix=" + str(bmake_install_dir)]
84 run(["sh", bmake_source_dir / "boot-strap"] + configure_args,
85 cwd=str(bmake_build_dir), env=env)
87 run(["sh", bmake_source_dir / "boot-strap", "op=install"] + configure_args,
88 cwd=str(bmake_build_dir))
89 print("Finished bootstrapping bmake...")
93 def debug(*args, **kwargs):
96 print(*args, **kwargs)
99 def is_make_var_set(var):
101 x.startswith(var + "=") or x == ("-D" + var) for x in sys.argv[1:])
104 def check_required_make_env_var(varname, binary_name, bindir):
106 if os.getenv(varname):
109 sys.exit("Could not infer value for $" + varname + ". Either set $" +
110 varname + " or pass --cross-bindir=/cross/compiler/dir/bin")
111 # try to infer the path to the tool
112 guess = os.path.join(bindir, binary_name)
113 if not os.path.isfile(guess):
114 sys.exit("Could not infer value for $" + varname + ": " + guess +
116 new_env_vars[varname] = guess
117 debug("Inferred", varname, "as", guess)
119 if parsed_args.debug:
120 run([guess, "--version"])
123 def default_cross_toolchain():
124 # default to homebrew-installed clang on MacOS if available
125 if sys.platform.startswith("darwin"):
126 if shutil.which("brew"):
127 llvm_dir = subprocess.getoutput("brew --prefix llvm")
128 if llvm_dir and Path(llvm_dir, "bin").exists():
129 return str(Path(llvm_dir, "bin"))
133 if __name__ == "__main__":
134 parser = argparse.ArgumentParser(
135 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
136 parser.add_argument("--host-bindir",
137 help="Directory to look for cc/c++/cpp/ld to build "
138 "host (" + sys.platform + ") binaries",
140 parser.add_argument("--cross-bindir", default=default_cross_toolchain(),
141 help="Directory to look for cc/c++/cpp/ld to build "
142 "target binaries (only needed if XCC/XCPP/XLD "
144 parser.add_argument("--cross-compiler-type", choices=("clang", "gcc"),
146 help="Compiler type to find in --cross-bindir (only "
147 "needed if XCC/XCPP/XLD are not set)"
148 "Note: using CC is currently highly experimental")
149 parser.add_argument("--host-compiler-type", choices=("cc", "clang", "gcc"),
151 help="Compiler type to find in --host-bindir (only "
152 "needed if CC/CPP/CXX are not set). ")
153 parser.add_argument("--debug", action="store_true",
154 help="Print information on inferred env vars")
155 parser.add_argument("--clean", action="store_true",
156 help="Do a clean rebuild instead of building with "
158 parser.add_argument("--no-clean", action="store_false", dest="clean",
159 help="Do a clean rebuild instead of building with "
162 import argcomplete # bash completion:
164 argcomplete.autocomplete(parser)
167 parsed_args, bmake_args = parser.parse_known_args()
169 MAKEOBJDIRPREFIX = os.getenv("MAKEOBJDIRPREFIX")
170 if not MAKEOBJDIRPREFIX:
171 sys.exit("MAKEOBJDIRPREFIX is not set, cannot continue!")
172 if not Path(MAKEOBJDIRPREFIX).is_dir():
174 "Chosen MAKEOBJDIRPREFIX=" + MAKEOBJDIRPREFIX + " doesn't exit!")
175 objdir_prefix = Path(MAKEOBJDIRPREFIX).absolute()
176 source_root = Path(__file__).absolute().parent.parent.parent
179 if not sys.platform.startswith("freebsd"):
180 if not is_make_var_set("TARGET") or not is_make_var_set("TARGET_ARCH"):
181 if "universe" not in sys.argv and "tinderbox" not in sys.argv:
182 sys.exit("TARGET= and TARGET_ARCH= must be set explicitly "
183 "when building on non-FreeBSD")
184 # infer values for CC/CXX/CPP
186 if sys.platform.startswith(
187 "linux") and parsed_args.host_compiler_type == "cc":
188 # FIXME: bsd.compiler.mk doesn't handle the output of GCC if it
189 # is /usr/bin/cc on Ubuntu since it doesn't contain the GCC string.
190 parsed_args.host_compiler_type = "gcc"
192 if parsed_args.host_compiler_type == "gcc":
193 default_cc, default_cxx, default_cpp = ("gcc", "g++", "cpp")
194 # FIXME: this should take values like `clang-9` and then look for
195 # clang-cpp-9, etc. Would alleviate the need to set the bindir on
196 # ubuntu/debian at least.
197 elif parsed_args.host_compiler_type == "clang":
198 default_cc, default_cxx, default_cpp = (
199 "clang", "clang++", "clang-cpp")
201 default_cc, default_cxx, default_cpp = ("cc", "c++", "cpp")
203 check_required_make_env_var("CC", default_cc, parsed_args.host_bindir)
204 check_required_make_env_var("CXX", default_cxx,
205 parsed_args.host_bindir)
206 check_required_make_env_var("CPP", default_cpp,
207 parsed_args.host_bindir)
208 # Using the default value for LD is fine (but not for XLD!)
210 use_cross_gcc = parsed_args.cross_compiler_type == "gcc"
211 # On non-FreeBSD we need to explicitly pass XCC/XLD/X_COMPILER_TYPE
212 check_required_make_env_var("XCC", "gcc" if use_cross_gcc else "clang",
213 parsed_args.cross_bindir)
214 check_required_make_env_var("XCXX",
215 "g++" if use_cross_gcc else "clang++",
216 parsed_args.cross_bindir)
217 check_required_make_env_var("XCPP",
218 "cpp" if use_cross_gcc else "clang-cpp",
219 parsed_args.cross_bindir)
220 check_required_make_env_var("XLD", "ld" if use_cross_gcc else "ld.lld",
221 parsed_args.cross_bindir)
222 check_required_make_env_var("STRIPBIN",
223 "strip" if use_cross_gcc else "llvm-strip",
224 parsed_args.cross_bindir)
226 bmake_binary = bootstrap_bmake(source_root, objdir_prefix)
227 # at -j1 cleandir+obj is unbearably slow. AUTO_OBJ helps a lot
228 debug("Adding -DWITH_AUTO_OBJ")
229 bmake_args.append("-DWITH_AUTO_OBJ")
230 if parsed_args.clean is False:
231 bmake_args.append("-DWITHOUT_CLEAN")
232 if (parsed_args.clean is None and not is_make_var_set("NO_CLEAN")
233 and not is_make_var_set("WITHOUT_CLEAN")):
234 # Avoid accidentally deleting all of the build tree and wasting lots of
235 # time cleaning directories instead of just doing a rm -rf ${.OBJDIR}
236 want_clean = input("You did not set -DNO_CLEAN/--clean/--no-clean."
237 " Did you really mean to do a clean build? y/[N] ")
238 if not want_clean.lower().startswith("y"):
239 bmake_args.append("-DNO_CLEAN")
241 env_cmd_str = " ".join(
242 shlex.quote(k + "=" + v) for k, v in new_env_vars.items())
243 make_cmd_str = " ".join(
244 shlex.quote(s) for s in [str(bmake_binary)] + bmake_args)
245 debug("Running `env ", env_cmd_str, " ", make_cmd_str, "`", sep="")
246 os.environ.update(new_env_vars)
247 os.chdir(str(source_root))
248 os.execv(str(bmake_binary), [str(bmake_binary)] + bmake_args)