Skip to content

Commit 5efed5b

Browse files
committed
common: add shared FLS binary download utilities
Add a shared module for FLS (Flasher) binary download utilities that can be used by multiple drivers. Update flashers driver to use the shared get_fls_github_url() function. Signed-off-by: Benny Zlotnik <bzlotnik@redhat.com> Assisted-by: claude-opus-4.5
1 parent 96e1a1e commit 5efed5b

4 files changed

Lines changed: 183 additions & 3 deletions

File tree

python/packages/jumpstarter-driver-flashers/jumpstarter_driver_flashers/client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from jumpstarter.client.decorators import driver_click_group
2727
from jumpstarter.common.exceptions import ArgumentError, JumpstarterException
28+
from jumpstarter.common.fls import get_fls_github_url
2829

2930

3031
class FlashError(JumpstarterException):
@@ -533,8 +534,8 @@ def _flash_with_fls(
533534
self._download_fls_binary(console, prompt, fls_binary_url, f"Failed to download FLS from {fls_binary_url}")
534535
elif fls_version != "":
535536
self.logger.info(f"Downloading FLS version {fls_version} from GitHub releases")
536-
# Download fls binary to the target device (until it is available on the target device)
537-
fls_url = f"https://github.com/jumpstarter-dev/fls/releases/download/{fls_version}/fls-aarch64-linux"
537+
# Download fls binary to the target device (always aarch64 for target devices)
538+
fls_url = get_fls_github_url(fls_version, arch="aarch64")
538539
self._download_fls_binary(console, prompt, fls_url, f"Failed to download FLS from {fls_url}")
539540

540541
# Flash the image
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1+
from .fls import download_fls, get_fls_binary, get_fls_github_url
12
from .metadata import Metadata
23
from .tempfile import TemporarySocket, TemporaryTcpListener, TemporaryUnixListener
34

4-
__all__ = ["Metadata", "TemporarySocket", "TemporaryUnixListener", "TemporaryTcpListener"]
5+
__all__ = [
6+
"Metadata",
7+
"TemporarySocket",
8+
"TemporaryUnixListener",
9+
"TemporaryTcpListener",
10+
"download_fls",
11+
"get_fls_binary",
12+
"get_fls_github_url",
13+
]
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""FLS (Flasher) binary download utilities.
2+
3+
This module provides functions for downloading the FLS binary tool
4+
used for flashing devices via fastboot with OCI image support.
5+
"""
6+
7+
import logging
8+
import platform
9+
import tempfile
10+
import urllib.request
11+
from pathlib import Path
12+
13+
logger = logging.getLogger(__name__)
14+
15+
FLS_GITHUB_REPO = "jumpstarter-dev/fls"
16+
17+
18+
def get_fls_github_url(version: str, arch: str | None = None) -> str:
19+
"""Get GitHub release URL for FLS version.
20+
21+
Args:
22+
version: FLS version (e.g., "0.1.9")
23+
arch: Target architecture (e.g., "aarch64", "x86_64"). If None,
24+
auto-detects from current platform.
25+
26+
Returns:
27+
Download URL for the architecture-appropriate binary
28+
"""
29+
if arch is None:
30+
arch = platform.machine().lower()
31+
else:
32+
arch = arch.lower()
33+
if arch in ("aarch64", "arm64"):
34+
binary_name = "fls-aarch64-linux"
35+
elif arch in ("x86_64", "amd64"):
36+
binary_name = "fls-x86_64-linux"
37+
else:
38+
binary_name = "fls-aarch64-linux" # Default to aarch64
39+
40+
return f"https://github.com/{FLS_GITHUB_REPO}/releases/download/{version}/{binary_name}"
41+
42+
43+
def download_fls(url: str) -> str:
44+
"""Download FLS binary from URL to a temp file.
45+
46+
Args:
47+
url: URL to download FLS binary from
48+
49+
Returns:
50+
Path to the downloaded binary
51+
"""
52+
fd, binary_path = tempfile.mkstemp(prefix="fls-")
53+
try:
54+
logger.info(f"Downloading FLS binary from: {url}")
55+
urllib.request.urlretrieve(url, binary_path)
56+
Path(binary_path).chmod(0o755)
57+
logger.info(f"FLS binary downloaded to: {binary_path}")
58+
return binary_path
59+
except Exception as e:
60+
Path(binary_path).unlink(missing_ok=True)
61+
logger.error(f"Failed to download FLS from {url}: {e}")
62+
raise RuntimeError(f"Failed to download FLS from {url}: {e}") from e
63+
64+
65+
def get_fls_binary(
66+
fls_version: str | None = None,
67+
fls_binary_url: str | None = None,
68+
) -> str:
69+
"""Get path to FLS binary.
70+
71+
Priority order:
72+
1. fls_binary_url - custom URL (highest priority)
73+
2. fls_version - GitHub release version
74+
3. System PATH - fallback to 'fls' command
75+
76+
Args:
77+
fls_version: FLS version to download from GitHub releases
78+
fls_binary_url: Custom URL to download FLS binary from
79+
80+
Returns:
81+
Path to the FLS binary or 'fls' if using system PATH
82+
"""
83+
if fls_binary_url:
84+
logger.info(f"Using custom FLS binary URL: {fls_binary_url}")
85+
return download_fls(fls_binary_url)
86+
87+
if fls_version:
88+
github_url = get_fls_github_url(fls_version)
89+
logger.info(f"Using FLS version {fls_version} from GitHub")
90+
return download_fls(github_url)
91+
92+
logger.debug("Using FLS from system PATH")
93+
return "fls"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from .fls import download_fls, get_fls_binary, get_fls_github_url
6+
7+
8+
@pytest.mark.parametrize(
9+
"arch,version,expected_binary",
10+
[
11+
("aarch64", "0.1.9", "fls-aarch64-linux"),
12+
("arm64", "0.1.9", "fls-aarch64-linux"),
13+
("x86_64", "0.2.0", "fls-x86_64-linux"),
14+
("amd64", "0.2.0", "fls-x86_64-linux"),
15+
("unknown", "0.1.9", "fls-aarch64-linux"), # defaults to aarch64
16+
],
17+
)
18+
def test_get_fls_github_url_auto_detect(arch, version, expected_binary):
19+
"""Test architecture auto-detection from platform.machine()"""
20+
with patch("platform.machine", return_value=arch):
21+
url = get_fls_github_url(version)
22+
assert url == f"https://github.com/jumpstarter-dev/fls/releases/download/{version}/{expected_binary}"
23+
24+
25+
@pytest.mark.parametrize(
26+
"arch,version,expected_binary",
27+
[
28+
("aarch64", "0.1.9", "fls-aarch64-linux"),
29+
("AARCH64", "0.1.9", "fls-aarch64-linux"), # case insensitive
30+
("x86_64", "0.2.0", "fls-x86_64-linux"),
31+
],
32+
)
33+
def test_get_fls_github_url_explicit_arch(arch, version, expected_binary):
34+
"""Test explicit architecture parameter (used by flashers for target device)"""
35+
url = get_fls_github_url(version, arch=arch)
36+
assert url == f"https://github.com/jumpstarter-dev/fls/releases/download/{version}/{expected_binary}"
37+
38+
39+
def test_get_fls_binary_with_custom_url():
40+
with patch("jumpstarter.common.fls.download_fls", return_value="/tmp/custom-fls") as mock_download:
41+
result = get_fls_binary(fls_binary_url="https://example.com/fls")
42+
43+
mock_download.assert_called_once_with("https://example.com/fls")
44+
assert result == "/tmp/custom-fls"
45+
46+
47+
def test_get_fls_binary_with_version():
48+
with patch("jumpstarter.common.fls.download_fls", return_value="/tmp/fls-0.1.9") as mock_download:
49+
with patch("jumpstarter.common.fls.get_fls_github_url", return_value="https://github.com/...") as mock_url:
50+
result = get_fls_binary(fls_version="0.1.9")
51+
52+
mock_url.assert_called_once_with("0.1.9")
53+
mock_download.assert_called_once()
54+
assert result == "/tmp/fls-0.1.9"
55+
56+
57+
def test_get_fls_binary_falls_back_to_path():
58+
result = get_fls_binary()
59+
assert result == "fls"
60+
61+
62+
def test_download_fls_success():
63+
with patch("urllib.request.urlretrieve") as mock_retrieve:
64+
with patch("tempfile.mkstemp", return_value=(0, "/tmp/fls-test")):
65+
with patch("pathlib.Path.chmod"):
66+
result = download_fls("https://example.com/fls")
67+
68+
mock_retrieve.assert_called_once_with("https://example.com/fls", "/tmp/fls-test")
69+
assert result == "/tmp/fls-test"
70+
71+
72+
def test_download_fls_failure():
73+
with patch("urllib.request.urlretrieve", side_effect=Exception("Network error")):
74+
with patch("tempfile.mkstemp", return_value=(0, "/tmp/fls-test")):
75+
with patch("pathlib.Path.unlink"):
76+
with pytest.raises(RuntimeError, match="Failed to download FLS"):
77+
download_fls("https://example.com/fls")

0 commit comments

Comments
 (0)