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
12 changes: 11 additions & 1 deletion apps/maple-agent/docs/desktop-builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,17 @@ application directory at `~/.local/maple-agent.app` (Dev:
`~/.local/maple-agent-dev.app`), a command in `~/.local/bin`, and a desktop entry.
Python 3 is required for initial installation only. First install
refuses existing installation or integration paths; subsequent upgrades use the
application's updater. Its `AppRun` entrypoint starts the
application's updater. After installation, open Activities / Show Applications
and search for **Maple Work** (or **Maple Work Dev**); that entry can be pinned
to the dock. The download includes `README.txt` with these instructions.

On Ubuntu/GNOME, do not double-click `AppRun` or try to open it inside File Roller
(Archive Manager). Extract the entire archive first, then run `install.sh` from
a terminal as shown above. `AppRun` is a shell script, not a desktop launcher;
GNOME Files need not execute it on double-click. There is no need to change file
associations or script-execution security settings. For a portable launch without
installing, run `./maple-agent.app/AppRun` from a terminal (Dev:
`./maple-agent-dev.app/AppRun`). Its `AppRun` entrypoint starts the
bundled executable and runtime directly, without an AppImage mount or FUSE.
The host must provide its normal CA certificate trust store for TLS validation;
existing user certificate-path overrides remain supported. The isolated CI
Expand Down
31 changes: 31 additions & 0 deletions apps/maple-agent/scripts/linux-bundle-README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Maple Work for Linux
====================

Install once, then launch from your desktop's application menu.

1. Extract the entire .tar.gz archive. Do not open AppRun inside Archive Manager
(File Roller): files inside the archive cannot be launched there.
2. Open a terminal in the extracted application folder (the folder containing
this README.txt and install.sh), then run:

./install.sh

Run as your normal desktop user, without sudo. Python 3 is required for
installation. If extracting in a terminal, use tar -xzpf ARCHIVE.tar.gz to
preserve the required file permissions.
3. Open Activities / Show Applications and search for Maple Work (or Maple Work
Dev for a Dev bundle). Pin that application to your dock if desired.

The installer copies the application into ~/.local and creates its application
menu entry. Keep the extracted folder intact until installation completes; after
that, the installed copy does not depend on the download folder.

AppRun is a terminal entrypoint, not a double-click launcher. GNOME Files may
open shell scripts instead of executing them; this does not mean Maple Work is
unsupported. Do not change your system's file associations or script-execution
security settings. To run without installing, use ./AppRun from a terminal in
this extracted folder.

Already installed? Launch Maple Work from the application menu and use its
built-in updater. The first-install helper refuses to overwrite an existing
installation and leaves account, settings and task data untouched.
4 changes: 3 additions & 1 deletion apps/maple-agent/scripts/linux-install.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ def main():
"unsafe installer lock")
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
paths = install(source, home)
print("Installed " + PROFILES[descriptor["profile"]][3] + ". Launch " + str(paths["wrapper"]))
display = PROFILES[descriptor["profile"]][3]
print("Installed " + display + ". Open your application menu and search for " + display + ".")
print("Or launch from a terminal: " + shlex.quote(str(paths["wrapper"])))
print("Updates are managed by the app. Profile data was not changed.")


Expand Down
1 change: 1 addition & 0 deletions apps/maple-agent/scripts/linux-release-bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def build(binary, output):
stage.run(sys.executable, SCRIPTS / "linux-release-appimage.py", "--stage-only", binary, root)
(root / "AppRun").write_text(bundle_apprun())
shutil.copyfile(SCRIPTS / "linux-install.sh", root / "install.sh")
shutil.copyfile(SCRIPTS / "linux-bundle-README.txt", root / "README.txt")
shutil.copyfile(SCRIPTS / "linux-install.py", root / "usr/share/maple-agent/install.py")
(root / "bundle.json").write_text(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n")
stage.normalize_public_permissions(root)
Expand Down
20 changes: 20 additions & 0 deletions apps/maple-agent/scripts/test_linux_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,26 @@ def test_dev_and_prod_have_distinct_bundle_commands_and_desktop_entries(self):
self.assertIn("Name=Maple Work Dev\n", dev["desktop"].read_text())
self.assertTrue(set(prod.values()).isdisjoint(dev.values()))

@unittest.skipUnless(sys.platform == "linux" and shutil.which("gio"),
"GLib's desktop launcher is required")
def test_gnome_desktop_entry_launches_installed_bundle_after_download_is_removed(self):
home = self.root / "desktop home with 'quotes'"
home.mkdir()
for profile in ("prod", "dev"):
with self.subTest(profile=profile):
source = bundle_fixture(self.root / ("desktop-" + profile), profile)
paths = installer.install(source, home)
shutil.rmtree(source)
# Use the same desktop-entry implementation as GNOME, not a
# shell approximation of Exec. The fixture AppRun only echoes
# arguments; no real app, account or desktop session is used.
result = subprocess.run(
["gio", "launch", str(paths["desktop"]), "https://example.invalid/launch"],
capture_output=True, text=True, timeout=10,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout, "https://example.invalid/launch\n")

def test_every_preexisting_managed_path_is_refused_without_replacement(self):
for kind in ("bundle", "wrapper", "desktop", "icon", "receipt"):
with self.subTest(kind=kind):
Expand Down
34 changes: 34 additions & 0 deletions apps/maple-agent/scripts/test_linux_release_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,40 @@ def test_rejects_content_after_tar_end_markers(self):


class LinuxBundleLauncherTests(unittest.TestCase):
def test_built_archives_ship_readable_install_instructions_for_both_profiles(self):
for profile, root_name in (("prod", ROOT), ("dev", "maple-agent-dev.app")):
with self.subTest(profile=profile), tempfile.TemporaryDirectory() as temporary:
temporary = Path(temporary)
output = temporary / f"maple-agent-{profile}-0.0.4-aaaaaaaa-linux-x86_64.tar.gz"
metadata = {"channel": profile, "version": "0.0.4", "build_number": "42",
"bundle_id": bundle.install.PROFILES[profile][2]}

def stage_fixture(*args):
root = Path(args[-1])
root.mkdir()
for name, data, mode, kind, link in fixture_entries():
relative = Path(name).relative_to(ROOT)
path = root / relative
if kind == tarfile.DIRTYPE:
path.mkdir(exist_ok=True)
elif kind == tarfile.SYMTYPE:
path.symlink_to(link)
else:
path.write_bytes(data)
path.chmod(mode)

with mock.patch.object(bundle.stage, "metadata_from_environment", return_value=metadata), \
mock.patch.object(bundle.stage, "run", side_effect=stage_fixture), \
mock.patch.object(bundle, "audit"), \
mock.patch.dict(os.environ, MAPLE_PACKAGE_SOURCE_SHA="a" * 40, SOURCE_DATE_EPOCH="1"):
bundle.build(temporary / "unused-binary", output)
root = bundle.extract_archive(output, temporary / "extracted", profile)
self.assertEqual(root.name, root_name)
readme = root / "README.txt"
self.assertEqual(readme.read_bytes(), (bundle.SCRIPTS / "linux-bundle-README.txt").read_bytes())
self.assertEqual(stat.S_IMODE(readme.stat().st_mode), 0o644)
self.assertEqual((root / "install.sh").read_bytes(), (bundle.SCRIPTS / "linux-install.sh").read_bytes())

def test_clears_same_appdir_image_state_and_preserves_host_overrides(self):
with tempfile.TemporaryDirectory() as temporary:
app = Path(temporary).resolve() / "maple-agent.app"
Expand Down
Loading