Skip to content

Commit dc76a0f

Browse files
committed
A whole new builder.
Rewritten in python. Only replace `build.sh` (but can easily replace scons runners and `build-release.sh`). See `./cli.py -h` for help.
1 parent 162cced commit dc76a0f

File tree

8 files changed

+665
-0
lines changed

8 files changed

+665
-0
lines changed

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,10 @@ releases/
1515
sha512sums/
1616
tmp/
1717
web/
18+
19+
# Editors
20+
*.swp
21+
22+
# Python
23+
*.pyc
24+
__pycache__/

builder/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .builder import PodmanRunner, GitRunner
2+
from .images import configs as ImageConfigs
3+
from .config import Config, write_config, load_config
4+
5+
BuildConfigs = [] # TODO

builder/builder.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/python
2+
3+
import logging
4+
import os
5+
import subprocess
6+
import sys
7+
8+
from .config import Config
9+
from .runner import RunError, Runner, run_simple
10+
11+
12+
def which(what):
13+
return run_simple(["which", what]).stdout.strip()
14+
15+
16+
def ensure_dir(dirname):
17+
os.makedirs(dirname, exist_ok=True)
18+
19+
20+
class PodmanRunner(Runner):
21+
IMAGES = ["mono-glue", "windows", "ubuntu-64", "ubuntu-32", "javascript"]
22+
IMAGES_PRIVATE = ["macosx", "android", "ios", "uwp"]
23+
24+
@staticmethod
25+
def get_images():
26+
return PodmanRunner.IMAGES + PodmanRunner.IMAGES_PRIVATE
27+
28+
def get_image_path(self, image, version="latest", local=False):
29+
if local:
30+
return f"localhost/{image}:{version}"
31+
path = Config.private_path if image in PodmanRunner.IMAGES_PRIVATE else Config.public_path
32+
return f"{Config.registry}/{path}/{image}:{version}"
33+
34+
def __init__(self, base_dir, dry_run=False):
35+
self.base_dir = base_dir
36+
self.dry_run = dry_run
37+
self.logged_in = False
38+
self._podman = self._detect_podman()
39+
40+
def _detect_podman(self):
41+
podman = which("podman")
42+
if not podman:
43+
podman = which("docker")
44+
if not podman:
45+
print("Either podman or docker needs to be installed")
46+
sys.exit(1)
47+
return podman
48+
49+
def login(self):
50+
if Config.username == "" or Config.password == "":
51+
logging.debug("Skipping login, missing username or password")
52+
return
53+
self.logged_in = run_simple(self._podman, "login", Config.regitry, "-u", Config.username, "-p", Config.password).returncode == 0
54+
55+
def image_exists(self, image):
56+
return run_simple([self._podman, "image", "exists", image]).returncode == 0
57+
58+
def fetch_image(self, image, force=False):
59+
exists = not force and self.image_exists(image)
60+
if not exists:
61+
self.run([self._podman, "pull", "%s/%s" % (Config.registry, image)])
62+
63+
def fetch_images(self, images=[], **kwargs):
64+
if len(images) == 0:
65+
images = PodmanRunner.get_images()
66+
for image in images:
67+
if image in PodmanRunner.IMAGES:
68+
self.fetch_image("%s/%s" % (Config.public_path, image), **kwargs)
69+
elif image in PodmanRunner.IMAGES_PRIVATE:
70+
if not self.logged_in:
71+
print("Can't fetch image: %s. Not logged in" % image)
72+
continue
73+
self.fetch_image("%s/%s" % (Config.private_path, image), **kwargs)
74+
75+
def podrun(self, run_config, classical=False, mono=False, local=False, interactive=False, **kwargs):
76+
def env(env_vars):
77+
for k, v in env_vars.items():
78+
yield("--env")
79+
yield(f"{k}={v}")
80+
81+
def mount(mount_points):
82+
for k, v in mount_points.items():
83+
yield("-v")
84+
yield(f"{self.base_dir}/{k}:/root/{v}")
85+
86+
for d in run_config.dirs:
87+
ensure_dir(os.path.join(self.base_dir, d))
88+
89+
cores = os.environ.get('NUM_CORES', os.cpu_count())
90+
cmd = [self._podman, "run", "--rm", "-w", "/root/"]
91+
cmd += env({
92+
"BUILD_NAME": os.environ.get("BUILD_NAME", "custom_build"),
93+
"NUM_CORES": os.environ.get("NUM_CORES", os.cpu_count()),
94+
"CLASSICAL": 1 if classical else 0,
95+
"MONO": 1 if mono else 0,
96+
})
97+
cmd += mount({
98+
"mono-glue": "mono-glue",
99+
"godot.tar.gz": "godot.tar.gz",
100+
})
101+
cmd += mount(run_config.mounts)
102+
if run_config.out_dir is not None:
103+
out_dir = f"out/{run_config.out_dir}"
104+
ensure_dir(f"{self.base_dir}/{out_dir}")
105+
cmd += mount({
106+
out_dir: "out"
107+
})
108+
cmd += run_config.extra_opts
109+
110+
image_path = self.get_image_path(run_config.image, version=run_config.image_version, local=local)
111+
if interactive:
112+
if self.dry_run:
113+
print(" ".join(cmd + ["-it", image_path, "bash"]))
114+
return
115+
return subprocess.run(cmd + ["-it", image_path, "bash"])
116+
117+
cmd += [image_path] + run_config.cmd
118+
if run_config.log and not 'log' in kwargs:
119+
ensure_dir(f"{self.base_dir}/out/logs")
120+
with open(os.path.join(self.base_dir, "out", "logs", run_config.log), "w") as log:
121+
return self.run(cmd, log=log, **kwargs)
122+
else:
123+
return self.run(cmd, **kwargs)
124+
125+
126+
class GitRunner(Runner):
127+
128+
def __init__(self, base_dir, dry_run=False):
129+
self.dry_run = dry_run
130+
self.base_dir = base_dir
131+
132+
def git(self, *args, can_fail=False):
133+
return self.run(["git"] + list(args), can_fail)
134+
135+
def check_version(self, godot_version):
136+
if self.dry_run:
137+
print("Skipping version check in dry run mode (would likely fail)")
138+
return
139+
import importlib.util
140+
version_file = os.path.join("git", "version.py")
141+
spec = importlib.util.spec_from_file_location("version", version_file)
142+
version = importlib.util.module_from_spec(spec)
143+
spec.loader.exec_module(version)
144+
if hasattr(version, "patch"):
145+
version_string = f"{version.major}.{version.minor}.{version.patch}-{version.status}"
146+
else:
147+
version_string = f"{version.major}.{version.minor}-{version.status}"
148+
ok = version_string == godot_version
149+
if not ok:
150+
print(f"Version mismatch, expected: {godot_version}, got: {version_string}")
151+
sys.exit(1)
152+
153+
def checkout(self, ref):
154+
repo = "https://github.com/godotengine/godot"
155+
dest = os.path.join(self.base_dir, "git")
156+
self.git("clone", dest, can_fail=True)
157+
self.git("-C", dest, "fetch", "--all")
158+
self.git("-C", dest, "checkout", "--detach", ref)
159+
160+
def tgz(self, version, ref="HEAD"):
161+
source = os.path.join(self.base_dir, "git")
162+
dest = os.path.join(self.base_dir, "godot.tar.gz")
163+
self.git("-C", source, "archive", f"--prefix=godot-{version}/", "-o", dest, ref)

builder/cli.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import logging, os, sys
2+
from argparse import ArgumentParser
3+
4+
from . import Config, ImageConfigs, GitRunner, PodmanRunner, write_config, load_config
5+
6+
class ConfigCLI:
7+
ACTION = "config"
8+
HELP = "Print or save config file"
9+
10+
@staticmethod
11+
def execute(base_dir, args):
12+
write_config(sys.stdout)
13+
sys.stdout.write('\n')
14+
if args.save is not None:
15+
path = args.save if os.path.isabs(args.save) else os.path.join(base_dir, args.save)
16+
if not path.endswith(".json"):
17+
print("Invalid config file: %s, must be '.json'" % args.save)
18+
sys.exit(1)
19+
with open(path, 'w') as w:
20+
write_config(w)
21+
print("Saved to file: %s" % path)
22+
23+
@staticmethod
24+
def bind(parser):
25+
parser.add_argument("-s", "--save")
26+
27+
class ImageCLI:
28+
ACTION = "fetch"
29+
HELP = "Fetch remote build containers"
30+
31+
@staticmethod
32+
def execute(base_dir, args):
33+
podman = PodmanRunner(
34+
base_dir,
35+
dry_run=args.dry_run
36+
)
37+
podman.login()
38+
podman.fetch_images(
39+
images = args.image,
40+
force=args.force_download
41+
)
42+
43+
@staticmethod
44+
def bind(parser):
45+
parser.add_argument("-f", "--force-download", action="store_true")
46+
parser.add_argument("-i", "--image", action="append", default=[], help="The image to fetch, all by default. Possible values: %s" % ", ".join(PodmanRunner.get_images()))
47+
48+
49+
class GitCLI:
50+
ACTION = "checkout"
51+
HELP = "git checkout, version check, tar"
52+
53+
@staticmethod
54+
def execute(base_dir, args):
55+
git = GitRunner(base_dir, dry_run=args.dry_run)
56+
if not args.skip_checkout:
57+
git.checkout(args.treeish)
58+
if not args.skip_check:
59+
git.check_version(args.godot_version)
60+
if not args.skip_tar:
61+
git.tgz(args.godot_version)
62+
63+
@staticmethod
64+
def bind(parser):
65+
parser.add_argument("treeish", help="git treeish, possibly a git ref, or commit hash.", default="origin/master")
66+
parser.add_argument("godot_version", help="godot version (e.g. 3.1-alpha5)")
67+
parser.add_argument("-c", "--skip-checkout", action="store_true")
68+
parser.add_argument("-t", "--skip-tar", action="store_true")
69+
parser.add_argument("--skip-check", action="store_true")
70+
71+
72+
class RunCLI:
73+
ACTION = "run"
74+
HELP = "Run the desired containers"
75+
76+
CONTAINERS = [cls.__name__.replace("Config", "") for cls in ImageConfigs]
77+
78+
@staticmethod
79+
def execute(base_dir, args):
80+
podman = PodmanRunner(base_dir, dry_run=args.dry_run)
81+
build_mono = args.build == "all" or args.build == "mono"
82+
build_classical = args.build == "all" or args.build == "classical"
83+
if len(args.container) == 0:
84+
args.container = RunCLI.CONTAINERS
85+
to_build = [ImageConfigs[RunCLI.CONTAINERS.index(c)] for c in args.container]
86+
for b in to_build:
87+
podman.podrun(b, classical=build_classical, mono=build_mono, local=not args.remote, interactive=args.interactive)
88+
89+
def bind(parser):
90+
parser.add_argument("-b", "--build", choices=["all", "classical", "mono"], default="all")
91+
parser.add_argument("-k", "--container", action="append", default=[], help="The containers to build, one of %s" % RunCLI.CONTAINERS)
92+
parser.add_argument("-r", "--remote", help="Run with remote containers", action="store_true")
93+
parser.add_argument("-i", "--interactive", action="store_true", help="Enter an interactive shell inside the container instead of running the default command")
94+
95+
96+
class ReleaseCLI:
97+
ACTION = "release"
98+
HELP = "Make a full release cycle, git checkout, reset, version check, tar, build all"
99+
100+
@staticmethod
101+
def execute(base_dir, args):
102+
git = GitRunner(base_dir, dry_run=args.dry_run)
103+
podman = PodmanRunner(base_dir, dry_run=args.dry_run)
104+
build_mono = args.build == "all" or args.build == "mono"
105+
build_classical = args.build == "all" or args.build == "classical"
106+
if not args.localhost and not args.skip_download:
107+
podman.login()
108+
podman.fetch_images(
109+
force=args.force_download
110+
)
111+
if not args.skip_git:
112+
git.checkout(args.git)
113+
git.check_version(args.godot_version)
114+
git.tgz(args.godot_version)
115+
116+
for b in ImageConfigs:
117+
podman.podrun(b, classical=build_classical, mono=build_mono, local=args.localhost)
118+
119+
@staticmethod
120+
def bind(parser):
121+
parser.add_argument("godot_version", help="godot version (e.g. 3.1-alpha5)")
122+
parser.add_argument("-b", "--build", choices=["all", "classical", "mono"], default="all")
123+
parser.add_argument("-s", "--skip-download", action="store_true")
124+
parser.add_argument("-c", "--skip-git", action="store_true")
125+
parser.add_argument("-g", "--git", help="git treeish, possibly a git ref, or commit hash.", default="origin/master")
126+
parser.add_argument("-f", "--force-download", action="store_true")
127+
parser.add_argument("-l", "--localhost", action="store_true")
128+
129+
130+
class CLI:
131+
OPTS = [(v, getattr(Config, v)) for v in dir(Config) if not v.startswith("_")]
132+
133+
def add_command(self, cli):
134+
parser = self.subparsers.add_parser(cli.ACTION, help=cli.HELP)
135+
parser.add_argument("-n", "--dry-run", action="store_true")
136+
parser.set_defaults(action_func=cli.execute)
137+
cli.bind(parser)
138+
139+
def __init__(self, base_dir):
140+
self.base_dir = base_dir
141+
self.parser = ArgumentParser()
142+
for k,v in CLI.OPTS:
143+
self.parser.add_argument("--%s" % k)
144+
self.parser.add_argument("-c", "--config", help="Configuration override")
145+
self.subparsers = self.parser.add_subparsers(dest="action", help="The requested action", required=True)
146+
self.add_command(ConfigCLI)
147+
self.add_command(GitCLI)
148+
self.add_command(ImageCLI)
149+
self.add_command(RunCLI)
150+
self.add_command(ReleaseCLI)
151+
152+
def execute(self):
153+
args = self.parser.parse_args()
154+
if args.config is not None:
155+
path = args.config if os.path.isabs(args.config) else os.path.join(self.base_dir, args.config)
156+
if not os.path.isfile(path):
157+
print("Invalid config file: %s" % path)
158+
sys.exit(1)
159+
load_config(path)
160+
for k,v in CLI.OPTS:
161+
override = getattr(args, k)
162+
if override is not None:
163+
setattr(Config, k, override)
164+
args.action_func(self.base_dir, args)
165+
166+
167+
def main(loglevel=logging.DEBUG):
168+
logging.basicConfig(level=loglevel)
169+
CLI(os.getcwd()).execute()
170+
171+
if __name__ == "__main__":
172+
main()

0 commit comments

Comments
 (0)