xcompile.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. # Copyright Materialize, Inc. and contributors. All rights reserved.
  2. #
  3. # Use of this software is governed by the Business Source License
  4. # included in the LICENSE file at the root of this repository.
  5. #
  6. # As of the Change Date specified in that file, in accordance with
  7. # the Business Source License, use of this software will be governed
  8. # by the Apache License, Version 2.0.
  9. """Support for cross-compiling to Linux."""
  10. import os
  11. import platform
  12. import sys
  13. from enum import Enum
  14. from materialize import MZ_ROOT, spawn
  15. from materialize.rustc_flags import Sanitizer
  16. class Arch(Enum):
  17. """A CPU architecture."""
  18. X86_64 = "x86_64"
  19. """The 64-bit x86 architecture."""
  20. AARCH64 = "aarch64"
  21. """The 64-bit ARM architecture."""
  22. def __str__(self) -> str:
  23. return self.value
  24. def go_str(self) -> str:
  25. """Return the architecture name in Go nomenclature: amd64 or arm64."""
  26. if self == Arch.X86_64:
  27. return "amd64"
  28. elif self == Arch.AARCH64:
  29. return "arm64"
  30. else:
  31. raise RuntimeError("unreachable")
  32. @staticmethod
  33. def host() -> "Arch":
  34. if platform.machine() == "x86_64":
  35. return Arch.X86_64
  36. elif platform.machine() in ["aarch64", "arm64"]:
  37. return Arch.AARCH64
  38. else:
  39. raise RuntimeError(f"unknown host architecture {platform.machine()}")
  40. def target(arch: Arch) -> str:
  41. """Construct a Linux target triple for the specified architecture."""
  42. return f"{arch}-unknown-linux-gnu"
  43. def target_cpu(arch: Arch) -> str:
  44. """
  45. Return the CPU micro architecture, assuming a Linux target, we should use for Rust compilation.
  46. Sync: This target-cpu should be kept in sync with the one in ci-builder and .cargo/config.
  47. """
  48. if arch == Arch.X86_64:
  49. return "x86-64-v3"
  50. elif arch == Arch.AARCH64:
  51. return "neoverse-n1"
  52. else:
  53. raise RuntimeError("unreachable")
  54. def target_features(arch: Arch) -> list[str]:
  55. """
  56. Returns a list of CPU features we should enable for Rust compilation.
  57. Note: We also specify the CPU target when compiling Rust which should enable the majority of
  58. available CPU features.
  59. Sync: This list of features should be kept in sync with the one in ci-builder and .cargo/config.
  60. """
  61. if arch == Arch.X86_64:
  62. return ["+aes", "+pclmulqdq"]
  63. elif arch == Arch.AARCH64:
  64. return ["+aes", "+sha2"]
  65. else:
  66. raise RuntimeError("unreachable")
  67. def bazel(
  68. arch: Arch,
  69. subcommand: str,
  70. rustflags: list[str],
  71. extra_env: dict[str, str] = {},
  72. ) -> list[str]:
  73. """Construct a Bazel invocation for cross compiling.
  74. Args:
  75. arch: The CPU architecture to build for.
  76. subcommand: The Bazel subcommand to invoke.
  77. rustflags: Override the flags passed to the Rust compiler. If the list
  78. is empty, the default flags are used.
  79. extra_env: Extra environment variables to set for the execution of
  80. Bazel.
  81. is_tagged_build: Should this build be stamped with release info.
  82. """
  83. # Note: Unlike `cargo`, Bazel does not use CI_BUILDER and all of the cross
  84. # compilation is handled at a higher level.
  85. platform = f"--platforms=@toolchains_llvm//platforms:linux-{str(arch)}"
  86. assert not (
  87. sys.platform == "darwin" and arch == Arch.X86_64
  88. ), "cross compiling to Linux x86_64 is not supported from macOS"
  89. bazel_flags = ["--config=linux"]
  90. rustc_flags = [
  91. f"--@rules_rust//:extra_rustc_flag={flag}"
  92. for flag in rustflags
  93. # We apply `tokio_unstable` at the `WORKSPACE` level so skip it here to
  94. # prevent changing the compile options and possibly missing cache hits.
  95. if "tokio_unstable" not in flag
  96. ]
  97. return ["bazel", subcommand, platform, *bazel_flags, *rustc_flags]
  98. def cargo(
  99. arch: Arch,
  100. subcommand: str,
  101. rustflags: list[str],
  102. channel: str | None = None,
  103. extra_env: dict[str, str] = {},
  104. ) -> list[str]:
  105. """Construct a Cargo invocation for cross compiling.
  106. Args:
  107. arch: The CPU architecture to build for.
  108. subcommand: The Cargo subcommand to invoke.
  109. rustflags: Override the flags passed to the Rust compiler. If the list
  110. is empty, the default flags are used.
  111. channel: The Rust toolchain channel to use. Either None/"stable" or "nightly".
  112. Returns:
  113. A list of arguments specifying the beginning of the command to invoke.
  114. """
  115. _target = target(arch)
  116. _target_env = _target.upper().replace("-", "_")
  117. env = {
  118. **extra_env,
  119. }
  120. rustflags += [
  121. "-Clink-arg=-Wl,--compress-debug-sections=zlib",
  122. "-Clink-arg=-Wl,-O3",
  123. "-Csymbol-mangling-version=v0",
  124. "--cfg=tokio_unstable",
  125. ]
  126. if sys.platform == "darwin":
  127. _bootstrap_darwin(arch)
  128. lld_prefix = spawn.capture(["brew", "--prefix", "lld"]).strip()
  129. sysroot = spawn.capture([f"{_target}-cc", "-print-sysroot"]).strip()
  130. rustflags += [
  131. f"-L{sysroot}/lib",
  132. "-Clink-arg=-fuse-ld=lld",
  133. f"-Clink-arg=-B{lld_prefix}/bin",
  134. ]
  135. env.update(
  136. {
  137. "CMAKE_SYSTEM_NAME": "Linux",
  138. f"CARGO_TARGET_{_target_env}_LINKER": f"{_target}-cc",
  139. "CARGO_TARGET_DIR": str(MZ_ROOT / "target-xcompile"),
  140. "TARGET_AR": f"{_target}-ar",
  141. "TARGET_CPP": f"{_target}-cpp",
  142. "TARGET_CC": f"{_target}-cc",
  143. "TARGET_CXX": f"{_target}-c++",
  144. "TARGET_CXXSTDLIB": "static=stdc++",
  145. "TARGET_LD": f"{_target}-ld",
  146. "TARGET_RANLIB": f"{_target}-ranlib",
  147. }
  148. )
  149. else:
  150. # NOTE(benesch): The required Rust flags have to be duplicated with
  151. # their definitions in ci/builder/Dockerfile because `rustc` has no way
  152. # to merge together Rust flags from different sources.
  153. rustflags += [
  154. "-Clink-arg=-fuse-ld=lld",
  155. f"-L/opt/x-tools/{_target}/{_target}/sysroot/lib",
  156. ]
  157. env.update({"RUSTFLAGS": " ".join(rustflags)})
  158. return [
  159. *_enter_builder(arch, channel),
  160. "env",
  161. *(f"{k}={v}" for k, v in env.items()),
  162. "cargo",
  163. subcommand,
  164. "--target",
  165. _target,
  166. ]
  167. def tool(
  168. arch: Arch, name: str, channel: str | None = None, prefix_name: bool = True
  169. ) -> list[str]:
  170. """Constructs a cross-compiling binutils tool invocation.
  171. Args:
  172. arch: The CPU architecture to build for.
  173. name: The name of the binutils tool to invoke.
  174. channel: The Rust toolchain channel to use. Either None/"stable" or "nightly".
  175. prefix_name: Whether or not the tool name should be prefixed with the target
  176. architecture.
  177. Returns:
  178. A list of arguments specifying the beginning of the command to invoke.
  179. """
  180. if sys.platform == "darwin":
  181. _bootstrap_darwin(arch)
  182. tool_name = f"{target(arch)}-{name}" if prefix_name else name
  183. return [
  184. *_enter_builder(arch, channel),
  185. tool_name,
  186. ]
  187. def _enter_builder(arch: Arch, channel: str | None = None) -> list[str]:
  188. if "MZ_DEV_CI_BUILDER" in os.environ or sys.platform == "darwin":
  189. return []
  190. else:
  191. default_channel = (
  192. "stable"
  193. if Sanitizer[os.getenv("CI_SANITIZER", "none")] == Sanitizer.none
  194. else "nightly"
  195. )
  196. return [
  197. "env",
  198. f"MZ_DEV_CI_BUILDER_ARCH={arch}",
  199. "bin/ci-builder",
  200. "run",
  201. channel if channel else default_channel,
  202. ]
  203. def _bootstrap_darwin(arch: Arch) -> None:
  204. # Building in Docker for Mac is painfully slow, so we install a
  205. # cross-compiling toolchain on the host and use that instead.
  206. BOOTSTRAP_VERSION = "5"
  207. BOOTSTRAP_FILE = MZ_ROOT / "target-xcompile" / target(arch) / ".xcompile-bootstrap"
  208. try:
  209. contents = BOOTSTRAP_FILE.read_text()
  210. except FileNotFoundError:
  211. contents = ""
  212. if contents == BOOTSTRAP_VERSION:
  213. return
  214. spawn.runv(["brew", "install", "lld", f"materializeinc/crosstools/{target(arch)}"])
  215. spawn.runv(["rustup", "target", "add", target(arch)])
  216. BOOTSTRAP_FILE.parent.mkdir(parents=True, exist_ok=True)
  217. BOOTSTRAP_FILE.write_text(BOOTSTRAP_VERSION)