Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/skills/bazaar.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ metadata:

- Editing Bazaar config in `system_files/bluefin/etc/bazaar/`
- Porting curated-page structure across Bazaar schema versions
- Changing Bazaar hook behavior for app install interception
- Changing Bazaar hook behavior for app install interception (JetBrains, VS Code/Codium, Zed)
- Adding or changing banner images (JXL→PNG conversion pipeline)
- Validating Bazaar behavior locally before opening a PR

Expand All @@ -50,6 +50,8 @@ metadata:
| `tests/test_bazaar_hook.py` | `bazaar-hook` state machine tests |
| `tests/test_curated_config.py` | Curated/Bazaar config shape regression checks |

Both hook scripts must remain synchronized: `hooks.py` (host `/run/host/etc/bazaar/hooks.py`) and `bazaar-hook` (`/usr/libexec/bazaar-hook`) must implement identical hook IDs, stages, and package redirect actions.

## Curated schema and compatibility notes

Bazaar supports two distinct configuration schemas depending on the installed Flatpak version. Because stable releases may lag behind upstream GitHub commits, agents must verify the local version's expected format before editing.
Expand Down Expand Up @@ -168,6 +170,7 @@ just test
- Editing curated content without local preview causes UI regressions to slip through.
- Copying Aurora/Bazaar examples directly can leave non-Bluefin branding or links.
- Changing hook dialog/response IDs must be mirrored in tests to avoid silent behavior drift.
- Editing `system_files/bluefin/etc/bazaar/hooks.py` without applying the same hook handler to `system_files/bluefin/usr/libexec/bazaar-hook` leaves the in-image entry point out of sync.
- Dropping `set -e` from the JXL conversion RUN step lets silent build failures through.
- Using `--color_space=sRGB` instead of `-C sRGB` breaks the conversion with "Unknown argument" error.

Expand Down
18 changes: 18 additions & 0 deletions system_files/bluefin/etc/bazaar/bazaar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,21 @@ hooks:
# styling
style: suggested
shell: exec python3 /run/host/etc/bazaar/hooks.py

- id: zed
when: before-transaction
dialogs:
- id: zed-warning
title: >-
Zed is managed through Homebrew on Bluefin
body: >-
Install Zed from the Universal Blue Homebrew tap for the supported
Bluefin integration.
default-response-id: cancel
options:
- id: cancel
string: "Cancel"
- id: download
string: "Download from Homebrew"
style: suggested
shell: exec python3 /run/host/etc/bazaar/hooks.py
43 changes: 40 additions & 3 deletions system_files/bluefin/etc/bazaar/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def spawn_brew(app):
brew = '/home/linuxbrew/.linuxbrew/bin/brew'
spawn_and_detach([
'flatpak-spawn', '--host', 'xdg-terminal-exec', '-x',
'bash', '-c', f'{brew} install --cask {app}'
'bash', '-c',
f'{brew} tap ublue-os/tap; {brew} trust ublue-os/tap; {brew} install --cask {app}'
])

def handle_jetbrains():
Expand Down Expand Up @@ -95,16 +96,50 @@ def appid_is_code(appid):
case 'action':
try:
if transaction_appid == ('com.vscodium.codium'):
spawn_brew('ublue/tap/vscodium-linux')
spawn_brew('ublue-os/tap/vscodium-linux')
else:
spawn_brew('ublue/tap/visual-studio-code-linux')
spawn_brew('ublue-os/tap/visual-studio-code-linux')
except:
pass
return ''

case 'teardown':
return 'deny'

def handle_zed():

def appid_is_zed(appid):
return appid == 'dev.zed.Zed'

match stage:
case 'setup':
if transaction_type == 'install' and appid_is_zed(transaction_appid):
return 'ok'
else:
return 'pass'

case 'setup-dialog':
return 'ok'

case 'teardown-dialog':
if dialog_response_id == 'download':
return 'ok'
else:
return 'abort'

case 'catch':
return 'abort'

case 'action':
try:
spawn_brew('ublue-os/tap/zed-linux')
except Exception:
pass
return ''

case 'teardown':
return 'deny'

# ---

response = 'pass'
Expand All @@ -113,6 +148,8 @@ def appid_is_code(appid):
response = handle_jetbrains()
case 'code':
response = handle_code()
case 'zed':
response = handle_zed()

print(response)
sys.exit(0)
37 changes: 37 additions & 0 deletions system_files/bluefin/usr/libexec/bazaar-hook
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,41 @@ def handle_code():
case 'teardown':
return 'deny'

def handle_zed():

def appid_is_zed(appid):
return appid == 'dev.zed.Zed'

match stage:
case 'setup':
if transaction_type == 'install' and appid_is_zed(transaction_appid):
return 'ok'
else:
return 'pass'

case 'setup-dialog':
return 'ok'

case 'teardown-dialog':
if dialog_response_id == 'download':
return 'ok'
else:
return 'abort'

case 'catch':
return 'abort'

case 'action':
try:
spawn_brew('ublue-os/tap/zed-linux')
except Exception:
pass
return ''

case 'teardown':
return 'deny'


# ---

response = 'pass'
Expand All @@ -115,6 +150,8 @@ match hook_id:
response = handle_jetbrains()
case 'code':
response = handle_code()
case 'zed':
response = handle_zed()

print(response)
sys.exit(0)
74 changes: 74 additions & 0 deletions tests/test_bazaar_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,80 @@ def test_teardown_returns_deny(self):
assert resp == "deny"


# ---------------------------------------------------------------------------
# Zed hook
# ---------------------------------------------------------------------------

class TestZedHook:
def test_setup_install_zed_returns_ok(self):
resp = _run_hook({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "setup",
"BAZAAR_TS_TYPE": "install",
"BAZAAR_TS_APPID": "dev.zed.Zed",
})
assert resp == "ok"

def test_setup_non_zed_returns_pass(self):
resp = _run_hook({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "setup",
"BAZAAR_TS_TYPE": "install",
"BAZAAR_TS_APPID": "org.mozilla.firefox",
})
assert resp == "pass"

def test_setup_dialog_returns_ok(self):
resp = _run_hook({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "setup-dialog",
})
assert resp == "ok"

def test_teardown_dialog_download_returns_ok(self):
resp = _run_hook({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "teardown-dialog",
"BAZAAR_HOOK_DIALOG_RESPONSE_ID": "download",
})
assert resp == "ok"

def test_teardown_dialog_cancel_returns_abort(self):
resp = _run_hook({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "teardown-dialog",
"BAZAAR_HOOK_DIALOG_RESPONSE_ID": "cancel",
})
assert resp == "abort"

def test_catch_returns_abort(self):
resp = _run_hook({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "catch",
})
assert resp == "abort"

def test_action_spawns_brew_and_returns_empty(self):
resp, popen_calls = _run_hook_with_mock({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "action",
"BAZAAR_TS_APPID": "dev.zed.Zed",
})
assert resp == ""
assert len(popen_calls) == 1
cmd = " ".join(popen_calls[0])
assert "brew tap ublue-os/tap" in cmd
assert "brew trust ublue-os/tap" in cmd
assert "zed-linux" in cmd

def test_teardown_returns_deny(self):
resp = _run_hook({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "teardown",
})
assert resp == "deny"


# ---------------------------------------------------------------------------
# Unknown hook ID
# ---------------------------------------------------------------------------
Expand Down
85 changes: 82 additions & 3 deletions tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@

def _load_hooks(env: dict) -> str:
"""Execute hooks.py under a given environment, return stdout."""
resp, _ = _load_hooks_with_mock(env)
return resp


def _load_hooks_with_mock(env: dict) -> tuple[str, list]:
"""Execute hooks.py under a given environment, return stdout and Popen calls."""
env_defaults = {
"BAZAAR_HOOK_INITIATED_UNIX_STAMP": "0",
"BAZAAR_HOOK_INITIATED_UNIX_STAMP_USEC": "0",
Expand All @@ -40,14 +46,14 @@ def _load_hooks(env: dict) -> str:
env_defaults.update(env)

buf = io.StringIO()
popen_calls = []

with patch.dict(os.environ, env_defaults, clear=True):
# Mock subprocess.Popen so spawn helpers don't actually launch processes
with patch("subprocess.Popen") as mock_popen:
mock_popen.return_value = MagicMock()
mock_popen.side_effect = lambda args, **kwargs: (popen_calls.append(args), MagicMock())[1]
spec = importlib.util.spec_from_file_location("hooks", HOOKS_PATH)
mod = importlib.util.module_from_spec(spec)
# Redirect sys.stdout during exec so print() is captured
old_stdout = sys.stdout
sys.stdout = buf
try:
Expand All @@ -57,7 +63,7 @@ def _load_hooks(env: dict) -> str:
finally:
sys.stdout = old_stdout

return buf.getvalue().strip()
return buf.getvalue().strip(), popen_calls


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -225,6 +231,79 @@ def test_action_codium_returns_empty(self):
assert resp == ""


# ---------------------------------------------------------------------------
# Zed hook
# ---------------------------------------------------------------------------

class TestZedHook:
def test_setup_install_zed_returns_ok(self):
resp = _load_hooks({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "setup",
"BAZAAR_TS_TYPE": "install",
"BAZAAR_TS_APPID": "dev.zed.Zed",
})
assert resp == "ok"

def test_setup_non_zed_returns_pass(self):
resp = _load_hooks({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "setup",
"BAZAAR_TS_TYPE": "install",
"BAZAAR_TS_APPID": "org.gnome.Calculator",
})
assert resp == "pass"

def test_setup_dialog_returns_ok(self):
resp = _load_hooks({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "setup-dialog",
})
assert resp == "ok"

def test_teardown_dialog_download_returns_ok(self):
resp = _load_hooks({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "teardown-dialog",
"BAZAAR_HOOK_DIALOG_RESPONSE_ID": "download",
})
assert resp == "ok"

def test_teardown_dialog_cancel_returns_abort(self):
resp = _load_hooks({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "teardown-dialog",
"BAZAAR_HOOK_DIALOG_RESPONSE_ID": "cancel",
})
assert resp == "abort"

def test_catch_returns_abort(self):
resp = _load_hooks({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "catch",
})
assert resp == "abort"

def test_teardown_returns_deny(self):
resp = _load_hooks({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "teardown",
})
assert resp == "deny"

def test_action_spawns_brew_with_tap_and_trust(self):
resp, popen_calls = _load_hooks_with_mock({
"BAZAAR_HOOK_ID": "zed",
"BAZAAR_HOOK_STAGE": "action",
})
assert resp == ""
assert len(popen_calls) == 1
cmd = " ".join(popen_calls[0])
assert "brew tap ublue-os/tap" in cmd
assert "brew trust ublue-os/tap" in cmd
assert "--trust" not in cmd
assert "zed-linux" in cmd

# ---------------------------------------------------------------------------
# Unknown hook ID
# ---------------------------------------------------------------------------
Expand Down
Loading