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")