Skip to content

Commit 5768660

Browse files
booffaoexbooffaoex
authored andcommitted
v0.3.2: Fix CLI binary download - use ZIP packages with resources
- loader.py: Download ZIP instead of single file, extract with resources/ - build-csharp.yml: Upload MeowthBridge-{platform}.zip to Release - Revert PublishSingleFile (breaks resources/ access) - Fix syntax error and orphaned code in loader.py - Bump version to 0.3.2
1 parent 5452ea5 commit 5768660

2 files changed

Lines changed: 62 additions & 127 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "meowth"
7-
version = "0.3.1"
7+
version = "0.3.2"
88
description = "GBA Pokemon ROM translation tool with CLI and GUI support"
99
requires-python = ">=3.10"
1010
dependencies = [

src/meowth/binaries/loader.py

Lines changed: 61 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,16 @@
1313

1414

1515
def get_meowth_version() -> str:
16-
"""Get the current meowth package version.
17-
18-
Returns:
19-
Version string (e.g., "0.3.1")
20-
"""
16+
"""Get the current meowth package version."""
2117
try:
2218
import importlib.metadata
2319
return importlib.metadata.version("meowth")
2420
except Exception:
25-
# Fallback for development mode
26-
return "0.3.1"
21+
return "0.3.2"
2722

2823

2924
def get_platform_name() -> str:
30-
"""Get the normalized platform name for binary selection.
31-
32-
Returns:
33-
One of: "windows", "macos", "linux"
34-
"""
25+
"""Get the normalized platform name for binary selection."""
3526
system = platform.system().lower()
3627
if system == "darwin":
3728
return "macos"
@@ -40,16 +31,11 @@ def get_platform_name() -> str:
4031
elif system == "linux":
4132
return "linux"
4233
else:
43-
# Fallback for unknown platforms
4434
return "linux"
4535

4636

4737
def get_executable_name() -> str:
48-
"""Get the executable name for the current platform.
49-
50-
Returns:
51-
"MeowthBridge.exe" on Windows, "MeowthBridge" on Unix
52-
"""
38+
"""Get the executable name for the current platform."""
5339
return "MeowthBridge.exe" if platform.system() == "Windows" else "MeowthBridge"
5440

5541

@@ -60,12 +46,8 @@ def find_meowth_bridge() -> Path:
6046
1. Environment variable MEOWTH_BRIDGE_PATH (if set)
6147
2. Bundled binary in package (src/meowth/binaries/{platform}/)
6248
3. Development build (src/MeowthBridge/bin/Release or Debug)
63-
64-
Returns:
65-
Path to the MeowthBridge executable
66-
67-
Raises:
68-
FileNotFoundError: If MeowthBridge cannot be found
49+
4. Cached download (~/.meowth/binaries/{platform}/)
50+
5. Download from GitHub releases
6951
"""
7052
exe_name = get_executable_name()
7153

@@ -75,93 +57,64 @@ def find_meowth_bridge() -> Path:
7557
env_exe = Path(env_path)
7658
if env_exe.exists() and env_exe.is_file():
7759
return env_exe
78-
# If MEOWTH_BRIDGE_PATH is a directory, look for executable inside
7960
if env_exe.is_dir():
8061
env_exe = env_exe / exe_name
8162
if env_exe.exists():
8263
return env_exe
8364

8465
# Strategy 2: Check bundled binary in package
85-
# This is where the binary will be when installed via pip
8666
package_dir = Path(__file__).parent
8767
platform_name = get_platform_name()
8868
bundled_exe = package_dir / platform_name / exe_name
8969

9070
if bundled_exe.exists():
91-
# Make sure it's executable on Unix systems
9271
if platform.system() != "Windows":
9372
bundled_exe.chmod(0o755)
9473
return bundled_exe
9574

9675
# Strategy 3: Check development build
97-
# This is for developers working on the project
98-
# Look for src/MeowthBridge/bin/{Release,Debug}/net8.0/MeowthBridge
9976
project_root = Path(__file__).parent.parent.parent.parent
10077
meowth_bridge_dir = project_root / "src" / "MeowthBridge"
10178

102-
# Try Release first, then Debug
10379
for build_config in ("Release", "Debug"):
10480
dev_exe = meowth_bridge_dir / "bin" / build_config / "net8.0" / exe_name
10581
if dev_exe.exists():
10682
return dev_exe
10783

108-
# Strategy 4: Check if it's in the old location (backward compatibility)
84+
# Strategy 4: Check old location (backward compatibility)
10985
old_location = project_root / "MeowthBridge" / "bin"
11086
for build_config in ("Release", "Debug"):
11187
old_exe = old_location / build_config / "net8.0" / exe_name
11288
if old_exe.exists():
11389
return old_exe
11490

115-
# Not found in standard locations, try downloading from GitHub
91+
# Strategy 5: Download from GitHub
11692
try:
11793
return _download_meowth_bridge()
11894
except Exception as download_error:
119-
# If download fails, raise the original error
12095
raise FileNotFoundError(
12196
f"MeowthBridge executable not found. Tried:\n"
12297
f" 1. Environment variable MEOWTH_BRIDGE_PATH: {env_path or '(not set)'}\n"
12398
f" 2. Bundled binary: {bundled_exe}\n"
124-
f" 3. Development build: {meowth_bridge_dir / 'bin' / '{Release,Debug}' / 'net8.0' / exe_name}\n"
99+
f" 3. Development build: {meowth_bridge_dir / 'bin'}\n"
125100
f" 4. Download from GitHub: {download_error}\n"
126101
f"\n"
127102
f"To fix this:\n"
128-
f" - If developing: Build MeowthBridge with 'dotnet build src/MeowthBridge -c Release'\n"
129-
f" - If using pip: Check your internet connection or set MEOWTH_BRIDGE_PATH\n"
130-
f" - Set MEOWTH_BRIDGE_PATH environment variable to the executable path"
103+
f" - Set MEOWTH_BRIDGE_PATH environment variable to the executable path\n"
104+
f" - Or check your internet connection for auto-download"
131105
) from download_error
132106

133107

134-
135-
136108
def _download_meowth_bridge() -> Path:
137-
"""Download MeowthBridge binary from GitHub release if not found locally.
138-
139-
Returns:
140-
Path to the downloaded executable
141-
142-
Raises:
143-
FileNotFoundError: If download fails
144-
"""
109+
"""Download MeowthBridge ZIP from GitHub release and extract it."""
145110
exe_name = get_executable_name()
146111
platform_name = get_platform_name()
147-
148-
# Get current version
149112
version = get_meowth_version()
150113

151-
# Determine the ZIP asset name based on platform
152-
asset_name_map = {
153-
"macos": "MeowthBridge-macos.zip",
154-
"windows": "MeowthBridge-windows.zip",
155-
"linux": "MeowthBridge-linux.zip",
156-
}
157-
asset_name = asset_name_map.get(platform_name)
158-
if not asset_name:
159-
raise FileNotFoundError(f"Unsupported platform: {platform_name}")
160-
161-
# URL to the GitHub release
114+
asset_name = f"MeowthBridge-{platform_name}.zip"
162115
download_url = f"https://github.com/Olcmyk/Meowth-GBA-Translator/releases/download/v{version}/{asset_name}"
163116

164-
# Create cache directory
117+
# Cache directory
165118
cache_dir = Path.home() / ".meowth" / "binaries" / platform_name
166119
cache_dir.mkdir(parents=True, exist_ok=True)
167120

@@ -177,8 +130,8 @@ def _download_meowth_bridge() -> Path:
177130
print(f"🔽 First-time setup: Downloading MeowthBridge for {platform_name}...")
178131
print(f" Source: {download_url}")
179132

133+
tmp_zip_path = None
180134
try:
181-
# Download to temporary file
182135
with tempfile.NamedTemporaryFile(delete=False, suffix='.zip') as tmp_file:
183136
tmp_zip_path = Path(tmp_file.name)
184137

@@ -201,6 +154,8 @@ def _download_meowth_bridge() -> Path:
201154
return exe_path
202155

203156
except urllib.error.HTTPError as e:
157+
if tmp_zip_path and tmp_zip_path.exists():
158+
tmp_zip_path.unlink()
204159
if e.code == 404:
205160
raise FileNotFoundError(
206161
f"MeowthBridge binary not found in release v{version}.\n"
@@ -209,97 +164,77 @@ def _download_meowth_bridge() -> Path:
209164
f" 1. The release hasn't been published yet\n"
210165
f" 2. The binary wasn't uploaded to the release\n\n"
211166
f"Workaround:\n"
212-
f" Set MEOWTH_BRIDGE_PATH environment variable to a local binary:\n"
213-
t MEOWTH_BRIDGE_PATH=/path/to/MeowthBridge"
167+
f" export MEOWTH_BRIDGE_PATH=/path/to/MeowthBridge"
214168
)
215169
else:
216170
raise FileNotFoundError(f"HTTP error {e.code} downloading from {download_url}: {e}")
217171
except Exception as e:
172+
if tmp_zip_path and tmp_zip_path.exists():
173+
tmp_zip_path.unlink()
218174
raise FileNotFoundError(f"Failed to download MeowthBridge: {e}")
219175

220176

221177
def _download_with_progress_and_retry(url: str, dest: Path, max_retries: int = 3):
222-
"""Download file with progress display and retry logic.
223-
224-
Args:
225-
url: URL to download from
226-
dest: Destination file path
227-
max_retries: Maximum number of retry attempts
228-
229-
Raises:
230-
Exception: If download fails after all retries
231-
"""
178+
"""Download file with progress display and retry logic."""
232179
for attempt in range(1, max_retries + 1):
233180
try:
234181
_download_with_progress(url, dest)
235-
return # Success
182+
return
236183
except Exception as e:
237184
if attempt < max_retries:
238185
print(f" ⚠️ Download failed (attempt {attempt}/{max_retries}): {e}")
239186
print(f" 🔄 Retrying in 2 seconds...")
240187
time.sleep(2)
241188
else:
242-
raise # Final attempt failed
189+
raise
243190

244191

245192
def _download_with_progress(url: str, dest: Path):
246-
"""Download file with progress display.
193+
"""Download file with progress display."""
194+
tmp_path = None
195+
try:
196+
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
197+
tmp_path = Path(tmp_file.name)
198+
199+
response = urllib.request.urlopen(url, timeout=60)
200+
total_size = int(response.headers.get('content-length', 0))
201+
202+
downloaded = 0
203+
chunk_size = 65536
204+
last_percent = -1
205+
206+
with open(tmp_path, 'wb') as f:
207+
while True:
208+
chunk = response.read(chunk_size)
209+
if not chunk:
210+
break
211+
f.write(chunk)
212+
downloaded += len(chunk)
213+
214+
if total_size > 0:
215+
percent = int((downloaded / total_size) * 100)
216+
if percent != last_percent and percent % 10 == 0:
217+
mb_downloaded = downloaded / (1024 * 1024)
218+
mb_total = total_size / (1024 * 1024)
219+
print(f" [{percent:3d}%] {mb_downloaded:.1f} MB / {mb_total:.1f} MB")
220+
last_percent = percent
221+
222+
shutil.move(str(tmp_path), str(dest))
247223

248-
Args:
249-
url: URL to download from
250-
dest: Destination file path
251-
"""
252-
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
253-
tmp_path = Path(tmp_file.name)
224+
except Exception:
225+
if tmp_path and tmp_path.exists():
226+
tmp_path.unlink()
227+
raise
254228

255-
try:
256-
response = urllib.request.urlopen(url, timeout=30)
257-
total_size = int(response.headers.get('content-length', 0))
258-
259-
downloaded = 0
260-
chunk_size = 8192
261-
last_percent = -1
262-
263-
with open(tmp_path, 'wb') as f:
264-
while True:
265-
chunk = response.read(chunk_size)
266-
if not chunk:
267-
break
268-
f.write(chunk)
269-
downloaded += len(chunk)
270-
271-
# Show progress
272-
if total_size > 0:
273-
percent = int((downloaded / total_size) * 100)
274-
if percent != last_percent and percent % 10 == 0:
275-
mb_downloaded = downloaded / (1024 * 1024)
276-
mb_total = total_size / (1024 * 1024)
277-
print(f" [{percent:3d}%] {mb_downloaded:.1f} MB / {mb_total:.1f} MB")
278-
last_percent = percent
279-
280-
# Move to final destination
281-
shutil.move(str(tmp_path), str(dest))
282229

283-
except Exception as e:
284-
# Clean up temp file on error
285-
if tmp_path.exists():
286-
tmp_path.unlink()
287-
raise
288-
"""Get information about the MeowthBridge binary.
289-
290-
Returns:
291-
Dictionary with binary information:
292-
- path: Path to the executable
293-
- platform: Platform name (windows/macos/linux)
294-
- source: Where the binary was found (env/bundled/dev)
295-
- exists: Whether the binary exists
296-
"""
230+
def get_binary_info() -> dict:
231+
"""Get information about the MeowthBridge binary."""
297232
try:
298233
exe_path = find_meowth_bridge()
299-
300-
# Determine source
301234
if os.environ.get("MEOWTH_BRIDGE_PATH"):
302235
source = "environment"
236+
elif ".meowth" in str(exe_path):
237+
source = "downloaded"
303238
elif "binaries" in str(exe_path):
304239
source = "bundled"
305240
else:

0 commit comments

Comments
 (0)