Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/freebsd-src
Path: blob/main/tools/build/make.py
39475 views
1
#!/usr/bin/env python3
2
# PYTHON_ARGCOMPLETE_OKAY
3
# -
4
# SPDX-License-Identifier: BSD-2-Clause
5
#
6
# Copyright (c) 2018 Alex Richardson <[email protected]>
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
#
30
31
# This script makes it easier to build on non-FreeBSD systems by bootstrapping
32
# bmake and inferring required compiler variables.
33
#
34
# On FreeBSD you can use it the same way as just calling make:
35
# `MAKEOBJDIRPREFIX=~/obj ./tools/build/make.py buildworld -DWITH_FOO`
36
#
37
# On Linux and MacOS you may need to explicitly indicate the cross toolchain
38
# to use. You can do this by:
39
# - setting XCC/XCXX/XLD/XCPP to the paths of each tool
40
# - using --cross-bindir to specify the path to the cross-compiler bindir:
41
# `MAKEOBJDIRPREFIX=~/obj ./tools/build/make.py
42
# --cross-bindir=/path/to/cross/compiler buildworld -DWITH_FOO TARGET=foo
43
# TARGET_ARCH=bar`
44
# - using --cross-toolchain to specify the package containing the cross-compiler
45
# (MacOS only currently):
46
# `MAKEOBJDIRPREFIX=~/obj ./tools/build/make.py
47
# --cross-toolchain=llvm@NN buildworld -DWITH_FOO TARGET=foo
48
# TARGET_ARCH=bar`
49
#
50
# On MacOS, this tool will search for an llvm toolchain installed via brew and
51
# use it as the cross toolchain if an explicit toolchain is not specified.
52
53
import argparse
54
import functools
55
import os
56
import shlex
57
import shutil
58
import subprocess
59
import sys
60
from pathlib import Path
61
62
63
# List of targets that are independent of TARGET/TARGET_ARCH and thus do not
64
# need them to be set. Keep in the same order as Makefile documents them (if
65
# they are documented).
66
mach_indep_targets = [
67
"cleanuniverse",
68
"universe",
69
"universe-toolchain",
70
"tinderbox",
71
"worlds",
72
"kernels",
73
"kernel-toolchains",
74
"targets",
75
"toolchains",
76
"makeman",
77
"sysent",
78
]
79
80
81
def run(cmd, **kwargs):
82
cmd = list(map(str, cmd)) # convert all Path objects to str
83
debug("Running", cmd)
84
subprocess.check_call(cmd, **kwargs)
85
86
87
# Always bootstraps in order to control bmake's config to ensure compatibility
88
def bootstrap_bmake(source_root, objdir_prefix):
89
bmake_source_dir = source_root / "contrib/bmake"
90
bmake_build_dir = objdir_prefix / "bmake-build"
91
bmake_install_dir = objdir_prefix / "bmake-install"
92
bmake_binary = bmake_install_dir / "bin/bmake"
93
bmake_config = bmake_install_dir / ".make-py-config"
94
95
bmake_source_version = subprocess.run([
96
"sh", "-c", ". \"$0\"/VERSION; echo $_MAKE_VERSION", bmake_source_dir],
97
stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip()
98
try:
99
bmake_source_version = int(bmake_source_version)
100
except ValueError:
101
sys.exit("Invalid source bmake version '" + bmake_source_version + "'")
102
103
bmake_installed_version = 0
104
if bmake_binary.exists():
105
bmake_installed_version = subprocess.run([
106
bmake_binary, "-r", "-f", "/dev/null", "-V", "MAKE_VERSION"],
107
stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout.strip()
108
try:
109
bmake_installed_version = int(bmake_installed_version.strip())
110
except ValueError:
111
print("Invalid installed bmake version '" +
112
bmake_installed_version + "', treating as not present")
113
114
configure_args = [
115
"--with-default-sys-path=.../share/mk:" +
116
str(bmake_install_dir / "share/mk"),
117
"--with-machine=amd64", # TODO? "--with-machine-arch=amd64",
118
"--without-filemon", "--prefix=" + str(bmake_install_dir)]
119
120
configure_args_str = ' '.join([shlex.quote(x) for x in configure_args])
121
if bmake_config.exists():
122
last_configure_args_str = bmake_config.read_text()
123
else:
124
last_configure_args_str = ""
125
126
debug("Source bmake version: " + str(bmake_source_version))
127
debug("Installed bmake version: " + str(bmake_installed_version))
128
debug("Configure args: " + configure_args_str)
129
debug("Last configure args: " + last_configure_args_str)
130
131
if bmake_installed_version == bmake_source_version and \
132
configure_args_str == last_configure_args_str:
133
return bmake_binary
134
135
print("Bootstrapping bmake...")
136
if bmake_build_dir.exists():
137
shutil.rmtree(str(bmake_build_dir))
138
if bmake_install_dir.exists():
139
shutil.rmtree(str(bmake_install_dir))
140
141
os.makedirs(str(bmake_build_dir))
142
143
env = os.environ.copy()
144
global new_env_vars
145
env.update(new_env_vars)
146
147
run(["sh", bmake_source_dir / "boot-strap"] + configure_args,
148
cwd=str(bmake_build_dir), env=env)
149
run(["sh", bmake_source_dir / "boot-strap", "op=install"] + configure_args,
150
cwd=str(bmake_build_dir))
151
bmake_config.write_text(configure_args_str)
152
153
print("Finished bootstrapping bmake...")
154
return bmake_binary
155
156
157
def debug(*args, **kwargs):
158
global parsed_args
159
if parsed_args.debug:
160
print(*args, **kwargs)
161
162
163
def is_make_var_set(var):
164
return any(
165
x.startswith(var + "=") or x == ("-D" + var) for x in sys.argv[1:])
166
167
168
def check_required_make_env_var(varname, binary_name, bindir):
169
global new_env_vars
170
if os.getenv(varname):
171
return
172
if not bindir:
173
sys.exit("Could not infer value for $" + varname + ". Either set $" +
174
varname + " or pass --cross-bindir=/cross/compiler/dir/bin" +
175
" or --cross-toolchain=<package>")
176
# try to infer the path to the tool
177
guess = os.path.join(bindir, binary_name)
178
if not os.path.isfile(guess):
179
sys.exit("Could not infer value for $" + varname + ": " + guess +
180
" does not exist")
181
new_env_vars[varname] = guess
182
debug("Inferred", varname, "as", guess)
183
global parsed_args
184
if parsed_args.debug:
185
run([guess, "--version"])
186
187
188
def check_xtool_make_env_var(varname, binary_name):
189
# Avoid calling brew --prefix on macOS if all variables are already set:
190
if os.getenv(varname):
191
return
192
global parsed_args
193
if parsed_args.cross_bindir is None:
194
cross_bindir = cross_toolchain_bindir(binary_name,
195
parsed_args.cross_toolchain)
196
else:
197
cross_bindir = parsed_args.cross_bindir
198
return check_required_make_env_var(varname, binary_name,
199
cross_bindir)
200
201
202
@functools.cache
203
def brew_prefix(package: str) -> str:
204
path = subprocess.run(["brew", "--prefix", package], stdout=subprocess.PIPE,
205
stderr=subprocess.PIPE).stdout.strip()
206
debug("Inferred", package, "dir as", path)
207
return path.decode("utf-8")
208
209
def binary_path(bindir: str, binary_name: str) -> "Optional[str]":
210
try:
211
if bindir and Path(bindir, "bin", binary_name).exists():
212
return str(Path(bindir, "bin"))
213
except OSError:
214
pass
215
return None
216
217
def cross_toolchain_bindir(binary_name: str, package: "Optional[str]") -> str:
218
# default to homebrew-installed clang on MacOS if available
219
if sys.platform.startswith("darwin"):
220
if shutil.which("brew"):
221
if not package:
222
package = "llvm"
223
bindir = binary_path(brew_prefix(package), binary_name)
224
if bindir:
225
return bindir
226
227
# brew installs lld as a separate package for LLVM 19 and later
228
if binary_name == "ld.lld":
229
lld_package = package.replace("llvm", "lld")
230
bindir = binary_path(brew_prefix(lld_package), binary_name)
231
if bindir:
232
return bindir
233
return None
234
235
236
if __name__ == "__main__":
237
parser = argparse.ArgumentParser(
238
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
239
parser.add_argument("--host-bindir",
240
help="Directory to look for cc/c++/cpp/ld to build "
241
"host (" + sys.platform + ") binaries",
242
default="/usr/bin")
243
parser.add_argument("--cross-bindir", default=None,
244
help="Directory to look for cc/c++/cpp/ld to build "
245
"target binaries (only needed if XCC/XCPP/XLD "
246
"are not set)")
247
parser.add_argument("--cross-compiler-type", choices=("clang", "gcc"),
248
default="clang",
249
help="Compiler type to find in --cross-bindir (only "
250
"needed if XCC/XCPP/XLD are not set)"
251
"Note: using CC is currently highly experimental")
252
parser.add_argument("--cross-toolchain", default=None,
253
help="Name of package containing cc/c++/cpp/ld to build "
254
"target binaries (only needed if XCC/XCPP/XLD "
255
"are not set)")
256
parser.add_argument("--host-compiler-type", choices=("cc", "clang", "gcc"),
257
default="cc",
258
help="Compiler type to find in --host-bindir (only "
259
"needed if CC/CPP/CXX are not set). ")
260
parser.add_argument("--debug", action="store_true",
261
help="Print information on inferred env vars")
262
parser.add_argument("--bootstrap-toolchain", action="store_true",
263
help="Bootstrap the toolchain instead of using an "
264
"external one (experimental and not recommended)")
265
parser.add_argument("--clean", action="store_true",
266
help="Do a clean rebuild instead of building with "
267
"-DWITHOUT_CLEAN")
268
parser.add_argument("--no-clean", action="store_false", dest="clean",
269
help="Do a clean rebuild instead of building with "
270
"-DWITHOUT_CLEAN")
271
try:
272
import argcomplete # bash completion:
273
274
argcomplete.autocomplete(parser)
275
except ImportError:
276
pass
277
parsed_args, bmake_args = parser.parse_known_args()
278
279
MAKEOBJDIRPREFIX = os.getenv("MAKEOBJDIRPREFIX")
280
if not MAKEOBJDIRPREFIX:
281
sys.exit("MAKEOBJDIRPREFIX is not set, cannot continue!")
282
if not Path(MAKEOBJDIRPREFIX).is_dir():
283
sys.exit(
284
"Chosen MAKEOBJDIRPREFIX=" + MAKEOBJDIRPREFIX + " doesn't exist!")
285
objdir_prefix = Path(MAKEOBJDIRPREFIX).absolute()
286
source_root = Path(__file__).absolute().parent.parent.parent
287
288
new_env_vars = {}
289
if not sys.platform.startswith("freebsd"):
290
if not is_make_var_set("TARGET") or not is_make_var_set("TARGET_ARCH"):
291
if not set(sys.argv).intersection(set(mach_indep_targets)):
292
sys.exit("TARGET= and TARGET_ARCH= must be set explicitly "
293
"when building on non-FreeBSD")
294
if not parsed_args.bootstrap_toolchain:
295
# infer values for CC/CXX/CPP
296
if parsed_args.host_compiler_type == "gcc":
297
default_cc, default_cxx, default_cpp = ("gcc", "g++", "cpp")
298
# FIXME: this should take values like `clang-9` and then look for
299
# clang-cpp-9, etc. Would alleviate the need to set the bindir on
300
# ubuntu/debian at least.
301
elif parsed_args.host_compiler_type == "clang":
302
default_cc, default_cxx, default_cpp = (
303
"clang", "clang++", "clang-cpp")
304
else:
305
default_cc, default_cxx, default_cpp = ("cc", "c++", "cpp")
306
307
check_required_make_env_var("CC", default_cc, parsed_args.host_bindir)
308
check_required_make_env_var("CXX", default_cxx,
309
parsed_args.host_bindir)
310
check_required_make_env_var("CPP", default_cpp,
311
parsed_args.host_bindir)
312
# Using the default value for LD is fine (but not for XLD!)
313
314
# On non-FreeBSD we need to explicitly pass XCC/XLD/X_COMPILER_TYPE
315
use_cross_gcc = parsed_args.cross_compiler_type == "gcc"
316
check_xtool_make_env_var("XCC", "gcc" if use_cross_gcc else "clang")
317
check_xtool_make_env_var("XCXX", "g++" if use_cross_gcc else "clang++")
318
check_xtool_make_env_var("XCPP",
319
"cpp" if use_cross_gcc else "clang-cpp")
320
check_xtool_make_env_var("XLD", "ld" if use_cross_gcc else "ld.lld")
321
322
# We also need to set STRIPBIN if there is no working strip binary
323
# in $PATH.
324
if not shutil.which("strip"):
325
if sys.platform.startswith("darwin"):
326
# On macOS systems we have to use /usr/bin/strip.
327
sys.exit("Cannot find required tool 'strip'. Please install "
328
"the host compiler and command line tools.")
329
if parsed_args.host_compiler_type == "clang":
330
strip_binary = "llvm-strip"
331
else:
332
strip_binary = "strip"
333
check_required_make_env_var("STRIPBIN", strip_binary,
334
parsed_args.host_bindir)
335
if os.getenv("STRIPBIN") or "STRIPBIN" in new_env_vars:
336
# If we are setting STRIPBIN, we have to set XSTRIPBIN to the
337
# default if it is not set otherwise already.
338
if not os.getenv("XSTRIPBIN") and not is_make_var_set("XSTRIPBIN"):
339
# Use the bootstrapped elftoolchain strip:
340
new_env_vars["XSTRIPBIN"] = "strip"
341
342
bmake_binary = bootstrap_bmake(source_root, objdir_prefix)
343
# at -j1 cleandir+obj is unbearably slow. AUTO_OBJ helps a lot
344
debug("Adding -DWITH_AUTO_OBJ")
345
bmake_args.append("-DWITH_AUTO_OBJ")
346
if parsed_args.clean is False:
347
bmake_args.append("-DWITHOUT_CLEAN")
348
if (parsed_args.clean is None and not is_make_var_set("NO_CLEAN")
349
and not is_make_var_set("WITHOUT_CLEAN")):
350
# Avoid accidentally deleting all of the build tree and wasting lots of
351
# time cleaning directories instead of just doing a rm -rf ${.OBJDIR}
352
want_clean = input("You did not set -DWITHOUT_CLEAN/--(no-)clean."
353
" Did you really mean to do a clean build? y/[N] ")
354
if not want_clean.lower().startswith("y"):
355
bmake_args.append("-DWITHOUT_CLEAN")
356
357
env_cmd_str = " ".join(
358
shlex.quote(k + "=" + v) for k, v in new_env_vars.items())
359
make_cmd_str = " ".join(
360
shlex.quote(s) for s in [str(bmake_binary)] + bmake_args)
361
debug("Running `env ", env_cmd_str, " ", make_cmd_str, "`", sep="")
362
os.environ.update(new_env_vars)
363
364
# Fedora defines bash function wrapper for some shell commands and this
365
# makes 'which <command>' return the function's source code instead of
366
# the binary path. Undefine it to restore the original behavior.
367
os.unsetenv("BASH_FUNC_which%%")
368
os.unsetenv("BASH_FUNC_ml%%")
369
os.unsetenv("BASH_FUNC_module%%")
370
371
os.chdir(str(source_root))
372
os.execv(str(bmake_binary), [str(bmake_binary)] + bmake_args)
373
374