summaryrefslogtreecommitdiffstats
path: root/src/cephadm/build.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/cephadm/build.py')
-rwxr-xr-xsrc/cephadm/build.py204
1 files changed, 204 insertions, 0 deletions
diff --git a/src/cephadm/build.py b/src/cephadm/build.py
new file mode 100755
index 000000000..4264b814f
--- /dev/null
+++ b/src/cephadm/build.py
@@ -0,0 +1,204 @@
+#!/usr/bin/python3
+"""Build cephadm from one or more files into a standalone executable.
+"""
+# TODO: If cephadm is being built and packaged within a format such as RPM
+# do we have to do anything special wrt passing in the version
+# of python to build with? Even with the intermediate cmake layer?
+
+import argparse
+import compileall
+import logging
+import os
+import pathlib
+import shutil
+import subprocess
+import tempfile
+import sys
+
+HAS_ZIPAPP = False
+try:
+ import zipapp
+
+ HAS_ZIPAPP = True
+except ImportError:
+ pass
+
+
+log = logging.getLogger(__name__)
+
+
+_VALID_VERS_VARS = [
+ "CEPH_GIT_VER",
+ "CEPH_GIT_NICE_VER",
+ "CEPH_RELEASE",
+ "CEPH_RELEASE_NAME",
+ "CEPH_RELEASE_TYPE",
+]
+
+
+def _reexec(python):
+ """Switch to the selected version of python by exec'ing into the desired
+ python path.
+ Sets the _BUILD_PYTHON_SET env variable as a sentinel to indicate exec has
+ been performed.
+ """
+ env = os.environ.copy()
+ env["_BUILD_PYTHON_SET"] = python
+ os.execvpe(python, [python, __file__] + sys.argv[1:], env)
+
+
+def _did_rexec():
+ """Returns true if the process has already exec'ed into the desired python
+ version.
+ """
+ return bool(os.environ.get("_BUILD_PYTHON_SET", ""))
+
+
+def _build(dest, src, versioning_vars=None):
+ """Build the binary."""
+ os.chdir(src)
+ tempdir = pathlib.Path(tempfile.mkdtemp(suffix=".cephadm.build"))
+ log.debug("working in %s", tempdir)
+ try:
+ if os.path.isfile("requirements.txt"):
+ _install_deps(tempdir)
+ log.info("Copying contents")
+ # TODO: currently the only file relevant to a compiled cephadm is the
+ # cephadm.py file. Once cephadm is broken up into multiple py files
+ # (and possibly other libs from python-common, etc) we'll want some
+ # sort organized structure to track what gets copied into the
+ # dir to be zipped. For now we just have a simple call to copy
+ # (and rename) the one file we care about.
+ shutil.copy("cephadm.py", tempdir / "__main__.py")
+ if versioning_vars:
+ generate_version_file(versioning_vars, tempdir / "_version.py")
+ _compile(dest, tempdir)
+ finally:
+ shutil.rmtree(tempdir)
+
+
+def _compile(dest, tempdir):
+ """Compile the zipapp."""
+ log.info("Byte-compiling py to pyc")
+ compileall.compile_dir(
+ tempdir,
+ maxlevels=16,
+ legacy=True,
+ quiet=1,
+ workers=0,
+ )
+ # TODO we could explicitly pass a python version here
+ log.info("Constructing the zipapp file")
+ try:
+ zipapp.create_archive(
+ source=tempdir,
+ target=dest,
+ interpreter=sys.executable,
+ compressed=True,
+ )
+ log.info("Zipapp created with compression")
+ except TypeError:
+ # automatically fall back to uncompressed
+ zipapp.create_archive(
+ source=tempdir,
+ target=dest,
+ interpreter=sys.executable,
+ )
+ log.info("Zipapp created without compression")
+
+
+def _install_deps(tempdir):
+ """Install dependencies with pip."""
+ # TODO we could explicitly pass a python version here
+ log.info("Installing dependencies")
+ # apparently pip doesn't have an API, just a cli.
+ subprocess.check_call(
+ [
+ sys.executable,
+ "-m",
+ "pip",
+ "install",
+ "--requirement",
+ "requirements.txt",
+ "--target",
+ tempdir,
+ ]
+ )
+
+
+def generate_version_file(versioning_vars, dest):
+ log.info("Generating version file")
+ log.debug("versioning_vars=%r", versioning_vars)
+ with open(dest, "w") as fh:
+ print("# GENERATED FILE -- do not edit", file=fh)
+ for key, value in versioning_vars:
+ print(f"{key} = {value!r}", file=fh)
+
+
+def version_kv_pair(value):
+ if "=" not in value:
+ raise argparse.ArgumentTypeError(f"not a key=value pair: {value!r}")
+ key, value = value.split("=", 1)
+ if key not in _VALID_VERS_VARS:
+ raise argparse.ArgumentTypeError(f"Unexpected key: {key!r}")
+ return key, value
+
+
+def main():
+ handler = logging.StreamHandler(sys.stdout)
+ handler.setFormatter(logging.Formatter("cephadm/build.py: %(message)s"))
+ log.addHandler(handler)
+ log.setLevel(logging.INFO)
+
+ log.debug("argv: %r", sys.argv)
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "dest", help="Destination path name for new cephadm binary"
+ )
+ parser.add_argument(
+ "--source", help="Directory containing cephadm sources"
+ )
+ parser.add_argument(
+ "--python", help="The path to the desired version of python"
+ )
+ parser.add_argument(
+ "--set-version-var",
+ "-S",
+ type=version_kv_pair,
+ dest="version_vars",
+ action="append",
+ help="Set a key=value pair in the generated version info file",
+ )
+ args = parser.parse_args()
+
+ if not _did_rexec() and args.python:
+ _reexec(args.python)
+
+ log.info(
+ "Python Version: {v.major}.{v.minor}.{v.micro}".format(
+ v=sys.version_info
+ )
+ )
+ log.info("Args: %s", vars(args))
+ if not HAS_ZIPAPP:
+ # Unconditionally display an error that the version of python
+ # lacks zipapp (probably too old).
+ print("error: zipapp module not found", file=sys.stderr)
+ print(
+ "(zipapp is available in Python 3.5 or later."
+ " are you using a new enough version?)",
+ file=sys.stderr,
+ )
+ sys.exit(2)
+ if args.source:
+ source = pathlib.Path(args.source).absolute()
+ else:
+ source = pathlib.Path(__file__).absolute().parent
+ dest = pathlib.Path(args.dest).absolute()
+ log.info("Source Dir: %s", source)
+ log.info("Destination Path: %s", dest)
+ _build(dest, source, versioning_vars=args.version_vars)
+
+
+if __name__ == "__main__":
+ main()