Archived
205 lines
6.0 KiB
Python
205 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LM Studio PKGBUILD Maintenance Engine
|
|
|
|
Resolves dynamic upstream link, extracts version, calculates SHA256,
|
|
and generates PKGBUILD from template.
|
|
|
|
Updated to handle:
|
|
- AppImage file with calculated sha256
|
|
- .desktop file with 'SKIP' (locally managed)
|
|
- Static icon (lmstudio.png) with calculated sha256
|
|
- .install file staging and template rendering
|
|
"""
|
|
|
|
import re
|
|
import hashlib
|
|
import requests
|
|
import subprocess
|
|
import tarfile
|
|
from pathlib import Path
|
|
from typing import Tuple, List
|
|
|
|
|
|
def resolve_download_url(redirect_url: str = "https://lmstudio.ai/download/latest/linux/x64") -> str:
|
|
"""
|
|
Follow redirects to resolve the final download URL.
|
|
Uses HEAD request to avoid downloading the full file.
|
|
"""
|
|
print(f"Resolving download URL: {redirect_url}")
|
|
|
|
session = requests.Session()
|
|
response = session.head(redirect_url, allow_redirects=True)
|
|
response.raise_for_status()
|
|
|
|
final_url = response.url
|
|
print(f"Resolved URL: {final_url}")
|
|
return final_url
|
|
|
|
|
|
def extract_version(filename: str) -> str:
|
|
"""
|
|
Extract semantic version from filename using regex.
|
|
Expected format: LM-Studio-X.Y.Z-x64.AppImage or LM-Studio-X.Y.Z-R-x64.AppImage
|
|
Extracts only X.Y.Z portion (pkgrel handled separately in PKGBUILD).
|
|
"""
|
|
match = re.search(r'LM-Studio-([\d.]+)-(?:\d+-)?x64\.AppImage', filename)
|
|
if match:
|
|
return match.group(1)
|
|
raise ValueError(f"Could not extract version from filename: {filename}")
|
|
|
|
|
|
def calculate_sha256(file_path: str) -> str:
|
|
"""
|
|
Calculate SHA256 checksum of a file.
|
|
Handles both AppImage and icon files with identical hashing logic.
|
|
"""
|
|
print(f"Calculating SHA256 for: {file_path}")
|
|
|
|
sha256_hash = hashlib.sha256()
|
|
with open(file_path, "rb") as f:
|
|
for byte_block in iter(lambda: f.read(4096), b""):
|
|
sha256_hash.update(byte_block)
|
|
|
|
checksum = sha256_hash.hexdigest()
|
|
print(f"SHA256: {checksum}")
|
|
return checksum
|
|
|
|
|
|
def download_file(url: str, file_path: str) -> None:
|
|
"""
|
|
Download file from URL if it doesn't already exist.
|
|
"""
|
|
path = Path(file_path)
|
|
if path.exists():
|
|
print(f"File already exists: {file_path}")
|
|
return
|
|
|
|
print(f"Downloading: {url}")
|
|
response = requests.get(url, stream=True)
|
|
response.raise_for_status()
|
|
|
|
with open(file_path, "wb") as f:
|
|
for chunk in response.iter_content(chunk_size=8192):
|
|
if chunk:
|
|
f.write(chunk)
|
|
|
|
print(f"Downloaded: {file_path}")
|
|
|
|
|
|
def stage_install_file() -> None:
|
|
"""
|
|
Ensure lmstudio-bin.install file exists and is staged.
|
|
"""
|
|
install_path = Path("/home/madgoat/Documents/LMStudio-bin Aur/lmstudio-bin.install")
|
|
|
|
if install_path.exists():
|
|
print(f"✓ Install file present: {install_path}")
|
|
else:
|
|
print(f"Warning: Install file not found at {install_path}")
|
|
|
|
|
|
def render_template(template_path: str, output_path: str, version: str, url: str, sha256: str, icon_sha256: str) -> None:
|
|
"""
|
|
Render PKGBUILD from template by replacing placeholders.
|
|
|
|
Placeholders:
|
|
- {{version}}: Semantic version extracted from filename
|
|
- {{url}}: Resolved download URL
|
|
- {{sha256}}: SHA256 checksum of the AppImage
|
|
- {{icon_sha256}}: SHA256 checksum of the static icon (lmstudio.png)
|
|
|
|
The template includes sha256sums array with appimage hash, icon hash, and SKIP for .desktop file.
|
|
"""
|
|
print(f"Rendering template: {template_path} -> {output_path}")
|
|
|
|
with open(template_path, "r") as f:
|
|
content = f.read()
|
|
|
|
content = content.replace("{{version}}", version)
|
|
content = content.replace("{{url}}", url)
|
|
content = content.replace("{{sha256}}", sha256)
|
|
content = content.replace("{{icon_sha256}}", icon_sha256)
|
|
|
|
with open(output_path, "w") as f:
|
|
f.write(content)
|
|
|
|
print(f"Generated: {output_path}")
|
|
|
|
|
|
def generate_srcinfo() -> None:
|
|
"""
|
|
Generate .SRCINFO using makepkg --printsrcinfo.
|
|
"""
|
|
print("Generating .SRCINFO...")
|
|
|
|
result = subprocess.run(
|
|
["makepkg", "--printsrcinfo"],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd="/home/madgoat/Documents/LMStudio-bin Aur"
|
|
)
|
|
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"makepkg failed: {result.stderr}")
|
|
|
|
with open("/home/madgoat/Documents/LMStudio-bin Aur/.SRCINFO", "w") as f:
|
|
f.write(result.stdout)
|
|
|
|
print("Generated: .SRCINFO")
|
|
|
|
|
|
def main():
|
|
"""
|
|
Main maintenance engine workflow.
|
|
"""
|
|
print("=== LM Studio PKGBUILD Maintenance Engine ===\n")
|
|
|
|
# Change to workspace directory
|
|
import os
|
|
os.chdir("/home/madgoat/Documents/LMStudio-bin Aur")
|
|
|
|
# Step 1: Resolve download URL
|
|
final_url = resolve_download_url()
|
|
|
|
# Step 2: Extract filename and version
|
|
filename = final_url.split("/")[-1]
|
|
version = extract_version(filename)
|
|
print(f"Detected version: {version}\n")
|
|
|
|
# Step 3: Download file if missing
|
|
download_file(final_url, filename)
|
|
|
|
# Step 4: Calculate SHA256 for AppImage
|
|
sha256 = calculate_sha256(filename)
|
|
print()
|
|
|
|
# Step 5: Calculate SHA256 for static icon (lmstudio.png)
|
|
# Uses identical hashing logic as AppImage
|
|
icon_hash = calculate_sha256("lmstudio.png")
|
|
print()
|
|
|
|
# Step 6: Stage install file
|
|
stage_install_file()
|
|
print()
|
|
|
|
# Step 7: Render PKGBUILD from template
|
|
# The template uses {{sha256}} for the AppImage hash and {{icon_sha256}} for icon hash
|
|
# and the sha256sums array in the template includes both hashes plus 'SKIP' for .desktop file
|
|
render_template("PKGBUILD.template", "PKGBUILD", version, final_url, sha256, icon_hash)
|
|
print()
|
|
|
|
# Step 8: Generate .SRCINFO
|
|
generate_srcinfo()
|
|
|
|
print("\n=== Maintenance Engine Complete ===")
|
|
print(f"Version: {version}")
|
|
print(f"URL: {final_url}")
|
|
print(f"AppImage SHA256: {sha256}")
|
|
print(f"Icon SHA256: {icon_hash}")
|
|
print(f"SHA256sums: ('{sha256}' '{icon_hash}' 'SKIP')")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|