Plain is headed towards 1.0! Subscribe for development updates →

  1"""
  2Biome standalone binary management for plain-code.
  3"""
  4
  5import os
  6import platform
  7import subprocess
  8
  9import click
 10import requests
 11import tomlkit
 12
 13from plain.internal import internalcode
 14from plain.runtime import PLAIN_TEMP_PATH
 15
 16
 17@internalcode
 18class Biome:
 19    """Download, install, and invoke the Biome CLI standalone binary."""
 20
 21    TAG_PREFIX = "@biomejs/biome@"
 22
 23    @property
 24    def target_directory(self) -> str:
 25        # Directory under .plain to store the binary and lockfile
 26        return str(PLAIN_TEMP_PATH)
 27
 28    @property
 29    def standalone_path(self) -> str:
 30        # On Windows, use .exe suffix
 31        exe = ".exe" if platform.system() == "Windows" else ""
 32        return os.path.join(self.target_directory, f"biome{exe}")
 33
 34    @property
 35    def version_lockfile_path(self) -> str:
 36        return os.path.join(self.target_directory, "biome.version")
 37
 38    def is_installed(self) -> bool:
 39        td = self.target_directory
 40        if not os.path.isdir(td):
 41            os.makedirs(td, exist_ok=True)
 42        return os.path.exists(self.standalone_path)
 43
 44    def needs_update(self) -> bool:
 45        if not self.is_installed():
 46            return True
 47        if not os.path.exists(self.version_lockfile_path):
 48            return True
 49        with open(self.version_lockfile_path) as f:
 50            locked = f.read().strip()
 51        return locked != self.get_version_from_config()
 52
 53    def get_version_from_config(self) -> str:
 54        # Read version from pyproject.toml under tool.plain.code.biome
 55        project_root = os.path.dirname(self.target_directory)
 56        pyproject = os.path.join(project_root, "pyproject.toml")
 57        if not os.path.exists(pyproject):
 58            return ""
 59        doc = tomlkit.loads(open(pyproject, "rb").read().decode())
 60        return (
 61            doc.get("tool", {})
 62            .get("plain", {})
 63            .get("code", {})
 64            .get("biome", {})
 65            .get("version", "")
 66        )
 67
 68    def set_version_in_config(self, version: str) -> None:
 69        # Persist version to pyproject.toml under tool.plain.code.biome
 70        project_root = os.path.dirname(self.target_directory)
 71        pyproject = os.path.join(project_root, "pyproject.toml")
 72        if not os.path.exists(pyproject):
 73            return
 74        doc = tomlkit.loads(open(pyproject, "rb").read().decode())
 75        doc.setdefault("tool", {}).setdefault("plain", {}).setdefault(
 76            "code", {}
 77        ).setdefault("biome", {})["version"] = version
 78        open(pyproject, "w").write(tomlkit.dumps(doc))
 79
 80    def detect_platform_slug(self) -> str:
 81        # Determine the asset slug for the current OS/arch
 82        system = platform.system()
 83        arch = platform.machine()
 84        if system == "Windows":
 85            # use win32 glibc build
 86            return "win32-arm64.exe" if arch.lower() == "arm64" else "win32-x64.exe"
 87        if system == "Linux":
 88            # prefer glibc builds
 89            return "linux-arm64" if arch == "aarch64" else "linux-x64"
 90        if system == "Darwin":
 91            return "darwin-arm64" if arch == "arm64" else "darwin-x64"
 92        raise RuntimeError(f"Unsupported platform for Biome: {system}/{arch}")
 93
 94    def download(self, version: str = "") -> str:
 95        # Build download URL based on version (tag: cli/vX.Y.Z) or latest
 96        slug = self.detect_platform_slug()
 97        if version:
 98            url = (
 99                f"https://github.com/biomejs/biome/releases/download/{self.TAG_PREFIX}{version}/"
100                f"biome-{slug}"
101            )
102        else:
103            url = (
104                f"https://github.com/biomejs/biome/releases/latest/download/"
105                f"biome-{slug}"
106            )
107
108        resp = requests.get(url, stream=True)
109        resp.raise_for_status()
110
111        # Make sure the target directory exists
112        td = self.target_directory
113        if not os.path.isdir(td):
114            os.makedirs(td, exist_ok=True)
115
116        total = int(resp.headers.get("Content-Length", 0))
117        with open(self.standalone_path, "wb") as f:
118            if total:
119                with click.progressbar(
120                    length=total,
121                    label="Downloading Biome",
122                    width=0,
123                ) as bar:
124                    for chunk in resp.iter_content(chunk_size=8192):
125                        f.write(chunk)
126                        bar.update(len(chunk))
127            else:
128                for chunk in resp.iter_content(chunk_size=8192):
129                    f.write(chunk)
130        os.chmod(self.standalone_path, 0o755)
131
132        # Determine resolved version for lockfile
133        if version:
134            resolved = version.lstrip("v")
135        else:
136            resolved = ""
137            if resp.history:
138                # Look for redirect to actual tag version
139                loc = resp.history[0].headers.get("Location", "")
140                if self.TAG_PREFIX in loc:
141                    remaining = loc.split(self.TAG_PREFIX, 1)[-1]
142                    resolved = remaining.split("/")[0]
143
144            if not resolved:
145                raise RuntimeError("Failed to determine resolved version from redirect")
146
147        open(self.version_lockfile_path, "w").write(resolved)
148
149        return resolved
150
151    def install(self, version: str = "") -> str:
152        v = self.download(version)
153        self.set_version_in_config(v)
154        return v
155
156    def invoke(self, *args, cwd=None) -> subprocess.CompletedProcess:
157        # Run the standalone biome binary with given args
158        config_path = os.path.abspath(
159            os.path.join(os.path.dirname(__file__), "biome_defaults.json")
160        )
161        args = list(args) + ["--config-path", config_path, "--vcs-root", os.getcwd()]
162        return subprocess.run([self.standalone_path, *args], cwd=cwd)