From 83f4c3cc61f473136919c7020e1ce5b215065d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Tue, 6 Oct 2026 08:21:04 +0300 Subject: [PATCH 1/2] Fix Flatpak XDG folders and make release packaging idempotent Honour XDG_*_DIR env vars, persist the Quick access seed only when folders were found, upload CI assets to an existing (draft) release with --clobber, attach SHA256SUMS and the AUR recipe, and document the publish procedure. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- .github/workflows/package-linux.yml | 25 +++++++++++++-- docs/linux-port/packaging.md | 32 ++++++++++++++++++- scripts/linux/gen-aur.sh | 27 +++++++++++++++- .../Services/DesktopQuickAccessService.cs | 9 ++++-- .../AppData/LinuxUserDirectories.cs | 8 +++++ .../AppData/AppDataTests.cs | 20 ++++++++++++ 6 files changed, 114 insertions(+), 7 deletions(-) diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml index f3cc57d85577..f2d21fc8f6e9 100644 --- a/.github/workflows/package-linux.yml +++ b/.github/workflows/package-linux.yml @@ -100,9 +100,28 @@ jobs: name: files-linux-x64 path: dist - - name: Create GitHub release + - name: Checksums and AUR recipe assets + run: | + cd dist + sha256sum files-linux-x64.tar.gz files-packaging.tar.gz Files-x86_64.AppImage Files-x86_64.flatpak > SHA256SUMS + # Release asset names may not start with a dot, so .SRCINFO is attached as SRCINFO. + cp aur/PKGBUILD PKGBUILD + cp aur/.SRCINFO SRCINFO + + - name: Create or update GitHub release env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} run: | - gh release create "$GITHUB_REF_NAME" -R "$GITHUB_REPOSITORY" --title "$GITHUB_REF_NAME" --generate-notes \ - dist/files-linux-x64.tar.gz dist/files-packaging.tar.gz dist/Files-x86_64.AppImage dist/Files-x86_64.flatpak + cd dist + assets=(files-linux-x64.tar.gz files-packaging.tar.gz Files-x86_64.AppImage Files-x86_64.flatpak SHA256SUMS PKGBUILD SRCINFO) + # `gh release list` includes drafts; a tag lookup does not, so a pre-made draft is found here. + if gh release list --limit 200 --json tagName --jq '.[].tagName' | grep -Fxq "$TAG"; then + # Draft or already published: keep its notes and state, replace the assets with the fresh build. + gh release upload "$TAG" "${assets[@]}" --clobber + else + flags=() + [[ "$TAG" == *-* ]] && flags+=(--prerelease) + gh release create "$TAG" "${assets[@]}" --title "$TAG" --generate-notes "${flags[@]}" + fi diff --git a/docs/linux-port/packaging.md b/docs/linux-port/packaging.md index 91ead674b1ff..d729cbc8b811 100644 --- a/docs/linux-port/packaging.md +++ b/docs/linux-port/packaging.md @@ -87,7 +87,17 @@ unpacks the self-contained publish tarball, so there is no .NET SDK, NuGet resto (Flathub-friendly: for a release, point the `file` source at the release URL with its sha256). `scripts/linux/build-flatpak.sh [--bundle]` runs `flatpak-builder` (needs the 25.08 Platform and Sdk installed) into `artifacts/flatpak-repo` and optionally writes `artifacts/Files-x86_64.flatpak`; without `flatpak-builder` it only -validates the manifest structure. Built locally; not yet installed or run inside the sandbox. +validates the manifest structure. Built locally, installed from a throwaway `FLATPAK_USER_DIR` and run inside the sandbox on a private Xvfb. + +XDG user folders (Quick access / Pinned): `user-dirs.dirs` is visible in the sandbox (flatpak bind-mounts the host +file at `$XDG_CONFIG_HOME`, which is remapped to `~/.var/app//config`), `$HOME` is the real home, and +`--filesystem=host` exposes the folders, so no extra finish-arg is needed (`xdg-*` permissions would be redundant). +With a seeded real-path HOME the Desktop/Downloads/Documents/Pictures/Music/Videos pins appear, also when +`user-dirs.dirs` is missing (fallback to `$HOME/`). What does hide them is a HOME under `/tmp`: +flatpak gives the app a private `/tmp`, so a HOME seeded there looks empty (only the Recycle Bin is pinned). Seed +test homes outside `/tmp`. Defensive changes: `LinuxUserDirectories` honours `XDG__DIR` environment variables +before the file, and the first-run seed of `pinned_folders.json` is persisted only when at least one folder was +found, so a launch that could not see the folders retries instead of freezing an empty list. `finish-args` rationale (each line is the minimum for a feature that exists in the code): @@ -115,6 +125,26 @@ files, which is how the package was build-tested with `makepkg` here (resulting installed on the dev box, so the package is not linted. `aur/linuxfiles/PKGBUILD` (from source) is unchanged and unbuilt. +### Publishing a release (exact procedure) + +The draft release `linux-vX.Y.Z[-pre]` already holds hand-uploaded assets; CI replaces them with its own build. + +1. Undraft the release (GitHub UI, or `gh release edit linux-vX.Y.Z-pre -R MemerGamer/LinuxFiles --draft=false`). + Publishing creates the tag, which triggers `package-linux.yml`. The `release` job finds the existing release + (drafts included) and runs `gh release upload --clobber` for `files-linux-x64.tar.gz`, `files-packaging.tar.gz`, + `Files-x86_64.AppImage`, `Files-x86_64.flatpak`, a regenerated `SHA256SUMS`, and the AUR `PKGBUILD` and `SRCINFO` + (the `.SRCINFO`; release assets cannot start with a dot). Notes and prerelease state are untouched. If no release + exists for the tag, it creates one (prerelease when the tag contains `-`). Wait for the run to finish: the CI + tarballs differ from the hand-built ones, so the checksums in the PKGBUILD only match the CI assets. +2. Download the CI's recipe and commit it in the AUR clone: + ``` + cd ~/Documents/aur/linuxfiles-bin + gh release download linux-vX.Y.Z-pre -R MemerGamer/LinuxFiles -p PKGBUILD -p SRCINFO --clobber + mv -f SRCINFO .SRCINFO + git add PKGBUILD .SRCINFO && git commit -m "Update to X.Y.Z-pre" + ``` +3. `git push aur` (from the same directory; the remote is the AUR `linuxfiles-bin` repo). + ## Nightly builds `.github/workflows/nightly-linux.yml` builds the tarball and AppImage on every push to `main` and replaces the diff --git a/scripts/linux/gen-aur.sh b/scripts/linux/gen-aur.sh index f53b02296a69..ee2215090482 100755 --- a/scripts/linux/gen-aur.sh +++ b/scripts/linux/gen-aur.sh @@ -28,5 +28,30 @@ if base: t = re.sub(r'^_base=.*$', '_base="%s"' % base, t, flags=re.M) open(dst, "w").write(t) PY -(cd "$out" && makepkg --printsrcinfo > .SRCINFO) +# makepkg only exists on Arch; elsewhere (CI runs on Ubuntu) emit the same fields by sourcing the PKGBUILD. +srcinfo() { + if command -v makepkg >/dev/null 2>&1 && [[ "${AUR_SRCINFO_FALLBACK:-}" != 1 ]]; then + (cd "$out" && makepkg --printsrcinfo) + return + fi + ( + # shellcheck disable=SC1091 + source "$out/PKGBUILD" + list() { local k="$1"; shift; local v; for v in "$@"; do printf '\t%s = %s\n' "$k" "$v"; done; } + printf 'pkgbase = %s\n' "$pkgname" + printf '\tpkgdesc = %s\n\tpkgver = %s\n\tpkgrel = %s\n\turl = %s\n' "$pkgdesc" "$pkgver" "$pkgrel" "$url" + list arch "${arch[@]}" + list license "${license[@]}" + list depends "${depends[@]}" + list optdepends "${optdepends[@]}" + list provides "${provides[@]}" + list conflicts "${conflicts[@]}" + list noextract "${noextract[@]}" + list options "${options[@]}" + list source "${source[@]}" + list sha256sums "${sha256sums[@]}" + printf '\npkgname = %s\n' "$pkgname" + ) +} +srcinfo > "$out/.SRCINFO" echo "Wrote $out/PKGBUILD and $out/.SRCINFO" diff --git a/src/Files.App/Platforms/Desktop/Services/DesktopQuickAccessService.cs b/src/Files.App/Platforms/Desktop/Services/DesktopQuickAccessService.cs index b9d55b91ec4f..671a15bc7b68 100644 --- a/src/Files.App/Platforms/Desktop/Services/DesktopQuickAccessService.cs +++ b/src/Files.App/Platforms/Desktop/Services/DesktopQuickAccessService.cs @@ -57,11 +57,16 @@ private List Read() defaults.AddRange(ReadGtkBookmarks().Where(p => !defaults.Contains(p))); // The trash is pinned by default like on other desktops + var foundFolders = defaults.Count > 1 || defaults.Count == 1 && defaults[0] != Constants.UserEnvironmentPaths.RecycleBinPath; defaults.Add(Constants.UserEnvironmentPaths.RecycleBinPath); - try { Write(defaults); } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + // Persist only a real seed, so a first run that could not see the user folders retries next launch. + if (foundFolders) { + try { Write(defaults); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + } } return defaults; diff --git a/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs b/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs index 6ef19d9147fe..8612b6f6cb7b 100644 --- a/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs +++ b/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs @@ -28,6 +28,7 @@ public LinuxUserDirectories() public LinuxUserDirectories(Func getEnvironmentVariable, string home) { Home = home; + _getEnvironmentVariable = getEnvironmentVariable; var configHome = getEnvironmentVariable("XDG_CONFIG_HOME"); if (string.IsNullOrEmpty(configHome) || !Path.IsPathRooted(configHome)) @@ -72,8 +73,15 @@ public LinuxUserDirectories(Func getEnvironmentVariable, string /// public string PublicShare { get; } + private readonly Func _getEnvironmentVariable; + private string Resolve(Dictionary configured, string key, string defaultName) { + // XDG__DIR in the environment (set by some sandboxes and sessions) wins over the file. + if (_getEnvironmentVariable($"XDG_{key}_DIR") is { Length: > 0 } fromEnv + && Path.IsPathRooted(fromEnv) && !IsHome(fromEnv)) + return fromEnv; + // Like xdg-user-dirs, a value equal to $HOME means the directory is disabled. return configured.TryGetValue(key, out var path) && !IsHome(path) ? path diff --git a/tests/Files.Platform.Tests/AppData/AppDataTests.cs b/tests/Files.Platform.Tests/AppData/AppDataTests.cs index 6a00110572b6..ead6bbfb18a9 100644 --- a/tests/Files.Platform.Tests/AppData/AppDataTests.cs +++ b/tests/Files.Platform.Tests/AppData/AppDataTests.cs @@ -134,6 +134,26 @@ public void UserDirectories_ReadsFromXdgConfigHome() Assert.AreEqual(Path.Combine(_root, "D"), d.Desktop); } + [TestMethod] + public void UserDirectories_EnvironmentVariablesOverrideFile() + { + var cfg = Path.Combine(_root, "cfg"); + Directory.CreateDirectory(cfg); + File.WriteAllText(Path.Combine(cfg, "user-dirs.dirs"), "XDG_DESKTOP_DIR=\"$HOME/D\"\nXDG_DOWNLOAD_DIR=\"$HOME/F\"\n"); + + var d = new LinuxUserDirectories(Env(new() + { + ["XDG_CONFIG_HOME"] = cfg, + ["XDG_DESKTOP_DIR"] = "/data/desk", + ["XDG_DOWNLOAD_DIR"] = "relative", + ["XDG_MUSIC_DIR"] = _root, + }), _root); + + Assert.AreEqual("/data/desk", d.Desktop); + Assert.AreEqual(Path.Combine(_root, "F"), d.Downloads, "relative env value ignored"); + Assert.AreEqual(Path.Combine(_root, "Music"), d.Music, "$HOME means disabled"); + } + [TestMethod] public void Settings_RoundTripsSupportedTypes_AcrossInstances() { From 611d407fd9c06b372970e85014955d9d7633d6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kov=C3=A1cs=20B=C3=A1lint=20Hunor?= Date: Tue, 6 Oct 2026 10:00:14 +0300 Subject: [PATCH 2/2] Address review: validate AUR version, keep real tag, stable vs prerelease, direct release lookup Co-Authored-By: Claude Sonnet 5.5 Reviewed-by: GPT-6.1-sol (OpenAI Codex) Claude-Session: https://claude.ai/code/session_01LeKXJc3DnK3PzAqwgA72NY --- .github/workflows/package-linux.yml | 7 ++++--- docs/linux-port/packaging.md | 2 +- packaging/linux/aur/linuxfiles-bin/PKGBUILD | 5 +++-- scripts/linux/gen-aur.sh | 16 ++++++++++------ .../AppData/LinuxUserDirectories.cs | 5 ++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/.github/workflows/package-linux.yml b/.github/workflows/package-linux.yml index f2d21fc8f6e9..43498a61a255 100644 --- a/.github/workflows/package-linux.yml +++ b/.github/workflows/package-linux.yml @@ -116,12 +116,13 @@ jobs: run: | cd dist assets=(files-linux-x64.tar.gz files-packaging.tar.gz Files-x86_64.AppImage Files-x86_64.flatpak SHA256SUMS PKGBUILD SRCINFO) - # `gh release list` includes drafts; a tag lookup does not, so a pre-made draft is found here. - if gh release list --limit 200 --json tagName --jq '.[].tagName' | grep -Fxq "$TAG"; then + # A direct lookup also finds drafts for a token with write access. + if gh release view "$TAG" >/dev/null 2>&1; then # Draft or already published: keep its notes and state, replace the assets with the fresh build. gh release upload "$TAG" "${assets[@]}" --clobber else flags=() - [[ "$TAG" == *-* ]] && flags+=(--prerelease) + # linux-v0.1.0-alpha1 is a prerelease; linux-v0.1.0 is stable. + [[ "${TAG#linux-v}" == *-* ]] && flags+=(--prerelease) gh release create "$TAG" "${assets[@]}" --title "$TAG" --generate-notes "${flags[@]}" fi diff --git a/docs/linux-port/packaging.md b/docs/linux-port/packaging.md index d729cbc8b811..681ac9eb81aa 100644 --- a/docs/linux-port/packaging.md +++ b/docs/linux-port/packaging.md @@ -134,7 +134,7 @@ The draft release `linux-vX.Y.Z[-pre]` already holds hand-uploaded assets; CI re (drafts included) and runs `gh release upload --clobber` for `files-linux-x64.tar.gz`, `files-packaging.tar.gz`, `Files-x86_64.AppImage`, `Files-x86_64.flatpak`, a regenerated `SHA256SUMS`, and the AUR `PKGBUILD` and `SRCINFO` (the `.SRCINFO`; release assets cannot start with a dot). Notes and prerelease state are untouched. If no release - exists for the tag, it creates one (prerelease when the tag contains `-`). Wait for the run to finish: the CI + exists for the tag, it creates one (prerelease when the version after `linux-v` contains `-`). `gen-aur.sh` validates the version strictly and keeps the real tag for the download URLs (`_tag`), while `pkgver` drops hyphens. Wait for the run to finish: the CI tarballs differ from the hand-built ones, so the checksums in the PKGBUILD only match the CI assets. 2. Download the CI's recipe and commit it in the AUR clone: ``` diff --git a/packaging/linux/aur/linuxfiles-bin/PKGBUILD b/packaging/linux/aur/linuxfiles-bin/PKGBUILD index 4af359f24a67..56e4d0a3b4cb 100644 --- a/packaging/linux/aur/linuxfiles-bin/PKGBUILD +++ b/packaging/linux/aur/linuxfiles-bin/PKGBUILD @@ -5,7 +5,8 @@ # Regenerate checksums and .SRCINFO for a release with scripts/linux/gen-aur.sh. pkgname=linuxfiles-bin _pkgname=linuxfiles -pkgver=0.1.0alpha1 +_tag=0.1.0-alpha1 # release tag without the linux-v prefix; scripts/linux/gen-aur.sh sets it +pkgver=0.1.0alpha1 # the tag with hyphens removed pkgrel=1 pkgdesc='LinuxFiles, Files for Linux: unofficial port of Files by the Files Community (Uno Platform), prebuilt binaries' arch=('x86_64') @@ -20,7 +21,7 @@ optdepends=('gvfs: network and MTP locations' provides=("$_pkgname") conflicts=("$_pkgname") options=('!strip' '!debug') # stripping breaks the self-contained .NET binaries -_base="$url/releases/download/linux-v${pkgver/alpha/-alpha}" # 0.1.0alpha1 -> tag linux-v0.1.0-alpha1 +_base="$url/releases/download/linux-v$_tag" source=("$pkgname-$pkgver.tar.gz::$_base/files-linux-x64.tar.gz" "$pkgname-packaging-$pkgver.tar.gz::$_base/files-packaging.tar.gz") noextract=("$pkgname-$pkgver.tar.gz" "$pkgname-packaging-$pkgver.tar.gz") diff --git a/scripts/linux/gen-aur.sh b/scripts/linux/gen-aur.sh index ee2215090482..53bbb5f7725d 100755 --- a/scripts/linux/gen-aur.sh +++ b/scripts/linux/gen-aur.sh @@ -7,8 +7,11 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -ver="${1:?version required, e.g. 0.1.0}" -ver="${ver//-/}" # pkgver may not contain hyphens: 0.1.0-alpha1 -> 0.1.0alpha1 +tag="${1:?version required, e.g. 0.1.0 or 0.1.0-alpha1}" +[[ "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]] || { echo "Invalid version: $tag" >&2; exit 2; } +if [[ -n "${AUR_BASE_URL:-}" && ! "$AUR_BASE_URL" =~ ^[A-Za-z0-9:/._~%@+-]+$ ]]; then + echo "Invalid AUR_BASE_URL" >&2; exit 2 +fi dir="${2:?directory with release tarballs required}" out="${3:-$root/artifacts/aur}" tpl="$root/packaging/linux/aur/linuxfiles-bin/PKGBUILD" @@ -18,14 +21,15 @@ bin_sum="$(sum files-linux-x64.tar.gz)" pkg_sum="$(sum files-packaging.tar.gz)" mkdir -p "$out" -python3 - "$tpl" "$out/PKGBUILD" "$ver" "$bin_sum" "$pkg_sum" "${AUR_BASE_URL:-}" <<'PY' +python3 - "$tpl" "$out/PKGBUILD" "$tag" "$bin_sum" "$pkg_sum" "${AUR_BASE_URL:-}" <<'PY' import re, sys tpl, dst, ver, a, b, base = sys.argv[1:7] t = open(tpl).read() -t = re.sub(r"^pkgver=.*$", "pkgver=" + ver, t, flags=re.M) -t = re.sub(r"sha256sums=\('SKIP'\n\s+'SKIP'\)", "sha256sums=('%s'\n '%s')" % (a, b), t) +t = re.sub(r"^_tag=.*$", lambda m: "_tag=" + ver, t, flags=re.M) +t = re.sub(r"^pkgver=.*$", lambda m: "pkgver=" + ver.replace("-", ""), t, flags=re.M) +t = re.sub(r"sha256sums=\('SKIP'\n\s+'SKIP'\)", lambda m: "sha256sums=('%s'\n '%s')" % (a, b), t) if base: - t = re.sub(r'^_base=.*$', '_base="%s"' % base, t, flags=re.M) + t = re.sub(r'^_base=.*$', lambda m: '_base="%s"' % base, t, flags=re.M) open(dst, "w").write(t) PY # makepkg only exists on Arch; elsewhere (CI runs on Ubuntu) emit the same fields by sourcing the PKGBUILD. diff --git a/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs b/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs index 8612b6f6cb7b..8d16d11e1f0e 100644 --- a/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs +++ b/src/Files.Platform.Linux/AppData/LinuxUserDirectories.cs @@ -78,9 +78,8 @@ public LinuxUserDirectories(Func getEnvironmentVariable, string private string Resolve(Dictionary configured, string key, string defaultName) { // XDG__DIR in the environment (set by some sandboxes and sessions) wins over the file. - if (_getEnvironmentVariable($"XDG_{key}_DIR") is { Length: > 0 } fromEnv - && Path.IsPathRooted(fromEnv) && !IsHome(fromEnv)) - return fromEnv; + if (_getEnvironmentVariable($"XDG_{key}_DIR") is { Length: > 0 } fromEnv && Path.IsPathRooted(fromEnv)) + return IsHome(fromEnv) ? Path.Combine(Home, defaultName) : fromEnv; // Like xdg-user-dirs, a value equal to $HOME means the directory is disabled. return configured.TryGetValue(key, out var path) && !IsHome(path)