diff --git a/Core.py b/Core.py index 655e207..d300b53 100644 --- a/Core.py +++ b/Core.py @@ -23,7 +23,7 @@ import tomlkit AppName = "SaraSuite" # Compared with the latest Gitea release to offer self-updates (SelfUpdate.py). # Bump it, then run Packaging/Release.py, to publish a new SaraSuite. -AppVersion = "1.1.1" +AppVersion = "1.2.0" IsFrozen = bool(getattr(sys, "frozen", False)) # running from the AppImage / .exe IsWindows = sys.platform == "win32" Platform = "Windows" if IsWindows else "Linux" # matches manifests' Platforms / Platform diff --git a/Packaging/Release.py b/Packaging/Release.py index 4402fe1..05d1314 100755 --- a/Packaging/Release.py +++ b/Packaging/Release.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 -"""Publish a SaraSuite release: build the AppImage, create the Gitea release -v (from Core.py) with it attached, and copy it to the Shared -folder. Running SaraSuites then offer to update themselves (SelfUpdate.py). +"""Publish a SaraSuite release: build the AppImage (Linux) and SaraSuite.exe +(Windows, built under Wine by BuildExe.sh), create the Gitea release +v (from Core.py) with both attached, and copy them to the Shared +folder. Running SaraSuites then offer to update themselves (SelfUpdate.py), +each to its own platform's file. Packaging/Release.py # after bumping AppVersion in Core.py Packaging/Release.py --notes "What changed" @@ -25,7 +27,11 @@ from pathlib import Path from urllib.parse import urlsplit RepoDir = Path(__file__).resolve().parent.parent -AppImage = RepoDir / "dist" / "SaraSuite-x86_64.AppImage" +# Each running SaraSuite downloads the one for its own platform (SelfUpdate.AssetName). +Builds = [ + ("Packaging/BuildAppImage.sh", RepoDir / "dist" / "SaraSuite-x86_64.AppImage"), + ("Packaging/BuildExe.sh", RepoDir / "dist" / "SaraSuite.exe"), +] SharedDir = Path("/mnt/files/Shared/SaraSuite") @@ -93,26 +99,29 @@ def Main() -> None: Range = f"{Previous[0]}..HEAD" if Previous else "HEAD" Notes = Git("log", "--format=- %s", Range) or "- First release with self-updates." - print(f"== Building SaraSuite {Version}") - subprocess.run(["Packaging/BuildAppImage.sh"], cwd=RepoDir, check=True, - env={**os.environ, "PATH": f"{Path.home()}/.local/bin:{os.environ['PATH']}"}) + for Script, Output in Builds: + print(f"== Building {Output.name} {Version}") + subprocess.run([Script], cwd=RepoDir, check=True, + env={**os.environ, "PATH": f"{Path.home()}/.local/bin:{os.environ['PATH']}"}) print(f"== Creating release {Tag}") Created = Api("POST", f"{ApiBase}/releases", TokenValue, json.dumps({ "tag_name": Tag, "target_commitish": Head, "name": f"SaraSuite {Version}", "body": Notes, }).encode()) - print(f"== Uploading {AppImage.name} ({AppImage.stat().st_size // 1_000_000} MB)") - Boundary = uuid.uuid4().hex - Body = ( - f"--{Boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{AppImage.name}\"\r\n" - f"Content-Type: application/octet-stream\r\n\r\n" - ).encode() + AppImage.read_bytes() + f"\r\n--{Boundary}--\r\n".encode() - Api("POST", f"{ApiBase}/releases/{Created['id']}/assets?name={AppImage.name}", TokenValue, Body, - f"multipart/form-data; boundary={Boundary}") + for _Script, Output in Builds: + print(f"== Uploading {Output.name} ({Output.stat().st_size // 1_000_000} MB)") + Boundary = uuid.uuid4().hex + Body = ( + f"--{Boundary}\r\nContent-Disposition: form-data; name=\"attachment\"; filename=\"{Output.name}\"\r\n" + f"Content-Type: application/octet-stream\r\n\r\n" + ).encode() + Output.read_bytes() + f"\r\n--{Boundary}--\r\n".encode() + Api("POST", f"{ApiBase}/releases/{Created['id']}/assets?name={Output.name}", TokenValue, Body, + f"multipart/form-data; boundary={Boundary}") if SharedDir.is_dir(): - shutil.copy2(AppImage, SharedDir / AppImage.name) + for _Script, Output in Builds: + shutil.copy2(Output, SharedDir / Output.name) print(f"== Copied to {SharedDir}") Git("fetch", "--quiet", "--tags", "origin") # the new tag, for the next release's notes print(f"== Released {Tag}: {Origin.scheme}://{Origin.netloc}/{Owner}/{Repo}/releases/tag/{Tag}") diff --git a/README.md b/README.md index 67cfd5a..af484c9 100644 --- a/README.md +++ b/README.md @@ -78,11 +78,34 @@ an option off takes out what it placed and puts back any file it had replaced; t places it. It reapplies the installed commit, so the program itself isn't updated. The **Update** button only shows up when there's an update (or a broken install to repair). +## Windows + +`SaraSuite.exe` is the Windows version. Each build only lists the programs for its own system +(a manifest's `Platforms`), and does everything the Windows way: + +| Linux | Windows | +| --- | --- | +| `~/.config/SaraSuite/` (settings) | `%APPDATA%\SaraSuite\` | +| `~/.local/share/SaraSuite/` (programs, receipts) | `%LOCALAPPDATA%\SaraSuite\` | +| `~/.cache/...` | `%LOCALAPPDATA%\...` | +| desktop entry | Start Menu shortcut (`[[Shortcuts]]`) | +| systemd user service | `ServiceRun.py`, started at login by a shortcut in the Startup folder; its PID is in `%LOCALAPPDATA%\SaraSuite\Services\.pid` | +| system `python3` | the `py` launcher / `python.exe`; if there's none, SaraSuite installs Python for the user with winget | +| AppImage self-update (rename over it) | `.exe` self-update (the running one is renamed to `SaraSuite.old.exe`, deleted on the next start) | + +Programs run on Windows with `pythonw.exe -X utf8` (no console window; UTF-8 mode, since Python +on Windows otherwise reads text files in the old ANSI code page). Nothing needs admin rights. +Linux-only features (SaraFem's file picker portal and default-file-manager option) simply +aren't offered there. + ## Running and building - AppImage: `Packaging/BuildAppImage.sh` builds `dist/SaraSuite-x86_64.AppImage` (PyInstaller one-file build + `appimagetool`, like SaraFem's). When run as an AppImage, the header has an "Add to app menu" button. +- Windows: `Packaging/BuildExe.sh` builds `dist/SaraSuite.exe` with the Windows Python inside a + Wine prefix (`build/WinBuild/`, set up on its first run), since PyInstaller can't build for + Windows from Linux directly. The .exe's header has an "Add to Start menu" button. - From source: `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`, then `Packaging/Launch.sh`. - Without a desktop (e.g. a server), the same file works from a terminal: @@ -98,6 +121,8 @@ places it. It reapplies the installed commit, so the program itself isn't update ## Where things live +On Linux (on Windows, the same layout under `%LOCALAPPDATA%` and `%APPDATA%`; see Windows above): + | Path | What | | --- | --- | | `~/.local/share/SaraSuite/Programs//` | installed program (`App/`) and its `.venv/` | @@ -115,13 +140,20 @@ Manifests are bundled into the AppImage, so a new or changed manifest means rebu Anything the manifest can't describe goes in an optional `Programs//Hooks.py`. Targets, templates and `Set` values can use `{Variables}`: `Name`, `GitHost`, `Home`, `ConfigHome`, -`DataHome`, `CacheHome`, `SystemdUserDir`, `ApplicationsDir`, `DBusServicesDir`, `ProgramDir`, -`AppDir`, `Python` (the venv's python), `ConfigDir`, `DesktopName` (e.g. `COSMIC`), -`Desktop` (e.g. `cosmic`), plus every Question's `Id`. +`DataHome`, `CacheHome`, `ProgramDir`, `AppDir`, `Python` (the venv's python), `PythonW` +(Windows: its console-less pythonw.exe), `ConfigDir`, `DesktopName` (e.g. `COSMIC`), +`Desktop` (e.g. `cosmic`), plus every Question's `Id`. Linux only: `SystemdUserDir`, +`ApplicationsDir`, `DBusServicesDir`. Windows only: `StartMenuDir`, `StartupDir`. + +Every list item (`[[Options]]`, `[[Templates]]`, `[[Services]]`, ...) can carry +`Platform = "Linux"` or `"Windows"` to only apply on that system; items without one apply on both. +In TOML templates, write paths as literal strings (`'{ConfigDir}'`): Windows paths are full of +backslashes, which a `"..."` string would read as escapes. ```toml Name = "Example" Description = "Shown on the program's card." +Platforms = ["Linux", "Windows"] # where it runs (default ["Linux"]) Parent = "SaraSortd" # optional: listed (indented) under that program RestartUnits = ["xdg-desktop-portal.service"] # try-restarted after install/update/remove @@ -161,10 +193,24 @@ Option = "Autostart" # optional: only while the option is ticked Root = false # true: outside $HOME, written through pkexec Executable = false -[[Services]] # systemd --user units, enabled + started +[[Services]] # Linux: systemd --user units, enabled + started +Platform = "Linux" Unit = "Example.service" Option = "Autostart" +[[Services]] # Windows: run by ServiceRun.py from a Startup-folder shortcut +Platform = "Windows" # (also add ../../ServiceRun.py as a Template to {ProgramDir}) +Name = "Example" +Script = "Example.py" # relative to the app folder +WorkingDir = "{ConfigDir}" +Option = "Autostart" + +[[Shortcuts]] # Windows: a Start Menu shortcut (pythonw.exe -X utf8 Script) +Platform = "Windows" +Name = "Example" +Script = "Main.py" # relative to the app folder, which is also its working folder +Icon = "assets/Icon.png" # turned into an .ico + [[IniEdits]] # one key in a file SaraSuite doesn't own; restored on removal File = "{ConfigHome}/mimeapps.list" Section = "Default Applications"