|
| 1 | +#! /usr/bin/env python3 |
| 2 | + |
| 3 | +import platform |
| 4 | +import os |
| 5 | +import re |
| 6 | +import subprocess as sp |
| 7 | +import shutil |
| 8 | + |
| 9 | +from pathlib import Path |
| 10 | +from io import StringIO |
| 11 | +from contextlib import contextmanager |
| 12 | + |
| 13 | + |
| 14 | +SCRIPT_PATH = Path(__file__).parent.resolve() |
| 15 | +BUILD_ROOT = SCRIPT_PATH.parent.resolve() |
| 16 | + |
| 17 | + |
| 18 | +class Build: |
| 19 | + skip_modules = [ |
| 20 | + "qt3d", |
| 21 | + "qtdoc", |
| 22 | + "qtmultimedia", |
| 23 | + "qtsensors", |
| 24 | + "qtserialport", |
| 25 | + "qtactiveqt", |
| 26 | + "qtcharts", |
| 27 | + "qtdatavis3d", |
| 28 | + "qtgraphicaleffects", |
| 29 | + "qtpurchasing", |
| 30 | + "qtwebview", |
| 31 | + "qtscript", |
| 32 | + "qtlocation", |
| 33 | + "qtscxml", |
| 34 | + "qtspeech", |
| 35 | + "qtlottie", |
| 36 | + "qtwebglplugin", |
| 37 | + ] |
| 38 | + |
| 39 | + common_flags = [ |
| 40 | + "-opensource", |
| 41 | + "-confirm-license", |
| 42 | + "-webengine-proprietary-codecs", |
| 43 | + "-nomake tests", |
| 44 | + "-nomake examples", |
| 45 | + "-no-gif", |
| 46 | + "-qt-libpng", |
| 47 | + "-qt-libjpeg", |
| 48 | + "-qt-pcre", |
| 49 | + "-no-cups", |
| 50 | + "-no-dbus", |
| 51 | + "-pch", |
| 52 | + "-no-qml-debug", |
| 53 | + "-no-openssl", |
| 54 | + "-c++std c++14", |
| 55 | + ] |
| 56 | + |
| 57 | + prefix = "qt-install" |
| 58 | + |
| 59 | + def __init__(self, profile: str): |
| 60 | + self.profile = profile |
| 61 | + self.common_flags.append(f"-prefix {str(self.build_root / self.prefix)}") |
| 62 | + |
| 63 | + def run(self, is_dry=False): |
| 64 | + flags = self.compute_flags() |
| 65 | + env = self.compute_env() |
| 66 | + if self.is_windows: |
| 67 | + return self.run_windows(is_dry, flags, env) |
| 68 | + elif self.is_macos: |
| 69 | + return self.run_macos(is_dry, flags, env) |
| 70 | + |
| 71 | + def run_windows(self, is_dry, flags, env): |
| 72 | + self._download_jom() |
| 73 | + # to make the path as short as possible we create a junction here. |
| 74 | + rootdrive = os.path.splitdrive(os.getenv("JENKINS_WORKSPACE_ROOT", "c:\\"))[0] |
| 75 | + root_junction = Path(f"{rootdrive}\\j") |
| 76 | + root_junction.mkdir(exist_ok=True) |
| 77 | + junctiondir = root_junction / os.getenv("PLEX_BUILD_HASH", "xx") |
| 78 | + if junctiondir.exists(): |
| 79 | + # junctions? more like junk-tions |
| 80 | + sp.run(["fsutil", "reparsepoint", "delete", str(junctiondir)], shell=True) |
| 81 | + try: |
| 82 | + shutil.rmtree(junctiondir) |
| 83 | + except FileNotFoundError: |
| 84 | + pass |
| 85 | + mklink = sp.run(["mklink", "/J", str(junctiondir), str(self.build_root)], shell=True) |
| 86 | + |
| 87 | + paths = [self.python_path, *self._sanitize_path()] |
| 88 | + for extra_path in ("gnuwin/bin", "qtbase/bin", str(self.script_path)): |
| 89 | + paths.append(str(self.build_root / extra_path)) |
| 90 | + env["PATH"] = os.pathsep.join(paths) |
| 91 | + env["PYTHONHOME"] = self.python_path |
| 92 | + |
| 93 | + with StringIO() as script: |
| 94 | + for key, value in env.items(): |
| 95 | + print(f"set {key}={value}", file=script) |
| 96 | + print(file=script) |
| 97 | + |
| 98 | + vs_dir = self._get_vs_dir() |
| 99 | + print(f"python -V", file=script) |
| 100 | + print(f'call "{vs_dir}\\VC\\Auxiliary\\Build\\vcvarsall.bat" amd64', file=script) |
| 101 | + print(f"call {str(self.build_root / 'configure.bat')} {' '.join(flags)}", file=script) |
| 102 | + print(f"jom /j{self.jobs}", file=script) |
| 103 | + print(f"if %ERRORLEVEL% GEQ 1 EXIT /B %ERRORLEVEL%", file=script) |
| 104 | + print(f"jom install", file=script) |
| 105 | + print(f"if %ERRORLEVEL% GEQ 1 EXIT /B %ERRORLEVEL%", file=script) |
| 106 | + print(script.getvalue()) |
| 107 | + build_script = Path("plex_build.cmd") |
| 108 | + with build_script.open("w") as fp: |
| 109 | + fp.write(script.getvalue()) |
| 110 | + if not is_dry: |
| 111 | + sp.run(["cmd", "/C", str(build_script.resolve())]).check_returncode() |
| 112 | + else: |
| 113 | + print(script.getvalue()) |
| 114 | + |
| 115 | + def run_macos(self, is_dry, flags, env): |
| 116 | + with StringIO() as script: |
| 117 | + print("#! /bin/bash", file=script) |
| 118 | + print("set +x", file=script) |
| 119 | + print("set +v", file=script) |
| 120 | + print("set +e", file=script) |
| 121 | + print(file=script) |
| 122 | + for key, value in env.items(): |
| 123 | + print(f"export {key}='{value}'", file=script) |
| 124 | + print(file=script) |
| 125 | + print(f"python -V", file=script) |
| 126 | + print(f"{self.build_root}/configure {' '.join(flags)}", file=script) |
| 127 | + print(f"make -j {self.jobs}", file=script) |
| 128 | + print(f"make install", file=script) |
| 129 | + build_script = Path("plex_build.sh") |
| 130 | + with build_script.open("w") as fp: |
| 131 | + fp.write(script.getvalue()) |
| 132 | + import stat |
| 133 | + st = os.stat(build_script).st_mode |
| 134 | + os.chmod(build_script, st | stat.S_IEXEC) |
| 135 | + print(script.getvalue()) |
| 136 | + if not is_dry: |
| 137 | + sp.run(["bash", "-c", str(build_script.resolve())]).check_returncode() |
| 138 | + |
| 139 | + def package(self): |
| 140 | + with chdir(self.build_root / self.prefix): |
| 141 | + print(f"Creating {self.package_name}") |
| 142 | + cmake = sp.run(["cmake", "-E", "tar", "cJf", |
| 143 | + f"../{self.package_name}", |
| 144 | + "--format=gnutar", "."]) |
| 145 | + cmake.check_returncode() |
| 146 | + |
| 147 | + def compute_flags(self) -> str: |
| 148 | + flags = self.common_flags |
| 149 | + flags += [f"-skip {mod}" for mod in self.skip_modules] |
| 150 | + |
| 151 | + if self.is_macos: |
| 152 | + flags += [ |
| 153 | + "-securetransport", |
| 154 | + "-opengl desktop", |
| 155 | + "-sdk macosx10.15", |
| 156 | + "-device-option QMAKE_APPLE_DEVICE_ARCHS=x86_64", |
| 157 | + "-xplatform macx-clang", |
| 158 | + "-reduce-exports", |
| 159 | + ] |
| 160 | + if self.is_debug: |
| 161 | + flags += ["-debug-and-release"] |
| 162 | + elif self.is_windows: |
| 163 | + flags += [ "-schannel", "-opengl dynamic" ] |
| 164 | + if self.is_debug: |
| 165 | + flags += ["-debug"] |
| 166 | + |
| 167 | + if not self.is_debug: |
| 168 | + flags += ["-release", "-ltcg", "-optimize-size"] |
| 169 | + else: |
| 170 | + flags += ["-separate-debug-info"] |
| 171 | + |
| 172 | + return flags |
| 173 | + |
| 174 | + def compute_env(self): |
| 175 | + jobs = self.jobs |
| 176 | + environment = { |
| 177 | + "CFLAGS": "", "CXXFLAGS": "", "LDFLAGS": "", "CL": "", |
| 178 | + "NINJAFLAGS": f"-j{jobs} -v", |
| 179 | + "PATH": f"{self.python_path}{os.pathsep}{os.environ['PATH']}" |
| 180 | + } |
| 181 | + return environment |
| 182 | + |
| 183 | + def write_spec(self): |
| 184 | + template = open(self.script_path / "Artifactory.spec.in").read() |
| 185 | + os_name = { |
| 186 | + "Darwin": "Macos", |
| 187 | + "Windows": "Windows", |
| 188 | + "Linux": "Linux" |
| 189 | + }[platform.system()] |
| 190 | + subst = { |
| 191 | + "build_root": str(self.build_root).replace("\\", "/"), |
| 192 | + "package_name": self.package_name, |
| 193 | + "qt_version": self.qt_version, |
| 194 | + "git_sha": self.git_sha, |
| 195 | + "full_version": self.full_version, |
| 196 | + "build_type": self.build_type, |
| 197 | + "os_name": os_name, |
| 198 | + } |
| 199 | + with open("Artifactory.spec", "w") as spec: |
| 200 | + spec.write(template.format(**subst)) |
| 201 | + |
| 202 | + @property |
| 203 | + def is_macos(self): |
| 204 | + return platform.system() == "Darwin" |
| 205 | + |
| 206 | + @property |
| 207 | + def is_windows(self): |
| 208 | + return platform.system() == "Windows" |
| 209 | + |
| 210 | + @property |
| 211 | + def is_debug(self): |
| 212 | + return self.profile.endswith("-debug") |
| 213 | + |
| 214 | + @property |
| 215 | + def build_type(self): |
| 216 | + return "debug" if self.is_debug else "release" |
| 217 | + |
| 218 | + @property |
| 219 | + def build_root(self): |
| 220 | + return BUILD_ROOT |
| 221 | + |
| 222 | + @property |
| 223 | + def python_path(self): |
| 224 | + if self.is_windows: |
| 225 | + return "C:\\Python27" |
| 226 | + elif self.is_macos: |
| 227 | + return "/usr/bin" |
| 228 | + |
| 229 | + @property |
| 230 | + def script_path(self): |
| 231 | + return SCRIPT_PATH |
| 232 | + |
| 233 | + @property |
| 234 | + def jobs(self): |
| 235 | + return os.getenv("PLEX_JOBS", "6") |
| 236 | + |
| 237 | + @property |
| 238 | + def qt_version(self): |
| 239 | + with open(self.build_root / "qtbase/.qmake.conf") as qconfig: |
| 240 | + for line in qconfig: |
| 241 | + match = re.search(r'MODULE_VERSION = (.+)', line) |
| 242 | + if match: |
| 243 | + return match.group(1) |
| 244 | + raise RuntimeError("Could not read MODULE_VERSION from qtbase/.qmake.conf") |
| 245 | + |
| 246 | + @property |
| 247 | + def full_version(self): |
| 248 | + return f"{self.qt_version}-{self.git_sha[:8]}" |
| 249 | + |
| 250 | + @property |
| 251 | + def git_sha(self): |
| 252 | + if "GIT_COMMIT" in os.environ: |
| 253 | + sha = os.environ["GIT_COMMIT"] |
| 254 | + else: |
| 255 | + git = sp.run(["git", "rev-parse", "HEAD"], stdout=sp.PIPE) |
| 256 | + git.check_returncode() |
| 257 | + sha = git.stdout.decode().strip() |
| 258 | + return sha |
| 259 | + |
| 260 | + @property |
| 261 | + def package_name(self): |
| 262 | + os_name = platform.system().lower() |
| 263 | + return f"qt-{self.full_version}-{os_name}-x86_64-{self.build_type}.tar.xz" |
| 264 | + |
| 265 | + def _download_jom(self): |
| 266 | + # we have curl on the build nodes, but not requests (it's also quicker) |
| 267 | + sp.run(["curl", "-L", "-o", "jom.zip", |
| 268 | + "https://download.qt.io/official_releases/jom/jom.zip"]) |
| 269 | + from zipfile import ZipFile |
| 270 | + with ZipFile("jom.zip", "r") as zfp: |
| 271 | + zfp.extractall() |
| 272 | + os.remove("jom.zip") |
| 273 | + |
| 274 | + def _download_vswhere(self): |
| 275 | + sp.run(["curl", "-L", "-o", "vswhere.exe", |
| 276 | + "https://github.com/microsoft/vswhere/releases/download/2.8.4/vswhere.exe"]) |
| 277 | + |
| 278 | + def _get_vs_dir(self): |
| 279 | + import json |
| 280 | + self._download_vswhere() |
| 281 | + vswhere = sp.run([str(Path.cwd() / "vswhere.exe"), "-format", "json", |
| 282 | + "-version", "15.0"], stdout=sp.PIPE) |
| 283 | + vswhere.check_returncode() |
| 284 | + vs_data = json.loads(vswhere.stdout.decode()) |
| 285 | + return vs_data[0]["installationPath"] |
| 286 | + |
| 287 | + def _sanitize_path(self): |
| 288 | + paths = os.environ["PATH"].split(os.pathsep) |
| 289 | + result = [] |
| 290 | + for path in paths: |
| 291 | + if "python" in path.lower() or "pyenv" in path.lower(): |
| 292 | + continue |
| 293 | + result.append(path) |
| 294 | + return result |
| 295 | + |
| 296 | + |
| 297 | +@contextmanager |
| 298 | +def chdir(dirname): |
| 299 | + try: |
| 300 | + cwd = os.getcwd() |
| 301 | + os.chdir(dirname) |
| 302 | + yield |
| 303 | + finally: |
| 304 | + os.chdir(cwd) |
| 305 | + |
| 306 | + |
| 307 | +if __name__ == "__main__": |
| 308 | + from argparse import ArgumentParser |
| 309 | + parser = ArgumentParser() |
| 310 | + parser.add_argument("--dry-run", action="store_true", help="Only display steps") |
| 311 | + parser.add_argument("--make-package", action="store_true", help="Create tarball") |
| 312 | + parser.add_argument("profile") |
| 313 | + args = parser.parse_args() |
| 314 | + build = Build(args.profile) |
| 315 | + build.run(args.dry_run) |
| 316 | + if args.make_package: |
| 317 | + build.package() |
| 318 | + build.write_spec() |
| 319 | + |
0 commit comments