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