Plain is headed towards 1.0! Subscribe for development updates →

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