uluka 0.1.0__tar.gz
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.
- uluka-0.1.0/PKG-INFO +38 -0
- uluka-0.1.0/README.md +25 -0
- uluka-0.1.0/pyproject.toml +27 -0
- uluka-0.1.0/setup.cfg +4 -0
- uluka-0.1.0/src/uluka.egg-info/PKG-INFO +38 -0
- uluka-0.1.0/src/uluka.egg-info/SOURCES.txt +11 -0
- uluka-0.1.0/src/uluka.egg-info/dependency_links.txt +1 -0
- uluka-0.1.0/src/uluka.egg-info/entry_points.txt +2 -0
- uluka-0.1.0/src/uluka.egg-info/top_level.txt +1 -0
- uluka-0.1.0/src/ulukalabs/__init__.py +2 -0
- uluka-0.1.0/src/ulukalabs/__main__.py +5 -0
- uluka-0.1.0/src/ulukalabs/cli.py +352 -0
- uluka-0.1.0/tests/test_cli.py +165 -0
uluka-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uluka
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Install and open the Uluka Mac app (owner-approved accounts)
|
|
5
|
+
Author: UlukaLabs
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://ulukalabs.com
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Environment :: MacOS X
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# uluka
|
|
15
|
+
|
|
16
|
+
The Uluka terminal: a coding and data agent running the Uluka 26B model on your
|
|
17
|
+
own Mac, for owner-approved accounts.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pipx install uluka # or: python3 -m pip install uluka
|
|
21
|
+
uluka
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The first time, `uluka` opens your browser to connect this Mac: sign in with an
|
|
25
|
+
approved account, check that the device code matches, and choose
|
|
26
|
+
**Allow this Mac**. It then downloads the signed, encrypted release once
|
|
27
|
+
(about 9 GB; an interrupted download continues where it stopped), checks it and
|
|
28
|
+
opens the Uluka terminal in the same window. Later runs check that your account
|
|
29
|
+
is still approved and open the terminal; new releases install themselves.
|
|
30
|
+
`uluka login` signs this Mac in again.
|
|
31
|
+
|
|
32
|
+
Requirements: an Apple silicon Mac with macOS 14 or newer, 16 GB of memory and
|
|
33
|
+
about 20 GB of free space while installing. Access requires an approved
|
|
34
|
+
account: request it at https://ulukalabs.com/app.
|
|
35
|
+
|
|
36
|
+
This package contains only the sign-in and setup. The terminal and the model
|
|
37
|
+
arrive in the signed release; the model stays encrypted on disk and is unlocked
|
|
38
|
+
only while your account is approved.
|
uluka-0.1.0/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# uluka
|
|
2
|
+
|
|
3
|
+
The Uluka terminal: a coding and data agent running the Uluka 26B model on your
|
|
4
|
+
own Mac, for owner-approved accounts.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
pipx install uluka # or: python3 -m pip install uluka
|
|
8
|
+
uluka
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The first time, `uluka` opens your browser to connect this Mac: sign in with an
|
|
12
|
+
approved account, check that the device code matches, and choose
|
|
13
|
+
**Allow this Mac**. It then downloads the signed, encrypted release once
|
|
14
|
+
(about 9 GB; an interrupted download continues where it stopped), checks it and
|
|
15
|
+
opens the Uluka terminal in the same window. Later runs check that your account
|
|
16
|
+
is still approved and open the terminal; new releases install themselves.
|
|
17
|
+
`uluka login` signs this Mac in again.
|
|
18
|
+
|
|
19
|
+
Requirements: an Apple silicon Mac with macOS 14 or newer, 16 GB of memory and
|
|
20
|
+
about 20 GB of free space while installing. Access requires an approved
|
|
21
|
+
account: request it at https://ulukalabs.com/app.
|
|
22
|
+
|
|
23
|
+
This package contains only the sign-in and setup. The terminal and the model
|
|
24
|
+
arrive in the signed release; the model stays encrypted on disk and is unlocked
|
|
25
|
+
only while your account is approved.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "uluka"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Install and open the Uluka Mac app (owner-approved accounts)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [{ name = "UlukaLabs" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Operating System :: MacOS",
|
|
16
|
+
"Environment :: MacOS X",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://ulukalabs.com"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
uluka = "ulukalabs.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.dynamic]
|
|
27
|
+
version = { attr = "ulukalabs.__version__" }
|
uluka-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uluka
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Install and open the Uluka Mac app (owner-approved accounts)
|
|
5
|
+
Author: UlukaLabs
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://ulukalabs.com
|
|
8
|
+
Classifier: Operating System :: MacOS
|
|
9
|
+
Classifier: Environment :: MacOS X
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# uluka
|
|
15
|
+
|
|
16
|
+
The Uluka terminal: a coding and data agent running the Uluka 26B model on your
|
|
17
|
+
own Mac, for owner-approved accounts.
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pipx install uluka # or: python3 -m pip install uluka
|
|
21
|
+
uluka
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The first time, `uluka` opens your browser to connect this Mac: sign in with an
|
|
25
|
+
approved account, check that the device code matches, and choose
|
|
26
|
+
**Allow this Mac**. It then downloads the signed, encrypted release once
|
|
27
|
+
(about 9 GB; an interrupted download continues where it stopped), checks it and
|
|
28
|
+
opens the Uluka terminal in the same window. Later runs check that your account
|
|
29
|
+
is still approved and open the terminal; new releases install themselves.
|
|
30
|
+
`uluka login` signs this Mac in again.
|
|
31
|
+
|
|
32
|
+
Requirements: an Apple silicon Mac with macOS 14 or newer, 16 GB of memory and
|
|
33
|
+
about 20 GB of free space while installing. Access requires an approved
|
|
34
|
+
account: request it at https://ulukalabs.com/app.
|
|
35
|
+
|
|
36
|
+
This package contains only the sign-in and setup. The terminal and the model
|
|
37
|
+
arrive in the signed release; the model stays encrypted on disk and is unlocked
|
|
38
|
+
only while your account is approved.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/uluka.egg-info/PKG-INFO
|
|
4
|
+
src/uluka.egg-info/SOURCES.txt
|
|
5
|
+
src/uluka.egg-info/dependency_links.txt
|
|
6
|
+
src/uluka.egg-info/entry_points.txt
|
|
7
|
+
src/uluka.egg-info/top_level.txt
|
|
8
|
+
src/ulukalabs/__init__.py
|
|
9
|
+
src/ulukalabs/__main__.py
|
|
10
|
+
src/ulukalabs/cli.py
|
|
11
|
+
tests/test_cli.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ulukalabs
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Open the Uluka terminal for an owner-approved account.
|
|
2
|
+
|
|
3
|
+
pipx install uluka (or: python3 -m pip install uluka)
|
|
4
|
+
uluka sign in if needed, install or update Uluka, open the terminal
|
|
5
|
+
uluka login sign this Mac in again
|
|
6
|
+
|
|
7
|
+
This package holds no Uluka code. Signing in is the app's own "Allow this Mac" approval in the browser; the
|
|
8
|
+
terminal, its agent and the encrypted model come in the signed release, which only an approved, signed-in Mac
|
|
9
|
+
can download. The release is checked (size, SHA-256, code signature) before it is installed, and the terminal
|
|
10
|
+
checks the release's signed files and the account's approval every time it starts. Uses only Python and macOS.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import hashlib
|
|
16
|
+
import http.client
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import platform
|
|
20
|
+
import plistlib
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import socket
|
|
24
|
+
import ssl
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
import urllib.error
|
|
29
|
+
import urllib.request
|
|
30
|
+
import webbrowser
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from . import __version__
|
|
34
|
+
|
|
35
|
+
ORIGIN = "https://ulukalabs.com"
|
|
36
|
+
OPENSSL = "/usr/bin/openssl"
|
|
37
|
+
CACHE = Path.home() / "Library" / "Caches" / "Uluka"
|
|
38
|
+
CHUNK = 8 * 2**20
|
|
39
|
+
LOGIN_SECONDS = 600
|
|
40
|
+
HEX64 = re.compile(r"[0-9a-f]{64}")
|
|
41
|
+
NETWORK_ERRORS = (urllib.error.URLError, http.client.HTTPException, ConnectionError, TimeoutError, socket.timeout)
|
|
42
|
+
# Run by the app's own bundled Python, so the macOS Keychain treats the item as the app's (no access prompt).
|
|
43
|
+
# Only the credential comes back out; the device's private key never leaves the app's Keychain item.
|
|
44
|
+
KEYCHAIN_GET = ("import sys, json; sys.path.insert(0, sys.argv[1]); from app.model_license import MacKeychain; "
|
|
45
|
+
"i = MacKeychain().get(); print(json.dumps(i and {'credential': i['credential'], 'expiresAt': i['expiresAt']}))")
|
|
46
|
+
KEYCHAIN_PUT = ("import sys, json; sys.path.insert(0, sys.argv[1]); from app.model_license import MacKeychain; "
|
|
47
|
+
"MacKeychain().put(json.load(sys.stdin))")
|
|
48
|
+
TERMINAL = "import sys; sys.path.insert(0, sys.argv.pop(1)); from uluka.cli import main; raise SystemExit(main())"
|
|
49
|
+
USAGE = __doc__.split("\n\n")[1]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Fail(Exception):
|
|
53
|
+
"""A message for the person at the terminal."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SignInAgain(Exception):
|
|
57
|
+
"""The saved credential was refused: the Mac must be approved again."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
61
|
+
def redirect_request(self, *args, **kwargs):
|
|
62
|
+
return None # a redirect is an error: the device credential is never sent anywhere else
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_OPENER = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _opener():
|
|
69
|
+
"""No proxies (as in the app), no redirects, and macOS's root certificates whichever Python runs this
|
|
70
|
+
(python.org builds ship without a CA bundle)."""
|
|
71
|
+
global _OPENER
|
|
72
|
+
if _OPENER is None:
|
|
73
|
+
context = ssl.create_default_context()
|
|
74
|
+
roots = subprocess.run(["security", "find-certificate", "-a", "-p",
|
|
75
|
+
"/System/Library/Keychains/SystemRootCertificates.keychain"],
|
|
76
|
+
capture_output=True, text=True).stdout
|
|
77
|
+
if roots:
|
|
78
|
+
context.load_verify_locations(cadata=roots)
|
|
79
|
+
_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}), _NoRedirect,
|
|
80
|
+
urllib.request.HTTPSHandler(context=context))
|
|
81
|
+
return _OPENER
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def http(method, path, *, data=None, headers=None, timeout=60):
|
|
85
|
+
body = None if data is None else json.dumps(data).encode()
|
|
86
|
+
request = urllib.request.Request(ORIGIN + path, data=body, method=method, headers={
|
|
87
|
+
"User-Agent": f"uluka/{__version__}", **({"Content-Type": "application/json"} if body else {}),
|
|
88
|
+
**(headers or {})})
|
|
89
|
+
return _opener().open(request, timeout=timeout)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def call(method, path, data=None):
|
|
93
|
+
"""A JSON request -> (status, body). HTTP error statuses are returned, not raised."""
|
|
94
|
+
try:
|
|
95
|
+
with http(method, path, data=data) as response:
|
|
96
|
+
return response.status, json.load(response)
|
|
97
|
+
except urllib.error.HTTPError as exc:
|
|
98
|
+
try:
|
|
99
|
+
return exc.code, json.load(exc)
|
|
100
|
+
except ValueError:
|
|
101
|
+
return exc.code, {}
|
|
102
|
+
except NETWORK_ERRORS as exc:
|
|
103
|
+
raise Fail(f"Could not reach {ORIGIN}: {getattr(exc, 'reason', exc)}") from exc
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def check_mac():
|
|
107
|
+
release = platform.mac_ver()[0]
|
|
108
|
+
if sys.platform != "darwin" or platform.machine() != "arm64":
|
|
109
|
+
raise Fail("Uluka runs on Apple silicon Macs for now. Windows and Linux are coming later.")
|
|
110
|
+
if int(release.split(".")[0] or 0) < 14:
|
|
111
|
+
raise Fail(f"Uluka needs macOS 14 or newer (this Mac has {release}).")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def latest():
|
|
115
|
+
status, body = call("GET", "/api/releases")
|
|
116
|
+
mac = next((r for r in body.get("releases", []) if r.get("platform") == "macos"), None) if status == 200 else None
|
|
117
|
+
if not mac or not mac.get("available"):
|
|
118
|
+
raise Fail("No Mac release is available right now.")
|
|
119
|
+
if not HEX64.fullmatch(str(mac.get("sha256"))) or type(mac.get("bytes")) is not int \
|
|
120
|
+
or not re.fullmatch(r"[A-Za-z0-9._-]{1,160}\.dmg", str(mac.get("filename"))):
|
|
121
|
+
raise Fail("The release information looks wrong. Try again later.")
|
|
122
|
+
return mac
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def find_app():
|
|
126
|
+
"""The installed app, or None. A drag-installed /Applications copy is the one kept up to date."""
|
|
127
|
+
return next((app for app in (Path("/Applications/Uluka.app"), Path.home() / "Applications" / "Uluka.app")
|
|
128
|
+
if app.is_dir()), None)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def default_target():
|
|
132
|
+
return Path("/Applications/Uluka.app") if os.access("/Applications", os.W_OK) \
|
|
133
|
+
else Path.home() / "Applications" / "Uluka.app"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def installed_version(app):
|
|
137
|
+
try:
|
|
138
|
+
return json.loads((app / "Contents" / "Resources" / "release-manifest.json").read_bytes()).get("version")
|
|
139
|
+
except (OSError, ValueError):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def running(app):
|
|
144
|
+
return subprocess.run(["pgrep", "-f", f"{app}/Contents/"], capture_output=True).returncode == 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def keychain(app, code, identity=None):
|
|
148
|
+
"""Run KEYCHAIN_GET / KEYCHAIN_PUT inside the installed app's runtime."""
|
|
149
|
+
resources = app / "Contents" / "Resources"
|
|
150
|
+
result = subprocess.run([str(resources / "runtime" / "python" / "bin" / "python3"), "-I", "-B", "-c", code,
|
|
151
|
+
str(resources)], input=json.dumps(identity) if identity else "",
|
|
152
|
+
capture_output=True, text=True, timeout=120)
|
|
153
|
+
if code is KEYCHAIN_GET:
|
|
154
|
+
return json.loads(result.stdout or "null") if result.returncode == 0 else None # unreadable: sign in again
|
|
155
|
+
if result.returncode != 0:
|
|
156
|
+
raise Fail("Uluka could not save its sign-in to the Keychain. Unlock your login keychain, then open Uluka.")
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def keypair():
|
|
161
|
+
"""A 3072-bit RSA device key, as the app makes it: PKCS#8 private DER and base64 SPKI public DER."""
|
|
162
|
+
private = subprocess.run([OPENSSL, "genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:3072",
|
|
163
|
+
"-outform", "DER"], capture_output=True, check=True).stdout
|
|
164
|
+
public = subprocess.run([OPENSSL, "pkey", "-inform", "DER", "-pubout", "-outform", "DER"], input=private,
|
|
165
|
+
capture_output=True, check=True).stdout
|
|
166
|
+
return private, base64.b64encode(public).decode()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def sign_in():
|
|
170
|
+
"""The app's device approval: the owner-approved account allows this Mac in the browser."""
|
|
171
|
+
private, public = keypair()
|
|
172
|
+
status, start = call("POST", "/api/model/start", {"publicKey": public})
|
|
173
|
+
if status != 201:
|
|
174
|
+
raise Fail(start.get("error") or f"Mac sign-in could not start (HTTP {status}).")
|
|
175
|
+
fingerprint = str(start.get("fingerprint"))
|
|
176
|
+
if fingerprint != hashlib.sha256(public.encode()).hexdigest() or not HEX64.fullmatch(str(start.get("id"))) \
|
|
177
|
+
or not HEX64.fullmatch(str(start.get("code"))):
|
|
178
|
+
raise Fail("Mac sign-in returned an unexpected answer.")
|
|
179
|
+
url = f"{ORIGIN}/app?macDevice={start['id']}#download" # built here, never taken from the server
|
|
180
|
+
print("Connect this Mac to Uluka", file=sys.stderr)
|
|
181
|
+
print(f" Device code: {' '.join(fingerprint[i:i + 4] for i in range(0, 16, 4))}", file=sys.stderr)
|
|
182
|
+
print(" Sign in with your approved account, check the code matches, then choose Allow this Mac.", file=sys.stderr)
|
|
183
|
+
print(f" If no browser opened: {url}", file=sys.stderr)
|
|
184
|
+
webbrowser.open(url)
|
|
185
|
+
deadline = time.monotonic() + LOGIN_SECONDS
|
|
186
|
+
while time.monotonic() < deadline:
|
|
187
|
+
time.sleep(4)
|
|
188
|
+
status, result = call("POST", "/api/model/poll", {"code": start["code"]})
|
|
189
|
+
if status in (202, 429):
|
|
190
|
+
continue
|
|
191
|
+
if status == 200 and HEX64.fullmatch(str(result.get("credential"))) and type(result.get("expiresAt")) is int:
|
|
192
|
+
print("Signed in.", file=sys.stderr)
|
|
193
|
+
return {"privateKey": base64.b64encode(private).decode(), "credential": result["credential"],
|
|
194
|
+
"expiresAt": result["expiresAt"]}
|
|
195
|
+
if status == 403:
|
|
196
|
+
raise Fail("This account is not approved for Uluka yet. Request access at https://ulukalabs.com/app")
|
|
197
|
+
raise Fail(result.get("error") or "Sign-in did not finish. Run uluka again.")
|
|
198
|
+
raise Fail("Sign-in timed out after 10 minutes. Run uluka again.")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def download(release, credential):
|
|
202
|
+
"""Resumable download of the release into CACHE, verified against the published size and SHA-256."""
|
|
203
|
+
CACHE.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
target = CACHE / release["filename"]
|
|
205
|
+
part = target.with_name(target.name + ".part")
|
|
206
|
+
for stale in CACHE.glob("*.dmg*"):
|
|
207
|
+
if stale not in (target, part):
|
|
208
|
+
stale.unlink()
|
|
209
|
+
size, digest = release["bytes"], hashlib.sha256()
|
|
210
|
+
have = part.stat().st_size if part.exists() else 0
|
|
211
|
+
if have > size:
|
|
212
|
+
part.unlink()
|
|
213
|
+
have = 0
|
|
214
|
+
with part.open("ab") as out:
|
|
215
|
+
if have:
|
|
216
|
+
with part.open("rb") as previous:
|
|
217
|
+
for block in iter(lambda: previous.read(CHUNK), b""):
|
|
218
|
+
digest.update(block)
|
|
219
|
+
if shutil.disk_usage(CACHE).free < (size - have) + size + 2 * 2**30:
|
|
220
|
+
raise Fail(f"Free up space first: installing needs about {2 * size / 1e9:.0f} GB.")
|
|
221
|
+
failures, shown = 0, 0.0
|
|
222
|
+
while have < size:
|
|
223
|
+
headers = {"Authorization": f"Uluka-Device {credential}"}
|
|
224
|
+
if have:
|
|
225
|
+
headers["Range"] = f"bytes={have}-"
|
|
226
|
+
try:
|
|
227
|
+
with http("GET", "/api/download/macos", headers=headers) as response:
|
|
228
|
+
if have and response.status != 206:
|
|
229
|
+
raise Fail("The server did not resume the download. Run uluka again.")
|
|
230
|
+
for block in iter(lambda: response.read(CHUNK), b""):
|
|
231
|
+
out.write(block)
|
|
232
|
+
digest.update(block)
|
|
233
|
+
have += len(block)
|
|
234
|
+
if time.monotonic() - shown > 0.5 or have == size:
|
|
235
|
+
shown = time.monotonic()
|
|
236
|
+
print(f"\rDownloading Uluka {release['version']}: {have / 1e9:.2f} of {size / 1e9:.2f} GB "
|
|
237
|
+
f"({100 * have // size}%)", end="", file=sys.stderr, flush=True)
|
|
238
|
+
except urllib.error.HTTPError as exc:
|
|
239
|
+
if exc.code == 401:
|
|
240
|
+
raise SignInAgain() from exc
|
|
241
|
+
if exc.code == 403:
|
|
242
|
+
raise Fail("This account is not approved for Uluka downloads.") from exc
|
|
243
|
+
raise Fail(f"The download failed (HTTP {exc.code}). Run uluka again to resume.") from exc
|
|
244
|
+
except NETWORK_ERRORS as exc:
|
|
245
|
+
failures += 1
|
|
246
|
+
if failures > 5:
|
|
247
|
+
raise Fail("The download keeps failing. Run uluka again to resume.") from exc
|
|
248
|
+
time.sleep(min(30, 2 ** failures))
|
|
249
|
+
print(file=sys.stderr)
|
|
250
|
+
if have != size or digest.hexdigest() != release["sha256"]:
|
|
251
|
+
part.unlink()
|
|
252
|
+
raise Fail("The download did not match the published checksum and was deleted. Run uluka again.")
|
|
253
|
+
part.replace(target)
|
|
254
|
+
return target
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def install(dmg, target, version):
|
|
258
|
+
"""Copy Uluka.app out of the verified disk image, replacing the installed copy only once it is complete."""
|
|
259
|
+
attached = plistlib.loads(subprocess.run(["hdiutil", "attach", "-readonly", "-nobrowse", "-noautoopen", "-plist",
|
|
260
|
+
str(dmg)], capture_output=True, check=True).stdout)
|
|
261
|
+
mount = next(Path(e["mount-point"]) for e in attached["system-entities"] if "mount-point" in e)
|
|
262
|
+
try:
|
|
263
|
+
source = mount / "Uluka.app"
|
|
264
|
+
if subprocess.run(["codesign", "--verify", "--deep", "--strict", str(source)],
|
|
265
|
+
capture_output=True).returncode != 0:
|
|
266
|
+
raise Fail("The downloaded app failed its code signature check.")
|
|
267
|
+
if installed_version(source) != version:
|
|
268
|
+
raise Fail("The disk image does not hold the published version.")
|
|
269
|
+
print("Installing…", file=sys.stderr)
|
|
270
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
staging, old = target.with_name(".Uluka.app.installing"), target.with_name(".Uluka.app.old")
|
|
272
|
+
for path in (staging, old):
|
|
273
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
274
|
+
subprocess.run(["ditto", str(source), str(staging)], check=True)
|
|
275
|
+
if target.exists():
|
|
276
|
+
target.rename(old)
|
|
277
|
+
staging.rename(target)
|
|
278
|
+
shutil.rmtree(old, ignore_errors=True)
|
|
279
|
+
finally:
|
|
280
|
+
subprocess.run(["hdiutil", "detach", str(mount)], capture_output=True)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def update(app, release):
|
|
284
|
+
"""Install `release` at the app's place (or the default one). Returns the installed app."""
|
|
285
|
+
target = app or default_target()
|
|
286
|
+
if app and running(app):
|
|
287
|
+
raise Fail("Quit Uluka, then run uluka again to update it.")
|
|
288
|
+
saved = keychain(app, KEYCHAIN_GET) if app else None
|
|
289
|
+
fresh = None
|
|
290
|
+
credential = saved["credential"] if saved and saved.get("expiresAt", 0) > time.time() else None
|
|
291
|
+
for _ in range(2):
|
|
292
|
+
if credential is None:
|
|
293
|
+
fresh = sign_in()
|
|
294
|
+
credential = fresh["credential"]
|
|
295
|
+
try:
|
|
296
|
+
dmg = download(release, credential)
|
|
297
|
+
break
|
|
298
|
+
except SignInAgain:
|
|
299
|
+
if fresh is not None:
|
|
300
|
+
raise Fail("The new sign-in was refused. Run uluka again.") from None
|
|
301
|
+
credential = None
|
|
302
|
+
install(dmg, target, release["version"])
|
|
303
|
+
dmg.unlink()
|
|
304
|
+
if fresh is not None:
|
|
305
|
+
keychain(target, KEYCHAIN_PUT, fresh) # the app starts signed in: one approval per Mac
|
|
306
|
+
print(f"Uluka {release['version']} is installed at {target}.", file=sys.stderr)
|
|
307
|
+
return target
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def signed_in(app):
|
|
311
|
+
saved = keychain(app, KEYCHAIN_GET)
|
|
312
|
+
return bool(saved) and saved.get("expiresAt", 0) > time.time()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def open_terminal(app, args):
|
|
316
|
+
"""Replace this process with the terminal inside the installed release (compiled, with its own Python)."""
|
|
317
|
+
import resource
|
|
318
|
+
resources = app / "Contents" / "Resources"
|
|
319
|
+
python = resources / "runtime" / "python" / "bin" / "python3"
|
|
320
|
+
resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) # as the app's launcher: no memory dumps of the model
|
|
321
|
+
env = dict(os.environ, HF_HUB_OFFLINE="1", TRANSFORMERS_OFFLINE="1",
|
|
322
|
+
PLAYWRIGHT_BROWSERS_PATH=str(resources / "browsers"))
|
|
323
|
+
os.execve(python, [str(python), "-I", "-B", "-c", TERMINAL, str(resources), *args], env)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def main(argv=None):
|
|
327
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
328
|
+
if args[:1] == ["--installer-version"]:
|
|
329
|
+
print(f"uluka installer {__version__}")
|
|
330
|
+
return 0
|
|
331
|
+
try:
|
|
332
|
+
check_mac()
|
|
333
|
+
app = find_app()
|
|
334
|
+
if app is None and args[:1] in (["-h"], ["--help"]):
|
|
335
|
+
print(USAGE)
|
|
336
|
+
return 0
|
|
337
|
+
release = latest()
|
|
338
|
+
if app is None or installed_version(app) != release["version"]:
|
|
339
|
+
app = update(app, release) # signs in first when needed
|
|
340
|
+
login = args[:1] == ["login"]
|
|
341
|
+
if login or not signed_in(app):
|
|
342
|
+
keychain(app, KEYCHAIN_PUT, sign_in())
|
|
343
|
+
if login:
|
|
344
|
+
return 0
|
|
345
|
+
open_terminal(app, args)
|
|
346
|
+
except Fail as exc:
|
|
347
|
+
print(f"\nuluka: {exc}", file=sys.stderr)
|
|
348
|
+
return 1
|
|
349
|
+
except KeyboardInterrupt:
|
|
350
|
+
print("\nuluka: stopped. Run uluka again to continue where it left off.", file=sys.stderr)
|
|
351
|
+
return 130
|
|
352
|
+
return 0
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""The launcher against a local stand-in for ulukalabs.com: sign-in, resumable verified download, install.
|
|
2
|
+
|
|
3
|
+
python -m pytest launcher/tests (macOS: uses openssl, hdiutil, codesign and ditto)
|
|
4
|
+
"""
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import plistlib
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
17
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "release" / "macos"))
|
|
18
|
+
from ulukalabs import cli # noqa: E402
|
|
19
|
+
|
|
20
|
+
pytestmark = pytest.mark.skipif(sys.platform != "darwin", reason="macOS tools")
|
|
21
|
+
PAYLOAD = bytes(range(256)) * 4096 + b"tail" # ~1 MiB stands in for the disk image
|
|
22
|
+
CREDENTIAL = "c" * 64
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Site:
|
|
26
|
+
"""Just enough of the Worker: releases, the device sign-in and the device-authorized download."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, payload=PAYLOAD, published=None):
|
|
29
|
+
self.payload, self.polls, self.requests = payload, 0, []
|
|
30
|
+
self.sha = published or hashlib.sha256(payload).hexdigest()
|
|
31
|
+
site = self
|
|
32
|
+
|
|
33
|
+
class Handler(BaseHTTPRequestHandler):
|
|
34
|
+
def log_message(self, *args):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def reply(self, status, body=b"", headers=None):
|
|
38
|
+
self.send_response(status)
|
|
39
|
+
for k, v in (headers or {}).items():
|
|
40
|
+
self.send_header(k, v)
|
|
41
|
+
self.send_header("Content-Length", str(len(body)))
|
|
42
|
+
self.end_headers()
|
|
43
|
+
self.wfile.write(body)
|
|
44
|
+
|
|
45
|
+
def do_GET(self):
|
|
46
|
+
site.requests.append((self.path, dict(self.headers)))
|
|
47
|
+
if self.path == "/api/releases":
|
|
48
|
+
return self.reply(200, json.dumps({"releases": [{"platform": "macos", "available": True,
|
|
49
|
+
"version": "9.9", "filename": "Uluka-test.dmg", "bytes": len(site.payload),
|
|
50
|
+
"sha256": site.sha}]}).encode())
|
|
51
|
+
if self.headers.get("Authorization") != f"Uluka-Device {CREDENTIAL}" or "Origin" in self.headers:
|
|
52
|
+
return self.reply(401)
|
|
53
|
+
rng = self.headers.get("Range")
|
|
54
|
+
if rng:
|
|
55
|
+
start = int(rng.split("=")[1].rstrip("-"))
|
|
56
|
+
return self.reply(206, site.payload[start:])
|
|
57
|
+
return self.reply(200, site.payload)
|
|
58
|
+
|
|
59
|
+
def do_POST(self):
|
|
60
|
+
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
|
61
|
+
if self.path == "/api/model/start":
|
|
62
|
+
site.public_key = body["publicKey"]
|
|
63
|
+
return self.reply(201, json.dumps({"id": "a" * 64, "code": "b" * 64,
|
|
64
|
+
"fingerprint": hashlib.sha256(body["publicKey"].encode()).hexdigest()}).encode())
|
|
65
|
+
site.polls += 1
|
|
66
|
+
if site.polls == 1:
|
|
67
|
+
return self.reply(202, b'{"pending": true}')
|
|
68
|
+
return self.reply(200, json.dumps({"credential": CREDENTIAL, "expiresAt": 2_000_000_000}).encode())
|
|
69
|
+
|
|
70
|
+
self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
71
|
+
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
|
72
|
+
self.origin = f"http://127.0.0.1:{self.server.server_port}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def site(monkeypatch, tmp_path):
|
|
77
|
+
s = Site()
|
|
78
|
+
monkeypatch.setattr(cli, "ORIGIN", s.origin)
|
|
79
|
+
monkeypatch.setattr(cli, "CACHE", tmp_path / "cache")
|
|
80
|
+
monkeypatch.setattr(cli.time, "sleep", lambda _: None)
|
|
81
|
+
opened = []
|
|
82
|
+
monkeypatch.setattr(cli.webbrowser, "open", opened.append)
|
|
83
|
+
s.opened = opened
|
|
84
|
+
yield s
|
|
85
|
+
s.server.shutdown()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_sign_in_makes_an_app_compatible_identity(site):
|
|
89
|
+
identity = cli.sign_in()
|
|
90
|
+
assert identity["credential"] == CREDENTIAL and site.polls == 2
|
|
91
|
+
assert site.opened == [f"{site.origin}/app?macDevice={'a' * 64}#download"] # built locally
|
|
92
|
+
from cryptography.hazmat.primitives import serialization
|
|
93
|
+
import base64
|
|
94
|
+
private = serialization.load_der_private_key(base64.b64decode(identity["privateKey"]), None)
|
|
95
|
+
assert private.key_size == 3072
|
|
96
|
+
spki = private.public_key().public_bytes(serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
|
|
97
|
+
assert base64.b64encode(spki).decode() == site.public_key # the key the site registered
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_download_resumes_and_verifies(site):
|
|
101
|
+
release = cli.latest()
|
|
102
|
+
part = cli.CACHE / "Uluka-test.dmg.part"
|
|
103
|
+
part.parent.mkdir(parents=True)
|
|
104
|
+
part.write_bytes(PAYLOAD[:1000]) # an interrupted earlier run
|
|
105
|
+
dmg = cli.download(release, CREDENTIAL)
|
|
106
|
+
assert dmg.read_bytes() == PAYLOAD and not part.exists()
|
|
107
|
+
path, headers = site.requests[-1]
|
|
108
|
+
assert headers["Range"] == "bytes=1000-" and "Origin" not in headers
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_a_checksum_mismatch_is_deleted(site, monkeypatch):
|
|
112
|
+
monkeypatch.setattr(site, "sha", "0" * 64)
|
|
113
|
+
with pytest.raises(cli.Fail, match="checksum"):
|
|
114
|
+
cli.download(cli.latest(), CREDENTIAL)
|
|
115
|
+
assert not list(cli.CACHE.iterdir())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_a_refused_credential_asks_for_sign_in(site):
|
|
119
|
+
with pytest.raises(cli.SignInAgain):
|
|
120
|
+
cli.download(cli.latest(), "d" * 64)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_install_replaces_the_app_from_a_signed_image(tmp_path):
|
|
124
|
+
from disk_image import build_disk_image
|
|
125
|
+
app = tmp_path / "src" / "Uluka.app"
|
|
126
|
+
(app / "Contents" / "MacOS").mkdir(parents=True)
|
|
127
|
+
(app / "Contents" / "Resources").mkdir()
|
|
128
|
+
(app / "Contents" / "MacOS" / "Uluka").write_bytes(Path("/usr/bin/true").read_bytes())
|
|
129
|
+
(app / "Contents" / "MacOS" / "Uluka").chmod(0o755)
|
|
130
|
+
(app / "Contents" / "Resources" / "release-manifest.json").write_text('{"version": "9.9"}')
|
|
131
|
+
(app / "Contents" / "Info.plist").write_bytes(plistlib.dumps({"CFBundleIdentifier": "com.ulukalabs.test.launcher",
|
|
132
|
+
"CFBundleExecutable": "Uluka"}))
|
|
133
|
+
subprocess.run(["codesign", "--force", "--sign", "-", str(app)], check=True, capture_output=True)
|
|
134
|
+
dmg = build_disk_image(app, tmp_path / "Uluka.dmg")
|
|
135
|
+
target = tmp_path / "Applications" / "Uluka.app"
|
|
136
|
+
target.mkdir(parents=True)
|
|
137
|
+
(target / "old").write_text("previous install")
|
|
138
|
+
cli.install(dmg, target, "9.9")
|
|
139
|
+
assert cli.installed_version(target) == "9.9" and not (target / "old").exists()
|
|
140
|
+
assert not list(target.parent.glob(".Uluka.app.*"))
|
|
141
|
+
with pytest.raises(cli.Fail, match="published version"):
|
|
142
|
+
cli.install(dmg, target, "1.0")
|
|
143
|
+
assert cli.installed_version(target) == "9.9" # a refused image changes nothing
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_the_terminal_replaces_the_installer_process(monkeypatch, tmp_path):
|
|
147
|
+
calls = []
|
|
148
|
+
monkeypatch.setattr(cli.os, "execve", lambda path, argv, env: calls.append((path, argv, env)))
|
|
149
|
+
import resource
|
|
150
|
+
monkeypatch.setattr(resource, "setrlimit", lambda *a: calls.append(("rlimit", a)))
|
|
151
|
+
app = tmp_path / "Uluka.app"
|
|
152
|
+
cli.open_terminal(app, ["--model", "26b"])
|
|
153
|
+
resources = app / "Contents" / "Resources"
|
|
154
|
+
python = resources / "runtime" / "python" / "bin" / "python3"
|
|
155
|
+
assert calls[0] == ("rlimit", (resource.RLIMIT_CORE, (0, 0)))
|
|
156
|
+
path, argv, env = calls[1]
|
|
157
|
+
assert path == python and argv == [str(python), "-I", "-B", "-c", cli.TERMINAL, str(resources), "--model", "26b"]
|
|
158
|
+
assert env["HF_HUB_OFFLINE"] == "1" and env["TRANSFORMERS_OFFLINE"] == "1"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_help_before_install_downloads_nothing(monkeypatch, capsys):
|
|
162
|
+
monkeypatch.setattr(cli, "find_app", lambda: None)
|
|
163
|
+
monkeypatch.setattr(cli, "latest", lambda: pytest.fail("no network for --help"))
|
|
164
|
+
assert cli.main(["--help"]) == 0
|
|
165
|
+
assert "uluka login" in capsys.readouterr().out
|