1#!/usr/bin/env python3
2# This Source Code Form is subject to the terms of the Mozilla Public
3# License, v. 2.0. If a copy of the MPL was not distributed with this
4# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
6import os
7import subprocess
8import json
9import argparse
10import sys
11import shutil
12from functools import reduce
13
14
15def check_run(args, path):
16    print(" ".join(args) + " in " + path, file=sys.stderr)
17    subprocess.run(args, cwd=path, check=True)
18
19
20def run_in(path, args, extra_env=None):
21    """
22    Runs the given commands in the directory specified by <path>.
23    """
24    env = dict(os.environ)
25    env.update(extra_env or {})
26    check_run(args, path)
27    subprocess.run(args, cwd=path)
28
29
30def build_tar_package(tar, name, base, directories):
31    name = os.path.realpath(name)
32    run_in(
33        base,
34        [tar, "-c", "-%s" % ("J" if ".xz" in name else "j"), "-f", name] + directories,
35    )
36
37
38def is_git_repo(dir):
39    """Check whether the given directory is a git repository."""
40    from subprocess import CalledProcessError
41
42    try:
43        check_run(["git", "rev-parse"], dir)
44        return True
45    except CalledProcessError:
46        return False
47
48
49def git_clone(main_dir, url, clone_dir, commit):
50    """
51    Clones the repository from <url> into <clone_dir>, and brings the
52    repository to the state of <commit>.
53    """
54    run_in(main_dir, ["git", "clone", url, clone_dir])
55    run_in(clone_dir, ["git", "checkout", commit])
56
57
58if __name__ == "__main__":
59    parser = argparse.ArgumentParser()
60    parser.add_argument(
61        "-c",
62        "--config",
63        required=True,
64        type=argparse.FileType("r"),
65        help="Infer configuration file",
66    )
67    parser.add_argument(
68        "-b", "--base-dir", help="Base directory for code and build artifacts"
69    )
70    parser.add_argument(
71        "--clean", action="store_true", help="Clean the build directory"
72    )
73    parser.add_argument(
74        "--skip-tar", action="store_true", help="Skip tar packaging stage"
75    )
76
77    args = parser.parse_args()
78
79    # The directories end up in the debug info, so the easy way of getting
80    # a reproducible build is to run it in a know absolute directory.
81    # We use a directory that is registered as a volume in the Docker image.
82    if args.base_dir:
83        base_dir = args.base_dir
84    else:
85        base_dir = reduce(
86            os.path.join, [os.sep + "builds", "worker", "workspace", "moz-toolchain"]
87        )
88    infer_dir = os.path.join(base_dir, "infer")
89    source_dir = os.path.join(infer_dir, "src")
90    build_dir = os.path.join(infer_dir, "build")
91
92    if args.clean:
93        shutil.rmtree(build_dir)
94        os.sys.exit(0)
95
96    config = json.load(args.config)
97    infer_revision = config["infer_revision"]
98    infer_repo = config["infer_repo"]
99
100    for folder in [infer_dir, source_dir, build_dir]:
101        os.makedirs(folder, exist_ok=True)
102
103    # clone infer
104    if not is_git_repo(source_dir):
105        # git doesn't like cloning into a non-empty folder. If src is not a git
106        # repo then just remove it in order to reclone
107        shutil.rmtree(source_dir)
108        git_clone(infer_dir, infer_repo, source_dir, infer_revision)
109    # apply a few patches
110    dir_path = os.path.dirname(os.path.realpath(__file__))
111    # clean the git directory by reseting all changes
112    git_commands = [["clean", "-f"], ["reset", "--hard"]]
113    for command in git_commands:
114        run_in(source_dir, ["git"] + command)
115    for p in config.get("patches", []):
116        run_in(source_dir, ["git", "apply", os.path.join(dir_path, p)])
117    # configure opam
118    run_in(source_dir, ["opam", "init", "--no-setup", "--disable-sandboxing"])
119    # build infer
120    run_in(source_dir, ["./build-infer.sh", "java"], extra_env={"NO_CMAKE_STRIP": "1"})
121
122    package_name = "infer"
123    infer_package = os.path.join(os.getcwd(), package_name)
124    # We need to create a package with all of the depended libraries injected in it
125    run_in(
126        source_dir,
127        [
128            "make",
129            "install-with-libs",
130            "BUILD_MODE=opt",
131            "PATCHELF=patchelf",
132            "DESTDIR={}".format(infer_package),
133            "libdir_relative_to_bindir=../lib",
134        ],
135    )
136
137    infer_package_with_pref = os.path.join(infer_package, "usr")
138    if not args.skip_tar:
139        os.rename(
140            os.path.join(infer_package_with_pref, "local"),
141            os.path.join(infer_package_with_pref, "infer"),
142        )
143        build_tar_package(
144            "tar",
145            "%s.tar.xz" % (package_name),
146            infer_package_with_pref,
147            [
148                os.path.join("infer", "bin"),
149                os.path.join("infer", "lib"),
150                os.path.join("infer", "share"),
151            ],
152        )
153