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