summarylogtreecommitdiffstats
path: root/update_package.py
blob: 37bc7366e375ea8b15a25226027f01699842ea9a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/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()