packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

packaging: a native Arch package, so pacman owns and tracks the daemon - #8

Merged
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package
Aug 21, 2026
Merged

packaging: a native Arch package, so pacman owns and tracks the daemon#8
Ryanmello07 merged 2 commits into
urnetwork:mainfrom
Ryanmello07:upstream/arch-package

Conversation

@Ryanmello07

@Ryanmello07Ryanmello07 commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked PR — depends on #5 (app id rename).
Opened against main because a cross-fork PR needs its base branch to exist in
this repo, so the diff below currently includes its parent's changes too.
Review after its parent lands.


Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and pacman -R
cannot remove it — and the tarball can only complain about missing nftables or
fuse2after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the way the .deb and
.rpm already are.

Why nfpm and not a PKGBUILD

All the native packages are assembled from onemeson install --destdir
staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the
daemon inside each is the same bytes by construction rather than by review. A
PKGBUILD would be a fourth independent copy of the installed-path table, and it
would need an Arch machine or a container to run makepkg — which neither the
build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down
to its zstd, so this builds anywhere the .deb does.

An AUR recipe remains reasonable later as a discovery channel. It is not a
replacement for a first-party binary package.

Three things make-arch.sh does that make-deb.sh does not

Each because pacman fails in a way the other formats do not:

  1. It moves the unit to /usr/lib/systemd/system. On Arch, /lib is a
    symlink to usr/lib owned by the filesystem package, and a .pkg.tar.zst
    carrying any member under lib/ does not merely offend a guideline — pacman
    aborts the transaction with /lib exists in filesystem (owned by filesystem) and installs nothing.

  2. It folds the whole version into pkgver, because pacman's pkgver may not
    contain - at all and nfpm silently mangles the other obvious shapes. The
    file is still named for the release version verbatim: pacman does not parse
    filenames, it reads .PKGINFO.

  3. It pins one timestamp and verifies the .MTREE, because pacman -Qkk
    the integrity check a careful user runs against a VPN daemon — is broken by
    two separate nfpm behaviours, both silent. type: tree writes Go's unmasked
    fs.FileMode for every directory, so every directory reports as altered
    forever; and the tar header mtime and the .MTREEtime= come from different
    clocks. Both are worked around, and the script asserts the workarounds held
    rather than trusting them.

Install/remove hooks

They mirror the .deb's maintainer scripts, with the one pacman ordering
difference that matters: pre_remove stops and disables the unit while the
binary still exists
, so its ExecStopPost can tear the nftables ruleset down.

Not for SteamOS

SteamOS is Arch-family but immutable. Its /usr is read-only, pacman -U there
needs steamos-readonly disable, and the next system update reverts it.
Immutable Arch hosts stay on the install tarball, which installs under
/usr/local. packaging/distro-smoke.sh says so on a SteamOS host.

The tarball guards were hypothetical; now they are live

packaging/tarball/install.sh and uninstall.sh already refused to touch paths
that pacman -Qo reports as owned, but both comments described it as protection
against a future AUR package. It is now a real conflict against our own
package, and both comments say so — otherwise the next reader assumes the check
is theoretical and weakens it.

Verified

bash -n on make-arch.sh, all six packaging/arch/scripts/*, distro-smoke.sh
and both tarball scripts; yaml.safe_load on packaging/arch/nfpm.yaml; every
path make-arch.sh references exists in the tree.

Why this is split this way

It is the single largest body of genuinely new code in the series (≈1400 lines),
it is self-contained, and it needs a reviewer who cares about pacman semantics —
not the same reviewer who should be checking an app id or a screenshot. It only
needs PR 2 underneath it, so it does not have to queue behind the icon and
Flathub work.

One note on comment references

Four comments in packaging/make-arch.sh and packaging/arch/nfpm.yaml cite
.github/workflows/beta-build.yml as the source of the release-asset contract.
That mirrors the existing house style — packaging/make-rpm.sh,
packaging/rpm/nfpm.yaml, docs/DISTRO-SUPPORT.md and docs/linux_agent_help.md
on main all reference that same path today. They were left as-is for
consistency. If you would rather those references be repointed at whatever
workflow owns the contract in this repo, say so and it is a one-line change in
each.

The Android and Apple clients ship under `com.bringyour.network`. Linux was
the only platform on a different reverse-DNS id, and every place the id is
written down had to be told which one to use. This makes Linux match, and it
has to be done in one change because the id is a join key: the GTK
application id, the .desktop basename, the AppStream component id, the polkit
action namespace, the icon-theme name and the Flatpak app id must all agree or
the desktop stops recognising the app.
WHAT MOVES, AND WHY IT IS ALL ONE COMMIT
main.cpp Gtk::Application::create() -- the GApplication id
*.desktop filename, Icon=, StartupWMClass=
metainfo.xml filename, <id>, <launchable>
polkit .policy filename + all four action ids, matched in
ControlProtocol.hpp so the daemon asks about the
actions the file actually declares
icons hicolor basenames; Flatpak refuses to export an icon
whose name is not the app id
flatpak manifest filename + id + the desktop-file-edit paths
deb/rpm/tarball/ the installed paths, the conffile entries, and the
AppImage/snap uninstaller's stale-path list
Splitting these would leave an intermediate commit where, for example, the
.desktop names an icon that does not exist, or the daemon checks polkit
actions the shipped .policy does not declare -- both of which fail silently
at runtime rather than at build time.
TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE
1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two
places -- the by-path load in UrTheme.cpp and the by-name fallback in
MainWindow.cpp. Renaming the packaging alone left both lookups pointing at
a file that no longer existed, and `set_from_icon_name()` renders a blank
image without raising anything, so the title-bar logo simply went empty.
It is now one constant that the packaging and both call sites share.
2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves
with the id. This is deliberately NOT dual-read: an entry written by an
older build is no longer found, the app falls back to a fresh RPC session
(the same one-time cost as the Flatpak data path moving), and the previous
app identity is not left holding live key material in the user's keyring
with nothing to clean it up.
No behaviour changes beyond those two. `network.ur.urnetwork` no longer
appears anywhere in the tree.
Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the
install tarball. That means nothing on the machine knows the daemon is there.
pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R`
cannot remove it -- and the tarball can only complain about missing nftables
or fuse2 after it has already written files. This adds
urnetwork-daemon-<version>-<arch>.pkg.tar.zst, built the same way the .deb and
.rpm are.
WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE
`meson install --destdir` staging tree through packaging/lib/common.sh's
assemble_daemon_root(), so the daemon inside each is the same bytes by
construction rather than by review. A PKGBUILD would be a fourth independent
copy of the installed-path table, and it would need an Arch machine or a
container to run makepkg -- which neither the build server nor the workflow's
ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds
anywhere the .deb does. An AUR recipe remains reasonable later as a discovery
channel; it is not a replacement for a first-party binary package.
THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman
fails in a way the other formats do not:
1. It MOVES THE UNIT to /usr/lib/systemd/system. On Arch /lib is a symlink to
usr/lib owned by the `filesystem` package, and a .pkg.tar.zst carrying any
member under lib/ does not merely offend a guideline -- pacman ABORTS the
transaction with "/lib exists in filesystem (owned by filesystem)" and
installs nothing.
2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not
contain '-' at all and nfpm silently mangles the other obvious shapes. The
file is still named for the release version verbatim: pacman does not parse
filenames, it reads .PKGINFO.
3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- the
integrity check a careful user runs against a VPN daemon -- is broken by two
separate nfpm behaviours, both silent. `type: tree` writes Go's unmasked
fs.FileMode for every directory, so every directory reports as altered
forever; and the tar header mtime and the .MTREE `time=` come from different
clocks. Both are worked around, and the script asserts the workarounds held
rather than trusting them.
The install/remove hooks mirror the .deb's maintainer scripts, with the pacman
ordering difference that matters: pre_remove stops and disables the unit while
the binary still exists, so its ExecStopPost can tear the nftables ruleset down.
NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only,
`pacman -U` there needs `steamos-readonly disable`, and the next system update
reverts it. Immutable Arch hosts stay on the install tarball, which installs
under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host.
The tarball installer and uninstaller already refused to touch paths that
`pacman -Qo` reports as owned. That guard was written as protection against a
hypothetical future AUR package; it is now a live conflict, and both comments
are updated to say so rather than leaving the next reader to assume the check
is theoretical.
@Ryanmello07Ryanmello07 changed the title PR 6 — upstream/arch-packagepackaging: a native Arch package, so pacman owns and tracks the daemonAug 21, 2026
@Ryanmello07
Ryanmello07 marked this pull request as ready for review August 21, 2026 15:40
@Ryanmello07
Ryanmello07 merged commit 7d371b1 into urnetwork:mainAug 21, 2026
3 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Ryanmello07