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