]>
Commit | Line | Data |
---|---|---|
1e59de90 TL |
1 | #!/usr/bin/python3 |
2 | """Build cephadm from one or more files into a standalone executable. | |
3 | """ | |
4 | # TODO: If cephadm is being built and packaged within a format such as RPM | |
5 | # do we have to do anything special wrt passing in the version | |
6 | # of python to build with? Even with the intermediate cmake layer? | |
7 | ||
8 | import argparse | |
9 | import compileall | |
10 | import logging | |
11 | import os | |
12 | import pathlib | |
13 | import shutil | |
14 | import subprocess | |
15 | import tempfile | |
16 | import sys | |
17 | ||
18 | HAS_ZIPAPP = False | |
19 | try: | |
20 | import zipapp | |
21 | ||
22 | HAS_ZIPAPP = True | |
23 | except ImportError: | |
24 | pass | |
25 | ||
26 | ||
27 | log = logging.getLogger(__name__) | |
28 | ||
29 | ||
aee94f69 TL |
30 | _VALID_VERS_VARS = [ |
31 | "CEPH_GIT_VER", | |
32 | "CEPH_GIT_NICE_VER", | |
33 | "CEPH_RELEASE", | |
34 | "CEPH_RELEASE_NAME", | |
35 | "CEPH_RELEASE_TYPE", | |
36 | ] | |
37 | ||
38 | ||
1e59de90 TL |
39 | def _reexec(python): |
40 | """Switch to the selected version of python by exec'ing into the desired | |
41 | python path. | |
42 | Sets the _BUILD_PYTHON_SET env variable as a sentinel to indicate exec has | |
43 | been performed. | |
44 | """ | |
45 | env = os.environ.copy() | |
46 | env["_BUILD_PYTHON_SET"] = python | |
47 | os.execvpe(python, [python, __file__] + sys.argv[1:], env) | |
48 | ||
49 | ||
50 | def _did_rexec(): | |
51 | """Returns true if the process has already exec'ed into the desired python | |
52 | version. | |
53 | """ | |
54 | return bool(os.environ.get("_BUILD_PYTHON_SET", "")) | |
55 | ||
56 | ||
aee94f69 | 57 | def _build(dest, src, versioning_vars=None): |
1e59de90 TL |
58 | """Build the binary.""" |
59 | os.chdir(src) | |
60 | tempdir = pathlib.Path(tempfile.mkdtemp(suffix=".cephadm.build")) | |
61 | log.debug("working in %s", tempdir) | |
62 | try: | |
63 | if os.path.isfile("requirements.txt"): | |
64 | _install_deps(tempdir) | |
65 | log.info("Copying contents") | |
66 | # TODO: currently the only file relevant to a compiled cephadm is the | |
67 | # cephadm.py file. Once cephadm is broken up into multiple py files | |
68 | # (and possibly other libs from python-common, etc) we'll want some | |
69 | # sort organized structure to track what gets copied into the | |
70 | # dir to be zipped. For now we just have a simple call to copy | |
71 | # (and rename) the one file we care about. | |
72 | shutil.copy("cephadm.py", tempdir / "__main__.py") | |
aee94f69 TL |
73 | if versioning_vars: |
74 | generate_version_file(versioning_vars, tempdir / "_version.py") | |
1e59de90 TL |
75 | _compile(dest, tempdir) |
76 | finally: | |
77 | shutil.rmtree(tempdir) | |
78 | ||
79 | ||
80 | def _compile(dest, tempdir): | |
81 | """Compile the zipapp.""" | |
82 | log.info("Byte-compiling py to pyc") | |
83 | compileall.compile_dir( | |
84 | tempdir, | |
85 | maxlevels=16, | |
86 | legacy=True, | |
87 | quiet=1, | |
88 | workers=0, | |
89 | ) | |
90 | # TODO we could explicitly pass a python version here | |
91 | log.info("Constructing the zipapp file") | |
92 | try: | |
93 | zipapp.create_archive( | |
94 | source=tempdir, | |
95 | target=dest, | |
96 | interpreter=sys.executable, | |
97 | compressed=True, | |
98 | ) | |
99 | log.info("Zipapp created with compression") | |
100 | except TypeError: | |
101 | # automatically fall back to uncompressed | |
102 | zipapp.create_archive( | |
103 | source=tempdir, | |
104 | target=dest, | |
105 | interpreter=sys.executable, | |
106 | ) | |
107 | log.info("Zipapp created without compression") | |
108 | ||
109 | ||
110 | def _install_deps(tempdir): | |
111 | """Install dependencies with pip.""" | |
112 | # TODO we could explicitly pass a python version here | |
113 | log.info("Installing dependencies") | |
114 | # apparently pip doesn't have an API, just a cli. | |
115 | subprocess.check_call( | |
116 | [ | |
117 | sys.executable, | |
118 | "-m", | |
119 | "pip", | |
120 | "install", | |
121 | "--requirement", | |
122 | "requirements.txt", | |
123 | "--target", | |
124 | tempdir, | |
125 | ] | |
126 | ) | |
127 | ||
128 | ||
aee94f69 TL |
129 | def generate_version_file(versioning_vars, dest): |
130 | log.info("Generating version file") | |
131 | log.debug("versioning_vars=%r", versioning_vars) | |
132 | with open(dest, "w") as fh: | |
133 | print("# GENERATED FILE -- do not edit", file=fh) | |
134 | for key, value in versioning_vars: | |
135 | print(f"{key} = {value!r}", file=fh) | |
136 | ||
137 | ||
138 | def version_kv_pair(value): | |
139 | if "=" not in value: | |
140 | raise argparse.ArgumentTypeError(f"not a key=value pair: {value!r}") | |
141 | key, value = value.split("=", 1) | |
142 | if key not in _VALID_VERS_VARS: | |
143 | raise argparse.ArgumentTypeError(f"Unexpected key: {key!r}") | |
144 | return key, value | |
145 | ||
146 | ||
1e59de90 TL |
147 | def main(): |
148 | handler = logging.StreamHandler(sys.stdout) | |
149 | handler.setFormatter(logging.Formatter("cephadm/build.py: %(message)s")) | |
150 | log.addHandler(handler) | |
151 | log.setLevel(logging.INFO) | |
152 | ||
153 | log.debug("argv: %r", sys.argv) | |
154 | parser = argparse.ArgumentParser() | |
155 | parser.add_argument( | |
156 | "dest", help="Destination path name for new cephadm binary" | |
157 | ) | |
158 | parser.add_argument( | |
159 | "--source", help="Directory containing cephadm sources" | |
160 | ) | |
161 | parser.add_argument( | |
162 | "--python", help="The path to the desired version of python" | |
163 | ) | |
aee94f69 TL |
164 | parser.add_argument( |
165 | "--set-version-var", | |
166 | "-S", | |
167 | type=version_kv_pair, | |
168 | dest="version_vars", | |
169 | action="append", | |
170 | help="Set a key=value pair in the generated version info file", | |
171 | ) | |
1e59de90 TL |
172 | args = parser.parse_args() |
173 | ||
174 | if not _did_rexec() and args.python: | |
175 | _reexec(args.python) | |
176 | ||
177 | log.info( | |
178 | "Python Version: {v.major}.{v.minor}.{v.micro}".format( | |
179 | v=sys.version_info | |
180 | ) | |
181 | ) | |
182 | log.info("Args: %s", vars(args)) | |
183 | if not HAS_ZIPAPP: | |
184 | # Unconditionally display an error that the version of python | |
185 | # lacks zipapp (probably too old). | |
186 | print("error: zipapp module not found", file=sys.stderr) | |
187 | print( | |
188 | "(zipapp is available in Python 3.5 or later." | |
189 | " are you using a new enough version?)", | |
190 | file=sys.stderr, | |
191 | ) | |
192 | sys.exit(2) | |
193 | if args.source: | |
194 | source = pathlib.Path(args.source).absolute() | |
195 | else: | |
196 | source = pathlib.Path(__file__).absolute().parent | |
197 | dest = pathlib.Path(args.dest).absolute() | |
198 | log.info("Source Dir: %s", source) | |
199 | log.info("Destination Path: %s", dest) | |
aee94f69 | 200 | _build(dest, source, versioning_vars=args.version_vars) |
1e59de90 TL |
201 | |
202 | ||
203 | if __name__ == "__main__": | |
204 | main() |