machineconfig 3.92__py3-none-any.whl → 3.94__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/jobs/{python_custom_installers → installer/custom}/gh.py +22 -7
- machineconfig/jobs/{python_custom_installers → installer/custom}/hx.py +13 -4
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/alacritty.py +11 -5
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/brave.py +17 -14
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/bypass_paywall.py +7 -9
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/code.py +13 -13
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/cursor.py +7 -7
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/espanso.py +21 -17
- machineconfig/jobs/installer/custom_dev/goes.py +63 -0
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/lvim.py +10 -14
- machineconfig/jobs/installer/custom_dev/nerdfont.py +87 -0
- machineconfig/{setup_windows/wt_and_pwsh/install_nerd_fonts.py → jobs/installer/custom_dev/nerfont_windows_helper.py} +68 -25
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/redis.py +13 -8
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/wezterm.py +13 -7
- machineconfig/jobs/{python_custom_installers/dev → installer/custom_dev}/winget.py +1 -3
- machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/docker.sh +1 -1
- machineconfig/jobs/installer/packages_custom_dev.json +226 -0
- machineconfig/jobs/installer/packages_custom_essential.json +39 -0
- machineconfig/jobs/installer/packages_github_dev.json +1110 -0
- machineconfig/jobs/installer/packages_github_essential.json +804 -0
- machineconfig/jobs/linux/msc/cli_agents.sh +5 -0
- machineconfig/scripts/python/ai/solutions/gemini/settings.json +1 -1
- machineconfig/scripts/python/devops_devapps_install.py +31 -20
- machineconfig/utils/installer.py +17 -80
- machineconfig/utils/installer_utils/github_release_bulk.py +198 -0
- machineconfig/utils/installer_utils/installer_class.py +223 -210
- machineconfig/utils/schemas/installer/installer_types.py +29 -6
- {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/METADATA +1 -1
- {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/RECORD +45 -67
- machineconfig/jobs/python_custom_installers/archive/ngrok.py +0 -63
- machineconfig/jobs/python_custom_installers/dev/aider.py +0 -37
- machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +0 -78
- machineconfig/jobs/python_custom_installers/dev/goes.py +0 -55
- machineconfig/jobs/python_custom_installers/dev/nerdfont.py +0 -68
- machineconfig/jobs/python_custom_installers/dev/reverse_proxy.md +0 -31
- machineconfig/jobs/python_custom_installers/docker.py +0 -74
- machineconfig/jobs/python_custom_installers/warp-cli.py +0 -71
- machineconfig/jobs/python_generic_installers/config.json +0 -603
- machineconfig/jobs/python_generic_installers/config.json.bak +0 -414
- machineconfig/jobs/python_generic_installers/dev/config.archive.json +0 -18
- machineconfig/jobs/python_generic_installers/dev/config.json +0 -825
- machineconfig/jobs/python_generic_installers/dev/config.json.bak +0 -565
- machineconfig/jobs/python_linux_installers/__init__.py +0 -0
- machineconfig/jobs/python_linux_installers/archive/config.json +0 -18
- machineconfig/jobs/python_linux_installers/archive/config.json.bak +0 -10
- machineconfig/jobs/python_linux_installers/config.json +0 -145
- machineconfig/jobs/python_linux_installers/config.json.bak +0 -110
- machineconfig/jobs/python_linux_installers/dev/__init__.py +0 -0
- machineconfig/jobs/python_linux_installers/dev/config.json +0 -276
- machineconfig/jobs/python_linux_installers/dev/config.json.bak +0 -206
- machineconfig/jobs/python_windows_installers/__init__.py +0 -0
- machineconfig/jobs/python_windows_installers/archive/__init__.py +0 -0
- machineconfig/jobs/python_windows_installers/archive/file.json +0 -11
- machineconfig/jobs/python_windows_installers/config.json +0 -82
- machineconfig/jobs/python_windows_installers/config.json.bak +0 -56
- machineconfig/jobs/python_windows_installers/dev/__init__.py +0 -0
- machineconfig/jobs/python_windows_installers/dev/config.json +0 -4
- machineconfig/jobs/python_windows_installers/dev/config.json.bak +0 -3
- /machineconfig/jobs/{python_custom_installers → installer}/__init__.py +0 -0
- /machineconfig/jobs/{python_generic_installers → installer/custom_dev}/__init__.py +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/brave.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/docker_start.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/edge.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/nerdfont.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/pgsql.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/redis.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/timescaledb.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/vscode.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/warp-cli.sh +0 -0
- /machineconfig/jobs/{python_custom_installers/scripts/linux → installer/linux_scripts}/wezterm.sh +0 -0
- /machineconfig/{setup_windows/wt_and_pwsh → jobs/installer/powershell_scripts}/install_fonts.ps1 +0 -0
- {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/WHEEL +0 -0
- {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/entry_points.txt +0 -0
- {machineconfig-3.92.dist-info → machineconfig-3.94.dist-info}/top_level.txt +0 -0
|
@@ -3,12 +3,14 @@ from machineconfig.utils.installer_utils.installer_abc import find_move_delete_l
|
|
|
3
3
|
from machineconfig.utils.source_of_truth import INSTALL_TMP_DIR, INSTALL_VERSION_ROOT, LIBRARY_ROOT
|
|
4
4
|
from machineconfig.utils.options import check_tool_exists
|
|
5
5
|
from machineconfig.utils.io import read_json
|
|
6
|
-
from machineconfig.utils.schemas.installer.installer_types import InstallerData, InstallerDataFiles
|
|
6
|
+
from machineconfig.utils.schemas.installer.installer_types import InstallerData, InstallerDataFiles, get_os_name, get_normalized_arch
|
|
7
7
|
|
|
8
8
|
import platform
|
|
9
9
|
import subprocess
|
|
10
|
-
|
|
10
|
+
import json
|
|
11
|
+
from typing import Optional, Any
|
|
11
12
|
from pathlib import Path
|
|
13
|
+
from urllib.parse import urlparse
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class Installer:
|
|
@@ -16,23 +18,21 @@ class Installer:
|
|
|
16
18
|
self.installer_data: InstallerData = installer_data
|
|
17
19
|
|
|
18
20
|
def __repr__(self) -> str:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return f"Installer of {exe_name} {app_name} @ {repo_url}"
|
|
23
|
-
|
|
24
|
-
def get_description(self):
|
|
25
|
-
# old_version_cli = Terminal().run(f"{self.exe_name} --version").op.replace("\n", "")
|
|
26
|
-
# old_version_cli = os.system(f"{self.exe_name} --version").replace("\n", "")
|
|
27
|
-
exe_name = self.installer_data.get("exeName", "")
|
|
28
|
-
if not exe_name:
|
|
29
|
-
return "Invalid installer: missing exeName"
|
|
21
|
+
app_name = self.installer_data["appName"]
|
|
22
|
+
repo_url = self.installer_data["repoURL"]
|
|
23
|
+
return f"Installer of {app_name} @ {repo_url}"
|
|
30
24
|
|
|
25
|
+
def get_description(self) -> str:
|
|
26
|
+
exe_name = self._get_exe_name()
|
|
27
|
+
|
|
31
28
|
old_version_cli: bool = check_tool_exists(tool_name=exe_name)
|
|
32
29
|
old_version_cli_str = "✅" if old_version_cli else "❌"
|
|
33
|
-
|
|
34
|
-
doc = self.installer_data.get("doc", "No description")
|
|
30
|
+
doc = self.installer_data["doc"]
|
|
35
31
|
return f"{exe_name:<12} {old_version_cli_str} {doc}"
|
|
32
|
+
|
|
33
|
+
def _get_exe_name(self) -> str:
|
|
34
|
+
"""Derive executable name from app name by converting to lowercase and removing spaces."""
|
|
35
|
+
return self.installer_data["appName"].lower().replace(" ", "").replace("-", "")
|
|
36
36
|
|
|
37
37
|
@staticmethod
|
|
38
38
|
def choose_app_and_install():
|
|
@@ -63,14 +63,15 @@ class Installer:
|
|
|
63
63
|
raise ValueError(f"Could not find installer data for {app_name}")
|
|
64
64
|
|
|
65
65
|
installer = Installer(installer_data=selected_installer_data)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
exe_name = installer._get_exe_name()
|
|
67
|
+
print(f"📦 Selected application: {exe_name}")
|
|
68
|
+
version = input(f"📝 Enter version to install for {exe_name} [latest]: ") or None
|
|
69
|
+
print(f"\n{'=' * 80}\n🚀 INSTALLING {exe_name.upper()} 🚀\n{'=' * 80}")
|
|
69
70
|
installer.install(version=version)
|
|
70
71
|
|
|
71
|
-
def install_robust(self, version: Optional[str]):
|
|
72
|
+
def install_robust(self, version: Optional[str]) -> str:
|
|
72
73
|
try:
|
|
73
|
-
exe_name = self.
|
|
74
|
+
exe_name = self._get_exe_name()
|
|
74
75
|
print(f"\n{'=' * 80}\n🚀 INSTALLING {exe_name.upper()} 🚀\n{'=' * 80}")
|
|
75
76
|
result_old = subprocess.run(f"{exe_name} --version", shell=True, capture_output=True, text=True)
|
|
76
77
|
old_version_cli = result_old.stdout.strip()
|
|
@@ -90,67 +91,106 @@ class Installer:
|
|
|
90
91
|
return f"""📦️ 🤩 {exe_name} updated from {old_version_cli} ➡️ TO ➡️ {new_version_cli}"""
|
|
91
92
|
|
|
92
93
|
except Exception as ex:
|
|
93
|
-
exe_name = self.
|
|
94
|
-
app_name = self.installer_data
|
|
94
|
+
exe_name = self._get_exe_name()
|
|
95
|
+
app_name = self.installer_data["appName"]
|
|
95
96
|
print(f"❌ ERROR: Installation failed for {exe_name}: {ex}")
|
|
96
97
|
return f"""📦️ ❌ Failed to install `{app_name}` with error: {ex}"""
|
|
97
98
|
|
|
98
|
-
def install(self, version: Optional[str]):
|
|
99
|
-
exe_name = self.
|
|
100
|
-
repo_url = self.installer_data
|
|
99
|
+
def install(self, version: Optional[str]) -> None:
|
|
100
|
+
exe_name = self._get_exe_name()
|
|
101
|
+
repo_url = self.installer_data["repoURL"]
|
|
102
|
+
os_name = get_os_name()
|
|
103
|
+
arch = get_normalized_arch()
|
|
104
|
+
installer_arch_os = self.installer_data["fileNamePattern"][arch][os_name]
|
|
105
|
+
if installer_arch_os is None:
|
|
106
|
+
raise ValueError(f"No installation pattern for {exe_name} on {os_name} {arch}")
|
|
101
107
|
|
|
102
108
|
print(f"\n{'=' * 80}\n🔧 INSTALLATION PROCESS: {exe_name} 🔧\n{'=' * 80}")
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
print(f"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
109
|
+
version_to_be_installed: str = "unknown" # Initialize to ensure it's always bound
|
|
110
|
+
if repo_url == "CMD":
|
|
111
|
+
if "npm " in installer_arch_os or "pip " in installer_arch_os or "winget " in installer_arch_os:
|
|
112
|
+
package_manager = installer_arch_os.split(" ", maxsplit=1)[0]
|
|
113
|
+
print(f"📦 Using package manager: {package_manager}")
|
|
114
|
+
desc = package_manager + " installation"
|
|
115
|
+
version_to_be_installed = package_manager + "Latest"
|
|
116
|
+
print(f"🚀 Running: {installer_arch_os}")
|
|
117
|
+
result = subprocess.run(installer_arch_os, shell=True, capture_output=True, text=True)
|
|
118
|
+
success = result.returncode == 0 and result.stderr == ""
|
|
119
|
+
if not success:
|
|
120
|
+
print(f"❌ {desc} failed")
|
|
121
|
+
if result.stdout:
|
|
122
|
+
print(f"STDOUT: {result.stdout}")
|
|
123
|
+
if result.stderr:
|
|
124
|
+
print(f"STDERR: {result.stderr}")
|
|
125
|
+
print(f"Return code: {result.returncode}")
|
|
126
|
+
print(f"✅ Package manager installation completed\n{'=' * 80}")
|
|
127
|
+
elif installer_arch_os.endswith((".sh", ".py", ".ps1")):
|
|
128
|
+
# search for the script, see which path ends with the script name
|
|
129
|
+
import machineconfig.jobs.installer as module
|
|
130
|
+
from pathlib import Path
|
|
131
|
+
search_root = Path(module.__file__).parent
|
|
132
|
+
search_results = list(search_root.rglob(installer_arch_os))
|
|
133
|
+
if len(search_results) == 0:
|
|
134
|
+
raise FileNotFoundError(f"Could not find installation script: {installer_arch_os}")
|
|
135
|
+
elif len(search_results) > 1:
|
|
136
|
+
raise ValueError(f"Multiple installation scripts found for {installer_arch_os}: {search_results}")
|
|
137
|
+
installer_path = search_results[0]
|
|
138
|
+
print(f"📄 Found installation script: {installer_path}")
|
|
139
|
+
if installer_arch_os.endswith(".sh"):
|
|
140
|
+
if platform.system() not in ["Linux", "Darwin"]:
|
|
141
|
+
raise NotImplementedError(f"Shell script installation not supported on {platform.system()}")
|
|
142
|
+
print(f"🚀 Running shell script: {installer_path}")
|
|
143
|
+
subprocess.run(f"bash {installer_path}", shell=True, check=True)
|
|
144
|
+
version_to_be_installed = "scripted_installation"
|
|
145
|
+
print(f"✅ Shell script installation completed\n{'=' * 80}")
|
|
146
|
+
elif installer_arch_os.endswith(".ps1"):
|
|
147
|
+
if platform.system() != "Windows":
|
|
148
|
+
raise NotImplementedError(f"PowerShell script installation not supported on {platform.system()}")
|
|
149
|
+
print(f"🚀 Running PowerShell script: {installer_path}")
|
|
150
|
+
subprocess.run(f"powershell -ExecutionPolicy Bypass -File {installer_path}", shell=True, check=True)
|
|
151
|
+
version_to_be_installed = "scripted_installation"
|
|
152
|
+
print(f"✅ PowerShell script installation completed\n{'=' * 80}")
|
|
153
|
+
elif installer_arch_os.endswith(".py"):
|
|
154
|
+
import runpy
|
|
155
|
+
runpy.run_path(str(installer_path), run_name=None)["main"](self.installer_data, version=version)
|
|
156
|
+
version_to_be_installed = str(version)
|
|
157
|
+
print(f"✅ Custom installation completed\n{'=' * 80}")
|
|
158
|
+
elif installer_arch_os.startswith("https://"): # its a url to be downloaded
|
|
159
|
+
print(f"📥 Downloading object from URL: {installer_arch_os}")
|
|
160
|
+
downloaded_object = PathExtended(installer_arch_os).download(folder=INSTALL_TMP_DIR)
|
|
161
|
+
# object is either a zip containing a binary or a straight out binary.
|
|
162
|
+
if downloaded_object.suffix in [".zip", ".tar.gz"]:
|
|
163
|
+
print(f"📦 Decompressing downloaded archive: {downloaded_object}")
|
|
164
|
+
downloaded_object = downloaded_object.decompress()
|
|
165
|
+
print(f"✅ Decompression completed to: {downloaded_object}")
|
|
166
|
+
if downloaded_object.suffix in [".exe", ""]: # likely an executable
|
|
167
|
+
if platform.system() == "Windows":
|
|
168
|
+
print("🪟 Installing on Windows...")
|
|
169
|
+
exe = find_move_delete_windows(downloaded_file_path=downloaded_object, exe_name=exe_name, delete=True, rename_to=exe_name.replace(".exe", "") + ".exe")
|
|
170
|
+
elif platform.system() in ["Linux", "Darwin"]:
|
|
171
|
+
system_name = "Linux" if platform.system() == "Linux" else "macOS"
|
|
172
|
+
print(f"🐧 Installing on {system_name}...")
|
|
173
|
+
exe = find_move_delete_linux(downloaded=downloaded_object, tool_name=exe_name, delete=True, rename_to=exe_name)
|
|
174
|
+
else:
|
|
175
|
+
error_msg = f"❌ ERROR: System {platform.system()} not supported"
|
|
176
|
+
print(error_msg)
|
|
177
|
+
raise NotImplementedError(error_msg)
|
|
178
|
+
|
|
179
|
+
_ = exe
|
|
180
|
+
if exe.name.replace(".exe", "") != exe_name.replace(".exe", ""):
|
|
181
|
+
from rich import print as pprint
|
|
182
|
+
from rich.panel import Panel
|
|
183
|
+
print("⚠️ Warning: Executable name mismatch")
|
|
184
|
+
pprint(Panel(f"Expected exe name: [red]{exe_name}[/red] \nAttained name: [red]{exe.name.replace('.exe', '')}[/red]", title="exe name mismatch", subtitle=repo_url))
|
|
185
|
+
new_exe_name = exe_name + ".exe" if platform.system() == "Windows" else exe_name
|
|
186
|
+
print(f"🔄 Renaming to correct name: {new_exe_name}")
|
|
187
|
+
exe.with_name(name=new_exe_name, inplace=True, overwrite=True)
|
|
188
|
+
version_to_be_installed = "downloaded_binary"
|
|
189
|
+
print(f"✅ Downloaded binary installation completed\n{'=' * 80}")
|
|
129
190
|
else:
|
|
130
|
-
|
|
131
|
-
full_command = f"{start_cmd} {script_file}"
|
|
132
|
-
subprocess.run(full_command, stdin=None, stdout=None, stderr=None, shell=True, text=True)
|
|
133
|
-
version_to_be_installed = str(version)
|
|
134
|
-
print(f"✅ Custom installation completed\n{'=' * 80}")
|
|
135
|
-
|
|
136
|
-
elif "npm " in repo_url or "pip " in repo_url or "winget " in repo_url:
|
|
137
|
-
package_manager = repo_url.split(" ", maxsplit=1)[0]
|
|
138
|
-
print(f"📦 Using package manager: {package_manager}")
|
|
139
|
-
desc = package_manager + " installation"
|
|
140
|
-
version_to_be_installed = package_manager + "Latest"
|
|
141
|
-
print(f"🚀 Running: {repo_url}")
|
|
142
|
-
result = subprocess.run(repo_url, shell=True, capture_output=True, text=True)
|
|
143
|
-
success = result.returncode == 0 and result.stderr == ""
|
|
144
|
-
if not success:
|
|
145
|
-
print(f"❌ {desc} failed")
|
|
146
|
-
if result.stdout:
|
|
147
|
-
print(f"STDOUT: {result.stdout}")
|
|
148
|
-
if result.stderr:
|
|
149
|
-
print(f"STDERR: {result.stderr}")
|
|
150
|
-
print(f"Return code: {result.returncode}")
|
|
151
|
-
print(f"✅ Package manager installation completed\n{'=' * 80}")
|
|
152
|
-
|
|
191
|
+
raise NotImplementedError(f"CMD installation method not implemented for: {installer_arch_os}")
|
|
153
192
|
else:
|
|
193
|
+
assert repo_url.startswith("https://github.com/"), f"repoURL must be a GitHub URL, got {repo_url}"
|
|
154
194
|
print("📥 Downloading from repository...")
|
|
155
195
|
downloaded, version_to_be_installed = self.download(version=version)
|
|
156
196
|
if str(downloaded).endswith(".deb"):
|
|
@@ -195,55 +235,39 @@ class Installer:
|
|
|
195
235
|
|
|
196
236
|
print(f"💾 Saving version information to: {INSTALL_VERSION_ROOT.joinpath(exe_name)}")
|
|
197
237
|
INSTALL_VERSION_ROOT.joinpath(exe_name).parent.mkdir(parents=True, exist_ok=True)
|
|
198
|
-
INSTALL_VERSION_ROOT.joinpath(exe_name).write_text(version_to_be_installed, encoding="utf-8")
|
|
238
|
+
INSTALL_VERSION_ROOT.joinpath(exe_name).write_text(version_to_be_installed or "unknown", encoding="utf-8")
|
|
199
239
|
print(f"✅ Installation completed successfully!\n{'=' * 80}")
|
|
200
240
|
|
|
201
|
-
def download(self, version: Optional[str]):
|
|
202
|
-
exe_name = self.
|
|
203
|
-
repo_url = self.installer_data
|
|
204
|
-
app_name = self.installer_data
|
|
205
|
-
strip_v = self.installer_data.get("stripVersion", False)
|
|
206
|
-
|
|
241
|
+
def download(self, version: Optional[str]) -> tuple[PathExtended, str]:
|
|
242
|
+
exe_name = self._get_exe_name()
|
|
243
|
+
repo_url = self.installer_data["repoURL"]
|
|
244
|
+
app_name = self.installer_data["appName"]
|
|
207
245
|
print(f"\n{'=' * 80}\n📥 DOWNLOADING: {exe_name} 📥\n{'=' * 80}")
|
|
208
|
-
|
|
246
|
+
|
|
247
|
+
download_link: Optional[str] = None
|
|
209
248
|
version_to_be_installed: Optional[str] = None
|
|
249
|
+
|
|
210
250
|
if "github" not in repo_url or ".zip" in repo_url or ".tar.gz" in repo_url:
|
|
211
|
-
|
|
251
|
+
# Direct download URL
|
|
252
|
+
download_link = repo_url
|
|
212
253
|
version_to_be_installed = "predefined_url"
|
|
213
254
|
print(f"🔗 Using direct download URL: {download_link}")
|
|
214
255
|
print(f"📦 Version to be installed: {version_to_be_installed}")
|
|
215
|
-
elif self._any_direct_http_template():
|
|
216
|
-
template, arch = self._select_template()
|
|
217
|
-
if not template.startswith("http"):
|
|
218
|
-
# Fall back to github-style handling below
|
|
219
|
-
pass
|
|
220
|
-
else:
|
|
221
|
-
download_link = Path(template)
|
|
222
|
-
version_to_be_installed = "predefined_url"
|
|
223
|
-
system_name = self._system_name()
|
|
224
|
-
print(f"🧭 Detected system={system_name} arch={arch}")
|
|
225
|
-
print(f"🔗 Using architecture-specific direct URL: {download_link}")
|
|
226
|
-
print(f"📦 Version to be installed: {version_to_be_installed}")
|
|
227
|
-
# continue to unified download logic below
|
|
228
|
-
|
|
229
256
|
else:
|
|
257
|
+
# GitHub repository
|
|
230
258
|
print("🌐 Retrieving release information from GitHub...")
|
|
231
|
-
|
|
259
|
+
arch = get_normalized_arch()
|
|
260
|
+
os_name = get_os_name()
|
|
261
|
+
print(f"🧭 Detected system={os_name} arch={arch}")
|
|
262
|
+
|
|
263
|
+
# Use existing get_github_release method to get download link and version
|
|
264
|
+
download_link, version_to_be_installed = self.get_github_release(repo_url, version)
|
|
265
|
+
|
|
266
|
+
if download_link is None:
|
|
267
|
+
raise ValueError(f"Could not retrieve download link for {exe_name} version {version or 'latest'}")
|
|
268
|
+
|
|
232
269
|
print(f"📦 Version to be installed: {version_to_be_installed}")
|
|
233
|
-
print(f"
|
|
234
|
-
|
|
235
|
-
version_to_be_installed_stripped = version_to_be_installed.replace("v", "") if strip_v else version_to_be_installed
|
|
236
|
-
version_to_be_installed_stripped = version_to_be_installed_stripped.replace("ipinfo-", "")
|
|
237
|
-
|
|
238
|
-
template, arch = self._select_template()
|
|
239
|
-
system_name = self._system_name()
|
|
240
|
-
file_name = template.format(version_to_be_installed_stripped)
|
|
241
|
-
print(f"🧭 Detected system={system_name} arch={arch}")
|
|
242
|
-
print(f"📄 Using template: {template}")
|
|
243
|
-
print(f"🗂️ Resolved file name: {file_name}")
|
|
244
|
-
|
|
245
|
-
print(f"📄 File name: {file_name}")
|
|
246
|
-
download_link = release_url.joinpath(file_name)
|
|
270
|
+
print(f"🔗 Download URL: {download_link}")
|
|
247
271
|
|
|
248
272
|
assert download_link is not None, "download_link must be set"
|
|
249
273
|
assert version_to_be_installed is not None, "version_to_be_installed must be set"
|
|
@@ -253,100 +277,89 @@ class Installer:
|
|
|
253
277
|
return downloaded, version_to_be_installed
|
|
254
278
|
|
|
255
279
|
# --------------------------- Arch / template helpers ---------------------------
|
|
256
|
-
def _normalized_arch(self) -> str:
|
|
257
|
-
arch_raw = platform.machine().lower()
|
|
258
|
-
if arch_raw in ("x86_64", "amd64"):
|
|
259
|
-
return "amd64"
|
|
260
|
-
if arch_raw in ("aarch64", "arm64", "armv8", "armv8l"):
|
|
261
|
-
return "arm64"
|
|
262
|
-
return arch_raw
|
|
263
|
-
|
|
264
|
-
def _system_name(self) -> str:
|
|
265
|
-
sys_ = platform.system()
|
|
266
|
-
if sys_ == "Darwin":
|
|
267
|
-
return "macOS"
|
|
268
|
-
return sys_
|
|
269
|
-
|
|
270
|
-
def _any_direct_http_template(self) -> bool:
|
|
271
|
-
filename_templates = self.installer_data.get("filenameTemplate", {})
|
|
272
|
-
templates: list[str] = []
|
|
273
|
-
|
|
274
|
-
for arch_templates in filename_templates.values():
|
|
275
|
-
templates.extend([t for t in arch_templates.values() if t])
|
|
276
|
-
|
|
277
|
-
return any(t for t in templates if t.startswith("http"))
|
|
278
|
-
|
|
279
|
-
def _select_template(self) -> tuple[str, str]:
|
|
280
|
-
sys_name = platform.system()
|
|
281
|
-
arch = self._normalized_arch()
|
|
282
|
-
|
|
283
|
-
filename_templates = self.installer_data.get("filenameTemplate", {})
|
|
284
|
-
|
|
285
|
-
# Get templates for each architecture
|
|
286
|
-
amd64_templates = filename_templates.get("amd64", {})
|
|
287
|
-
arm64_templates = filename_templates.get("arm64", {})
|
|
288
|
-
|
|
289
|
-
# mapping logic
|
|
290
|
-
candidates: list[str] = []
|
|
291
|
-
template: Optional[str] = None
|
|
292
|
-
|
|
293
|
-
if sys_name == "Windows":
|
|
294
|
-
if arch == "arm64" and arm64_templates.get("windows"):
|
|
295
|
-
template = arm64_templates["windows"]
|
|
296
|
-
else:
|
|
297
|
-
template = amd64_templates.get("windows", "")
|
|
298
|
-
candidates = ["arm64.windows", "amd64.windows"]
|
|
299
|
-
elif sys_name == "Linux":
|
|
300
|
-
if arch == "arm64" and arm64_templates.get("linux"):
|
|
301
|
-
template = arm64_templates["linux"]
|
|
302
|
-
else:
|
|
303
|
-
template = amd64_templates.get("linux", "")
|
|
304
|
-
candidates = ["arm64.linux", "amd64.linux"]
|
|
305
|
-
elif sys_name == "Darwin":
|
|
306
|
-
if arch == "arm64" and arm64_templates.get("macos"):
|
|
307
|
-
template = arm64_templates["macos"]
|
|
308
|
-
elif arch == "amd64" and amd64_templates.get("macos"):
|
|
309
|
-
template = amd64_templates["macos"]
|
|
310
|
-
else:
|
|
311
|
-
# fallback between available mac templates
|
|
312
|
-
template = arm64_templates.get("macos") or amd64_templates.get("macos") or ""
|
|
313
|
-
candidates = ["arm64.macos", "amd64.macos"]
|
|
314
|
-
else:
|
|
315
|
-
raise NotImplementedError(f"System {sys_name} not supported")
|
|
316
|
-
|
|
317
|
-
if not template:
|
|
318
|
-
raise ValueError(f"No filename template available for system={sys_name} arch={arch}. Checked {candidates}")
|
|
319
|
-
|
|
320
|
-
return template, arch
|
|
321
280
|
|
|
322
281
|
@staticmethod
|
|
323
|
-
def
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
_latest_version = requests.get(str(repo_url) + "/releases/latest", timeout=10).url.split("/")[
|
|
333
|
-
-1
|
|
334
|
-
] # this is to resolve the redirection that occures: https://stackoverflow.com/questions/36070821/how-to-get-redirect-url-using-python-requests
|
|
335
|
-
version_to_be_installed = _latest_version
|
|
336
|
-
print(f"✅ Latest version detected: {version_to_be_installed}")
|
|
337
|
-
# print(version_to_be_installed)
|
|
338
|
-
else:
|
|
339
|
-
version_to_be_installed = version
|
|
340
|
-
print(f"📝 Using specified version: {version_to_be_installed}")
|
|
282
|
+
def _get_repo_name_from_url(repo_url: str) -> str:
|
|
283
|
+
"""Extract owner/repo from GitHub URL."""
|
|
284
|
+
try:
|
|
285
|
+
parsed = urlparse(repo_url)
|
|
286
|
+
path_parts = parsed.path.strip("/").split("/")
|
|
287
|
+
return f"{path_parts[0]}/{path_parts[1]}"
|
|
288
|
+
except (IndexError, AttributeError):
|
|
289
|
+
return ""
|
|
341
290
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
291
|
+
@staticmethod
|
|
292
|
+
def _fetch_github_release_data(repo_name: str, version: Optional[str] = None) -> Optional[dict[str, Any]]:
|
|
293
|
+
"""Fetch release data from GitHub API using requests."""
|
|
294
|
+
import requests
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
if version and version.lower() != "latest":
|
|
298
|
+
# Fetch specific version
|
|
299
|
+
url = f"https://api.github.com/repos/{repo_name}/releases/tags/{version}"
|
|
300
|
+
else:
|
|
301
|
+
# Fetch latest release
|
|
302
|
+
url = f"https://api.github.com/repos/{repo_name}/releases/latest"
|
|
303
|
+
|
|
304
|
+
response = requests.get(url, timeout=30)
|
|
305
|
+
|
|
306
|
+
if response.status_code != 200:
|
|
307
|
+
print(f"❌ Failed to fetch data for {repo_name}: HTTP {response.status_code}")
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
response_data = response.json()
|
|
311
|
+
|
|
312
|
+
# Check if API returned an error
|
|
313
|
+
if "message" in response_data:
|
|
314
|
+
if "API rate limit exceeded" in response_data.get("message", ""):
|
|
315
|
+
print(f"🚫 Rate limit exceeded for {repo_name}")
|
|
316
|
+
return None
|
|
317
|
+
elif "Not Found" in response_data.get("message", ""):
|
|
318
|
+
print(f"🔍 No releases found for {repo_name}")
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
return response_data
|
|
322
|
+
|
|
323
|
+
except (requests.RequestException, requests.Timeout, json.JSONDecodeError) as e:
|
|
324
|
+
print(f"❌ Error fetching {repo_name}: {e}")
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
def get_github_release(self, repo_url: str, version: Optional[str]) -> tuple[Optional[str], Optional[str]]:
|
|
328
|
+
"""
|
|
329
|
+
Get download link and version from GitHub release based on fileNamePattern.
|
|
330
|
+
Returns (download_url, actual_version)
|
|
331
|
+
"""
|
|
332
|
+
arch = get_normalized_arch()
|
|
333
|
+
os_name = get_os_name()
|
|
334
|
+
filename_pattern = self.installer_data["fileNamePattern"][arch][os_name]
|
|
335
|
+
if filename_pattern is None:
|
|
336
|
+
raise ValueError(f"No fileNamePattern for {self._get_exe_name()} on {os_name} {arch}")
|
|
337
|
+
repo_name = self._get_repo_name_from_url(repo_url)
|
|
338
|
+
if not repo_name:
|
|
339
|
+
print(f"❌ Invalid repository URL: {repo_url}")
|
|
340
|
+
return None, None
|
|
341
|
+
release_data = self._fetch_github_release_data(repo_name, version)
|
|
342
|
+
if not release_data:
|
|
343
|
+
return None, None
|
|
344
|
+
# print(release_data)
|
|
345
|
+
actual_version = release_data.get("tag_name", "unknown")
|
|
346
|
+
filename = filename_pattern.format(version=actual_version)
|
|
347
|
+
|
|
348
|
+
available_filenames: list[str] = []
|
|
349
|
+
for asset in release_data.get("assets", []):
|
|
350
|
+
an_dl = asset.get("browser_download_url", "NA")
|
|
351
|
+
available_filenames.append(an_dl.split("/")[-1])
|
|
352
|
+
if filename not in available_filenames:
|
|
353
|
+
filename = filename_pattern.format(version=actual_version.replace("v", ""))
|
|
354
|
+
if filename not in available_filenames:
|
|
355
|
+
print(f"❌ Filename {filename} not found in assets: {available_filenames}")
|
|
356
|
+
return None, None
|
|
357
|
+
browser_download_url = f"{repo_url}/releases/download/{actual_version}/{filename}"
|
|
358
|
+
return browser_download_url, actual_version
|
|
345
359
|
|
|
346
360
|
@staticmethod
|
|
347
|
-
def check_if_installed_already(exe_name: str, version: str, use_cache: bool):
|
|
361
|
+
def check_if_installed_already(exe_name: str, version: Optional[str], use_cache: bool) -> tuple[str, str, str]:
|
|
348
362
|
print(f"\n{'=' * 80}\n🔍 CHECKING INSTALLATION STATUS: {exe_name} 🔍\n{'=' * 80}")
|
|
349
|
-
version_to_be_installed = version
|
|
350
363
|
INSTALL_VERSION_ROOT.joinpath(exe_name).parent.mkdir(parents=True, exist_ok=True)
|
|
351
364
|
tmp_path = INSTALL_VERSION_ROOT.joinpath(exe_name)
|
|
352
365
|
|
|
@@ -368,18 +381,18 @@ class Installer:
|
|
|
368
381
|
existing_version = result.stdout.strip()
|
|
369
382
|
print(f"📄 Detected installed version: {existing_version}")
|
|
370
383
|
|
|
371
|
-
if existing_version is not None:
|
|
372
|
-
if existing_version ==
|
|
373
|
-
print(f"✅ {exe_name} is up to date (version {
|
|
384
|
+
if existing_version is not None and version is not None:
|
|
385
|
+
if existing_version == version:
|
|
386
|
+
print(f"✅ {exe_name} is up to date (version {version})")
|
|
374
387
|
print(f"📂 Version information stored at: {INSTALL_VERSION_ROOT}")
|
|
375
|
-
return ("✅ Up to date", version.strip(),
|
|
388
|
+
return ("✅ Up to date", version.strip(), version.strip())
|
|
376
389
|
else:
|
|
377
|
-
print(f"🔄 {exe_name} needs update: {existing_version.rstrip()} → {
|
|
378
|
-
tmp_path.write_text(
|
|
379
|
-
return ("❌ Outdated", existing_version.strip(),
|
|
390
|
+
print(f"🔄 {exe_name} needs update: {existing_version.rstrip()} → {version}")
|
|
391
|
+
tmp_path.write_text(version, encoding="utf-8")
|
|
392
|
+
return ("❌ Outdated", existing_version.strip(), version.strip())
|
|
380
393
|
else:
|
|
381
|
-
print(f"📦 {exe_name} is not installed. Will install version: {
|
|
382
|
-
tmp_path.write_text(
|
|
394
|
+
print(f"📦 {exe_name} is not installed. Will install version: {version}")
|
|
395
|
+
# tmp_path.write_text(version, encoding="utf-8")
|
|
383
396
|
|
|
384
397
|
print(f"{'=' * 80}")
|
|
385
|
-
return ("⚠️ NotInstalled", "None",
|
|
398
|
+
return ("⚠️ NotInstalled", "None", version or "unknown")
|
|
@@ -1,20 +1,43 @@
|
|
|
1
|
-
from typing import TypedDict, Literal, TypeAlias
|
|
1
|
+
from typing import TypedDict, Literal, TypeAlias, Optional
|
|
2
|
+
import platform
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
APP_INSTALLER_CATEGORY: TypeAlias = Literal["
|
|
5
|
+
APP_INSTALLER_CATEGORY: TypeAlias = Literal["GITHUB_ESSENTIAL", "CUSTOM_ESSENTIAL", "GITHUB_DEV", "CUSTOM_DEV"]
|
|
5
6
|
CPU_ARCHITECTURES: TypeAlias = Literal["amd64", "arm64"]
|
|
6
7
|
OPERATING_SYSTEMS: TypeAlias = Literal["windows", "linux", "macos"]
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class InstallerData(TypedDict):
|
|
10
11
|
appName: str
|
|
11
|
-
repoURL: str
|
|
12
12
|
doc: str
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
exeName: str
|
|
13
|
+
repoURL: str
|
|
14
|
+
fileNamePattern: dict[CPU_ARCHITECTURES, dict[OPERATING_SYSTEMS, Optional[str]]]
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class InstallerDataFiles(TypedDict):
|
|
19
18
|
version: str
|
|
20
19
|
installers: list[InstallerData]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_os_name() -> OPERATING_SYSTEMS:
|
|
23
|
+
"""Get the operating system name in the format expected by the github parser."""
|
|
24
|
+
sys_name = platform.system()
|
|
25
|
+
if sys_name == "Windows":
|
|
26
|
+
return "windows"
|
|
27
|
+
elif sys_name == "Linux":
|
|
28
|
+
return "linux"
|
|
29
|
+
elif sys_name == "Darwin":
|
|
30
|
+
return "macos"
|
|
31
|
+
else:
|
|
32
|
+
raise NotImplementedError(f"System {sys_name} not supported")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_normalized_arch() -> CPU_ARCHITECTURES:
|
|
36
|
+
"""Get the normalized CPU architecture."""
|
|
37
|
+
arch_raw = platform.machine().lower()
|
|
38
|
+
if arch_raw in ("x86_64", "amd64"):
|
|
39
|
+
return "amd64"
|
|
40
|
+
if arch_raw in ("aarch64", "arm64", "armv8", "armv8l"):
|
|
41
|
+
return "arm64"
|
|
42
|
+
# Default to amd64 if unknown architecture
|
|
43
|
+
return "amd64"
|