Plain is headed towards 1.0! Subscribe for development updates →

  1import os
  2import platform
  3import subprocess
  4import sys
  5
  6import requests
  7import tomlkit
  8
  9from plain.internal import internalcode
 10from plain.packages import packages_registry
 11from plain.runtime import APP_PATH, settings
 12
 13
 14@internalcode
 15class Tailwind:
 16    @property
 17    def target_directory(self) -> str:
 18        return str(settings.PLAIN_TEMP_PATH)
 19
 20    @property
 21    def standalone_path(self) -> str:
 22        return os.path.join(self.target_directory, "tailwind")
 23
 24    @property
 25    def version_lockfile_path(self) -> str:
 26        return os.path.join(self.target_directory, "tailwind.version")
 27
 28    @property
 29    def src_css_path(self) -> str:
 30        return settings.TAILWIND_SRC_PATH
 31
 32    @property
 33    def dist_css_path(self) -> str:
 34        return settings.TAILWIND_DIST_PATH
 35
 36    def update_plain_sources(self):
 37        paths = set()
 38
 39        # Add paths from installed packages
 40        for package_config in packages_registry.get_package_configs():
 41            abs_package_path = os.path.abspath(package_config.path)
 42            abs_app_path = os.path.abspath(APP_PATH)
 43            if os.path.commonpath([abs_app_path, abs_package_path]) != abs_app_path:
 44                paths.add(os.path.relpath(abs_package_path, self.target_directory))
 45
 46        # Sort the paths so that the order is consistent
 47        paths = sorted(paths)
 48
 49        plain_sources_path = os.path.join(self.target_directory, "tailwind.css")
 50        with open(plain_sources_path, "w") as f:
 51            for path in paths:
 52                f.write(f'@source "{path}";\n')
 53
 54    def invoke(self, *args, cwd=None) -> None:
 55        result = subprocess.run([self.standalone_path] + list(args), cwd=cwd)
 56        if result.returncode != 0:
 57            sys.exit(result.returncode)
 58
 59    def is_installed(self) -> bool:
 60        if not os.path.exists(self.target_directory):
 61            os.mkdir(self.target_directory)
 62        return os.path.exists(os.path.join(self.target_directory, "tailwind"))
 63
 64    def create_src_css(self):
 65        os.makedirs(os.path.dirname(self.src_css_path), exist_ok=True)
 66        with open(self.src_css_path, "w") as f:
 67            f.write("""@import "tailwindcss";\n@import "./.plain/tailwind.css";\n""")
 68
 69    def needs_update(self) -> bool:
 70        if not os.path.exists(self.version_lockfile_path):
 71            return True
 72
 73        with open(self.version_lockfile_path) as f:
 74            locked_version = f.read().strip()
 75
 76        if locked_version != self.get_version_from_config():
 77            return True
 78
 79        return False
 80
 81    def get_version_from_config(self) -> str:
 82        pyproject_path = os.path.join(
 83            os.path.dirname(self.target_directory), "pyproject.toml"
 84        )
 85
 86        if not os.path.exists(pyproject_path):
 87            return ""
 88
 89        with open(pyproject_path) as f:
 90            config = tomlkit.load(f)
 91            return (
 92                config.get("tool", {})
 93                .get("plain", {})
 94                .get("tailwind", {})
 95                .get("version", "")
 96            )
 97
 98    def set_version_in_config(self, version):
 99        pyproject_path = os.path.join(
100            os.path.dirname(self.target_directory), "pyproject.toml"
101        )
102
103        with open(pyproject_path) as f:
104            config = tomlkit.load(f)
105
106        config.setdefault("tool", {}).setdefault("plain", {}).setdefault(
107            "tailwind", {}
108        )["version"] = version
109
110        with open(pyproject_path, "w") as f:
111            tomlkit.dump(config, f)
112
113    def download(self, version="") -> str:
114        if version:
115            if not version.startswith("v"):
116                version = f"v{version}"
117            url = f"https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{self.detect_platform_slug()}"
118        else:
119            url = f"https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-{self.detect_platform_slug()}"
120
121        with requests.get(url, stream=True) as response:
122            response.raise_for_status()
123            with open(self.standalone_path, "wb") as f:
124                for chunk in response.iter_content(chunk_size=8192):
125                    f.write(chunk)
126
127        os.chmod(self.standalone_path, 0o755)
128
129        if not version:
130            # Get the version from the redirect chain (latest -> vX.Y.Z)
131            version = response.history[1].url.split("/")[-2]
132
133        version = version.lstrip("v")
134
135        with open(self.version_lockfile_path, "w") as f:
136            f.write(version)
137
138        return version
139
140    def install(self, version="") -> str:
141        installed_version = self.download(version)
142        self.set_version_in_config(installed_version)
143        return installed_version
144
145    @staticmethod
146    def detect_platform_slug() -> str:
147        uname = platform.uname()[0]
148
149        if uname == "Windows":
150            return "windows-x64.exe"
151
152        if uname == "Linux" and platform.uname()[4] == "aarch64":
153            return "linux-arm64"
154
155        if uname == "Linux":
156            return "linux-x64"
157
158        if uname == "Darwin" and platform.uname().machine == "arm64":
159            return "macos-arm64"
160
161        if uname == "Darwin":
162            return "macos-x64"
163
164        raise Exception("Unsupported platform for Tailwind standalone")