]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - tools/build/make.py
libarchive: import bugfix from upstream
[FreeBSD/FreeBSD.git] / tools / build / make.py
1 #!/usr/bin/env python3
2 # PYTHON_ARGCOMPLETE_OKAY
3 # -
4 # SPDX-License-Identifier: BSD-2-Clause-FreeBSD
5 #
6 # Copyright (c) 2018 Alex Richardson <arichardson@FreeBSD.org>
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
10 # are met:
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.
16 #
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
27 # SUCH DAMAGE.
28 #
29 # $FreeBSD$
30 #
31
32 # This script makes it easier to build on non-FreeBSD systems by bootstrapping
33 # bmake and inferring required compiler variables.
34 #
35 # On FreeBSD you can use it the same way as just calling make:
36 # `MAKEOBJDIRPREFIX=~/obj ./tools/build/make.py buildworld -DWITH_FOO`
37 #
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
42 # TARGET_ARCH=bar`
43 import argparse
44 import os
45 import shlex
46 import shutil
47 import subprocess
48 import sys
49 from pathlib import Path
50
51
52 def run(cmd, **kwargs):
53     cmd = list(map(str, cmd))  # convert all Path objects to str
54     debug("Running", cmd)
55     subprocess.check_call(cmd, **kwargs)
56
57
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"
63
64     if (bmake_install_dir / "bin/bmake").exists():
65         return bmake_binary
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()
71     global new_env_vars
72     env.update(new_env_vars)
73
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)
80     configure_args = [
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)
86
87     run(["sh", bmake_source_dir / "boot-strap", "op=install"] + configure_args,
88         cwd=str(bmake_build_dir))
89     print("Finished bootstrapping bmake...")
90     return bmake_binary
91
92
93 def debug(*args, **kwargs):
94     global parsed_args
95     if parsed_args.debug:
96         print(*args, **kwargs)
97
98
99 def is_make_var_set(var):
100     return any(
101         x.startswith(var + "=") or x == ("-D" + var) for x in sys.argv[1:])
102
103
104 def check_required_make_env_var(varname, binary_name, bindir):
105     global new_env_vars
106     if os.getenv(varname):
107         return
108     if not bindir:
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 +
115                  " does not exist")
116     new_env_vars[varname] = guess
117     debug("Inferred", varname, "as", guess)
118     global parsed_args
119     if parsed_args.debug:
120         run([guess, "--version"])
121
122 def check_xtool_make_env_var(varname, binary_name):
123     # Avoid calling brew --prefix on macOS if all variables are already set:
124     if os.getenv(varname):
125         return
126     global parsed_args
127     if parsed_args.cross_bindir is None:
128         parsed_args.cross_bindir = default_cross_toolchain()
129     return check_required_make_env_var(varname, binary_name,
130                                        parsed_args.cross_bindir)
131
132 def default_cross_toolchain():
133     # default to homebrew-installed clang on MacOS if available
134     if sys.platform.startswith("darwin"):
135         if shutil.which("brew"):
136             llvm_dir = subprocess.run(["brew", "--prefix", "llvm"],
137                                       capture_output=True).stdout.strip()
138             debug("Inferred LLVM dir as", llvm_dir)
139             try:
140                 if llvm_dir and Path(llvm_dir.decode("utf-8"), "bin").exists():
141                     return str(Path(llvm_dir.decode("utf-8"), "bin"))
142             except OSError:
143                 return None
144     return None
145
146
147 if __name__ == "__main__":
148     parser = argparse.ArgumentParser(
149         formatter_class=argparse.ArgumentDefaultsHelpFormatter)
150     parser.add_argument("--host-bindir",
151                         help="Directory to look for cc/c++/cpp/ld to build "
152                              "host (" + sys.platform + ") binaries",
153                         default="/usr/bin")
154     parser.add_argument("--cross-bindir", default=None,
155                         help="Directory to look for cc/c++/cpp/ld to build "
156                              "target binaries (only needed if XCC/XCPP/XLD "
157                              "are not set)")
158     parser.add_argument("--cross-compiler-type", choices=("clang", "gcc"),
159                         default="clang",
160                         help="Compiler type to find in --cross-bindir (only "
161                              "needed if XCC/XCPP/XLD are not set)"
162                              "Note: using CC is currently highly experimental")
163     parser.add_argument("--host-compiler-type", choices=("cc", "clang", "gcc"),
164                         default="cc",
165                         help="Compiler type to find in --host-bindir (only "
166                              "needed if CC/CPP/CXX are not set). ")
167     parser.add_argument("--debug", action="store_true",
168                         help="Print information on inferred env vars")
169     parser.add_argument("--bootstrap-toolchain", action="store_true",
170                         help="Bootstrap the toolchain instead of using an "
171                              "external one (experimental and not recommended)")
172     parser.add_argument("--clean", action="store_true",
173                         help="Do a clean rebuild instead of building with "
174                              "-DWITHOUT_CLEAN")
175     parser.add_argument("--no-clean", action="store_false", dest="clean",
176                         help="Do a clean rebuild instead of building with "
177                              "-DWITHOUT_CLEAN")
178     try:
179         import argcomplete  # bash completion:
180
181         argcomplete.autocomplete(parser)
182     except ImportError:
183         pass
184     parsed_args, bmake_args = parser.parse_known_args()
185
186     MAKEOBJDIRPREFIX = os.getenv("MAKEOBJDIRPREFIX")
187     if not MAKEOBJDIRPREFIX:
188         sys.exit("MAKEOBJDIRPREFIX is not set, cannot continue!")
189     if not Path(MAKEOBJDIRPREFIX).is_dir():
190         sys.exit(
191             "Chosen MAKEOBJDIRPREFIX=" + MAKEOBJDIRPREFIX + " doesn't exit!")
192     objdir_prefix = Path(MAKEOBJDIRPREFIX).absolute()
193     source_root = Path(__file__).absolute().parent.parent.parent
194
195     new_env_vars = {}
196     if not sys.platform.startswith("freebsd"):
197         if not is_make_var_set("TARGET") or not is_make_var_set("TARGET_ARCH"):
198             if "universe" not in sys.argv and "tinderbox" not in sys.argv:
199                 sys.exit("TARGET= and TARGET_ARCH= must be set explicitly "
200                          "when building on non-FreeBSD")
201     if not parsed_args.bootstrap_toolchain:
202         # infer values for CC/CXX/CPP
203         if parsed_args.host_compiler_type == "gcc":
204             default_cc, default_cxx, default_cpp = ("gcc", "g++", "cpp")
205         # FIXME: this should take values like `clang-9` and then look for
206         # clang-cpp-9, etc. Would alleviate the need to set the bindir on
207         # ubuntu/debian at least.
208         elif parsed_args.host_compiler_type == "clang":
209             default_cc, default_cxx, default_cpp = (
210                 "clang", "clang++", "clang-cpp")
211         else:
212             default_cc, default_cxx, default_cpp = ("cc", "c++", "cpp")
213
214         check_required_make_env_var("CC", default_cc, parsed_args.host_bindir)
215         check_required_make_env_var("CXX", default_cxx,
216                                     parsed_args.host_bindir)
217         check_required_make_env_var("CPP", default_cpp,
218                                     parsed_args.host_bindir)
219         # Using the default value for LD is fine (but not for XLD!)
220
221         # On non-FreeBSD we need to explicitly pass XCC/XLD/X_COMPILER_TYPE
222         use_cross_gcc = parsed_args.cross_compiler_type == "gcc"
223         check_xtool_make_env_var("XCC", "gcc" if use_cross_gcc else "clang")
224         check_xtool_make_env_var("XCXX", "g++" if use_cross_gcc else "clang++")
225         check_xtool_make_env_var("XCPP",
226                                  "cpp" if use_cross_gcc else "clang-cpp")
227         check_xtool_make_env_var("XLD", "ld" if use_cross_gcc else "ld.lld")
228
229         # We also need to set STRIPBIN if there is no working strip binary
230         # in $PATH.
231         if not shutil.which("strip"):
232             if sys.platform.startswith("darwin"):
233                 # On macOS systems we have to use /usr/bin/strip.
234                 sys.exit("Cannot find required tool 'strip'. Please install "
235                          "the host compiler and command line tools.")
236             if parsed_args.host_compiler_type == "clang":
237                 strip_binary = "llvm-strip"
238             else:
239                 strip_binary = "strip"
240             check_required_make_env_var("STRIPBIN", strip_binary,
241                                         parsed_args.host_bindir)
242         if os.getenv("STRIPBIN") or "STRIPBIN" in new_env_vars:
243             # If we are setting STRIPBIN, we have to set XSTRIPBIN to the
244             # default if it is not set otherwise already.
245             if not os.getenv("XSTRIPBIN") and not is_make_var_set("XSTRIPBIN"):
246                 # Use the bootstrapped elftoolchain strip:
247                 new_env_vars["XSTRIPBIN"] = "strip"
248
249     bmake_binary = bootstrap_bmake(source_root, objdir_prefix)
250     # at -j1 cleandir+obj is unbearably slow. AUTO_OBJ helps a lot
251     debug("Adding -DWITH_AUTO_OBJ")
252     bmake_args.append("-DWITH_AUTO_OBJ")
253     if parsed_args.clean is False:
254         bmake_args.append("-DWITHOUT_CLEAN")
255     if (parsed_args.clean is None and not is_make_var_set("NO_CLEAN")
256             and not is_make_var_set("WITHOUT_CLEAN")):
257         # Avoid accidentally deleting all of the build tree and wasting lots of
258         # time cleaning directories instead of just doing a rm -rf ${.OBJDIR}
259         want_clean = input("You did not set -DWITHOUT_CLEAN/--clean/--no-clean."
260                            " Did you really mean to do a clean build? y/[N] ")
261         if not want_clean.lower().startswith("y"):
262             bmake_args.append("-DWITHOUT_CLEAN")
263
264     env_cmd_str = " ".join(
265         shlex.quote(k + "=" + v) for k, v in new_env_vars.items())
266     make_cmd_str = " ".join(
267         shlex.quote(s) for s in [str(bmake_binary)] + bmake_args)
268     debug("Running `env ", env_cmd_str, " ", make_cmd_str, "`", sep="")
269     os.environ.update(new_env_vars)
270     os.chdir(str(source_root))
271     os.execv(str(bmake_binary), [str(bmake_binary)] + bmake_args)