Add Fedora and Arch package recipes (#24)

* Package Ferese for Fedora and Arch releases

* Build release packages alongside workspace checks

* Include dependency licenses and software GPU test drivers

* Limit package checks to release PRs and fix Arch Cargo dependency

* Prepare Ferese 0.1.0-beta

* Release config locks before closing inherited file descriptions

* Install the lock PAM policy for native release checks

* Include xauth in distribution package test dependencies

* Regenerate Arch Git package metadata

* Run package checks with an explicit UTC timezone

* Remove unused bundled fonts and accept installed license receipts

* Format the system font test helper

* Load an installed font for headless text rendering checks

* Isolate package test runtimes and reap container child processes
This commit is contained in:
Samuel Onoja authored and GitHub committed 2026-10-11 14:16:00 +01:00
1 parent 587602b493
commit 1b9fabbbe4
25 files changed
+1019 -170

No files matched your search

+5 -37
View File
@@ -3,12 +3,12 @@ on:
pull_request:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
workflow_call:
permissions:
contents: read
concurrency:
group: release-checks-${{ github.ref }}
group: release-checks-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
workspace:
@@ -69,7 +69,9 @@ jobs:
cargo test --locked -p ferese backends::direct::capture::tests::capture_recomposes_cursor_without_touching_the_display_target -- --ignored
cargo test --locked -p ferese winit::tests::capture_failure_restores_display_target_and_subsequent_frames_render -- --ignored
- name: Test native shell, visibility and resize behavior
run: python3 scripts/ci/run-native-checks.py --suite desktop --log-dir native-logs
run: |
install -m 0644 packaging/pam.d/ferese-lock.fedora /etc/pam.d/ferese-lock
python3 scripts/ci/run-native-checks.py --suite desktop --log-dir native-logs
- name: Upload native test logs
if: always()
uses: actions/upload-artifact@v4
@@ -79,37 +81,3 @@ jobs:
if-no-files-found: ignore
- name: Test installer, session recovery and private portal contracts
run: python3 -m unittest discover -s scripts/tests -v
publish:
name: Publish validated tag
needs: workspace
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/github-script@v8
with:
script: |
const tag = context.ref.slice('refs/tags/'.length);
const repo = context.repo;
const commit = await github.rest.repos.getCommit({...repo, ref: tag});
if (commit.data.sha !== context.sha) {
throw new Error('Release tag moved after validation');
}
let release;
try {
release = (await github.rest.repos.getReleaseByTag({...repo, tag})).data;
} catch (error) {
if (error.status !== 404) throw error;
}
if (release?.draft) {
await github.rest.repos.updateRelease({...repo, release_id: release.id, draft: false});
} else if (!release) {
await github.rest.repos.createRelease({
...repo, tag_name: tag, name: tag,
generate_release_notes: true, prerelease: tag.includes('-')
});
} else {
core.notice(`Release ${tag} is already published`);
}
+173
View File
@@ -0,0 +1,173 @@
name: Release packages
on:
pull_request:
types: [opened, synchronize, reopened, labeled]
push:
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
env:
CARGO_BUILD_JOBS: '2'
CARGO_INCREMENTAL: '0'
CARGO_PROFILE_RELEASE_DEBUG: '0'
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
checks:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'release')
uses: ./.github/workflows/release-checks.yml
source:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v6
- name: Read release version
id: version
env:
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
python3 - <<'PY'
import importlib.util, os
spec = importlib.util.spec_from_file_location('package', 'scripts/package.py')
package = importlib.util.module_from_spec(spec)
spec.loader.exec_module(package)
version = package.version()
package.check_tag(os.environ['RELEASE_TAG'], version)
with open(os.environ['GITHUB_OUTPUT'], 'a') as output:
print(f'version={version}', file=output)
PY
- name: Install pinned Rust toolchain
run: rustup show
- name: Package offline sources and recipes
env:
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || '' }}
run: |
python3 -m unittest discover -s scripts/tests -p test_package.py
python3 scripts/package.py source --tag "$RELEASE_TAG"
- uses: actions/upload-artifact@v7
with:
name: release-source
path: target/dist/*
if-no-files-found: error
compression-level: 0
fedora:
needs: source
runs-on: ubuntu-24.04
container:
image: fedora:44
options: --init
steps:
- name: Install RPM tooling
run: dnf install -y rpm-build dnf5-plugins git findutils tar gzip
- uses: actions/download-artifact@v8
with:
name: release-source
path: dist
- name: Build and test Fedora package
run: |
dnf builddep -y dist/ferese.spec
mkdir -p /tmp/rpmbuild/SOURCES
cp dist/ferese-*.tar.gz /tmp/rpmbuild/SOURCES/
rpmbuild -ba --define '_topdir /tmp/rpmbuild' dist/ferese.spec
mkdir -p artifacts
find /tmp/rpmbuild/RPMS /tmp/rpmbuild/SRPMS -name '*.rpm' -exec cp '{}' artifacts/ \;
- uses: actions/upload-artifact@v7
with:
name: release-fedora
path: artifacts/*.rpm
if-no-files-found: error
compression-level: 0
arch:
needs: source
runs-on: ubuntu-24.04
container:
image: archlinux:base-devel
options: --init
steps:
- name: Prepare Arch build user and dependencies
run: |
pacman -Syu --noconfirm --needed rust cargo cmake clang pkgconf python git desktop-file-utils \
wayland libxkbcommon libinput systemd seatd mesa vulkan-swrast libdrm fontconfig freetype2 expat dbus \
pam polkit foot pipewire wireplumber libpipewire gstreamer xdg-desktop-portal xdg-desktop-portal-gtk xorg-xauth
useradd --create-home builder
- uses: actions/download-artifact@v8
with:
name: release-source
path: dist
- name: Build and test Arch package
run: |
mkdir -p /home/builder/package
cp dist/PKGBUILD dist/ferese-*.tar.gz /home/builder/package/
chown -R builder:builder /home/builder/package
runuser -u builder -- bash -c 'cd /home/builder/package && makepkg --cleanbuild --noconfirm'
mkdir -p artifacts
cp /home/builder/package/*.pkg.tar.zst artifacts/
runuser -u builder -- bash -c 'cd /home/builder/package && makepkg --printsrcinfo' > artifacts/ferese.SRCINFO
- uses: actions/upload-artifact@v7
with:
name: release-arch
path: artifacts/*
if-no-files-found: error
compression-level: 0
publish:
if: github.event_name == 'push' && github.ref_type == 'tag'
needs: [source, checks, fedora, arch]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
pattern: release-*
merge-multiple: true
path: artifacts
- name: Prepare draft release
uses: actions/github-script@v8
with:
script: |
const tag = context.ref.slice('refs/tags/'.length);
const repo = context.repo;
const commit = await github.rest.repos.getCommit({...repo, ref: tag});
if (commit.data.sha !== context.sha) {
throw new Error('Release tag moved after validation');
}
let release;
try {
release = (await github.rest.repos.getReleaseByTag({...repo, tag})).data;
} catch (error) {
if (error.status !== 404) throw error;
}
if (release && !release.draft) {
throw new Error(`Release ${tag} is already published`);
}
if (!release) {
await github.rest.repos.createRelease({
...repo, tag_name: tag, name: tag, draft: true,
generate_release_notes: true, prerelease: tag.includes('-')
});
}
- name: Upload packages and publish completed release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
cd artifacts
rm SHA256SUMS
sha256sum * > SHA256SUMS
gh release upload "$RELEASE_TAG" ./* --clobber
test "$(gh api "repos/$GH_REPO/commits/$RELEASE_TAG" --jq .sha)" = "$GITHUB_SHA"
gh release edit "$RELEASE_TAG" --draft=false
Generated
+16 -16
View File
@@ -1735,7 +1735,7 @@ dependencies = [
[[package]]
name = "ferese"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ab_glyph",
"async-channel",
@@ -1766,7 +1766,7 @@ dependencies = [
[[package]]
name = "ferese-animation"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-layout",
"serde",
@@ -1774,7 +1774,7 @@ dependencies = [
[[package]]
name = "ferese-config"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-theme-model",
"jiff",
@@ -1786,7 +1786,7 @@ dependencies = [
[[package]]
name = "ferese-core"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-layout",
"serde",
@@ -1794,7 +1794,7 @@ dependencies = [
[[package]]
name = "ferese-ipc"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-theme-model",
"libc",
@@ -1804,11 +1804,11 @@ dependencies = [
[[package]]
name = "ferese-layout"
version = "0.1.0"
version = "0.1.0-beta"
[[package]]
name = "ferese-lock"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-config",
"ferese-ipc",
@@ -1828,7 +1828,7 @@ dependencies = [
[[package]]
name = "ferese-polkit-agent"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-config",
"ferese-ipc",
@@ -1847,7 +1847,7 @@ dependencies = [
[[package]]
name = "ferese-protocols"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"bitflags 2.13.2",
"wayland-backend",
@@ -1859,7 +1859,7 @@ dependencies = [
[[package]]
name = "ferese-settings"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-config",
"ferese-ipc",
@@ -1884,7 +1884,7 @@ source = "git+https://github.com/ferese-wm/ferese-shape?tag=v0.1.0#63d7b6dfd520d
[[package]]
name = "ferese-shell"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"base64",
"ferese-animation",
@@ -1912,7 +1912,7 @@ dependencies = [
[[package]]
name = "ferese-theme"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-config",
"ferese-theme-model",
@@ -1924,7 +1924,7 @@ dependencies = [
[[package]]
name = "ferese-theme-client"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"ferese-config",
"ferese-ipc",
@@ -1938,14 +1938,14 @@ dependencies = [
[[package]]
name = "ferese-theme-model"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"serde",
]
[[package]]
name = "feresectl"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"clap",
"ferese-config",
@@ -7702,7 +7702,7 @@ checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
[[package]]
name = "xdg-desktop-portal-ferese"
version = "0.1.0"
version = "0.1.0-beta"
dependencies = [
"calloop 0.14.4",
"dirs",
+1 -1
View File
@@ -20,7 +20,7 @@ members = [
resolver = "3"
[workspace.package]
version = "0.1.0"
version = "0.1.0-beta"
edition = "2024"
license = "MIT"
rust-version = "1.95"
+2
View File
@@ -58,6 +58,8 @@ also include desktop controls; their features depend on the shell and tools you
## Install
Fedora and Arch package builds are covered in [Packages and releases](docs/packaging.md).
Start with the [dependencies](docs/installation.md#requirements), then clone Ferese and
build it as your normal user:
-94
View File
@@ -1,94 +0,0 @@
Copyright 2011 The Comfortaa Project Authors
(https://github.com/alexeiva/comfortaa), with Reserved Font Name "Comfortaa".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+3 -2
View File
@@ -642,8 +642,9 @@ mod tests {
}
#[test]
fn removed_desktop_widgets_are_rejected_before_runtime_publication() {
assert!(Config::parse_source("desktop-widgets { clock { enabled #true; }; }").is_err());
fn removed_desktop_widgets_allow_runtime_publication() {
let config = Config::parse_source("desktop-widgets { clock { enabled #true; }; }").unwrap();
config.runtime_config().unwrap();
}
fn parse(source: &str) -> Config {
+10 -1
View File
@@ -1165,7 +1165,16 @@ mod tests {
}
fn test_font() -> FontArc {
FontArc::try_from_slice(include_bytes!("../../assets/fonts/Comfortaa-Regular.otf")).unwrap()
let mut database = fontdb::Database::new();
database.load_system_fonts();
let font = database.faces().find_map(|face| {
database.with_face_data(face.id, |bytes, index| {
FontVec::try_from_vec_and_index(bytes.to_vec(), index)
.ok()
.map(FontArc::new)
})?
});
font.expect("overview rendering tests require a system font")
}
#[test]
+16 -5
View File
@@ -44,13 +44,13 @@ impl ConfigFile {
fs::TryLockError::Error(error) => error.to_string(),
})?;
let source = read(&path)?;
Ok(Self {
let mut file = Self {
path,
source,
source: String::new(),
_lock: lock,
})
};
file.source = read(&file.path)?;
Ok(file)
}
pub fn source(&self) -> &str {
@@ -102,6 +102,14 @@ impl ConfigFile {
}
}
impl Drop for ConfigFile {
fn drop(&mut self) {
// A spawned child can briefly retain the open-file description after our
// descriptor closes. Release the lock before closing the owner's handle.
let _ = self._lock.unlock();
}
}
fn read(path: &Path) -> Result<String, String> {
let file = match File::open(path) {
Ok(file) => file,
@@ -161,6 +169,8 @@ mod tests {
std::os::unix::fs::symlink(&path, &alias).unwrap();
let file = ConfigFile::open(&path).unwrap();
assert!(ConfigFile::open(&alias).is_err());
// A concurrent process spawn can briefly inherit this open-file description.
let inherited = file._lock.try_clone().unwrap();
drop(file);
ConfigFile::open(&alias)
.unwrap()
@@ -168,6 +178,7 @@ mod tests {
.unwrap();
assert!(alias.is_symlink());
assert_eq!(fs::read_to_string(path).unwrap(), "// updated");
drop(inherited);
}
#[test]
+1
View File
@@ -17,6 +17,7 @@
## Develop Ferese
- [Packages and releases](packaging.md): build Fedora and Arch packages and publish a tagged release.
- [Development](development.md): build Ferese, run tests and check portal contracts.
- [Nested resize checks](nested-resize-testing.md).
- [Client soak testing](native-soak-testing.md).
+3
View File
@@ -3,6 +3,9 @@
Ferese runs on Linux as its own Wayland session alongside your existing desktop.
Launch it from a login manager, a command-based greeter or a local TTY.
For Fedora and Arch packages, see [Packages and releases](packaging.md). The
steps below build and install from source.
## Requirements
For a full session, you’ll need a Wayland-capable graphics driver and a local user
+114
View File
@@ -0,0 +1,114 @@
# Packages and releases
Ferese ships as one package. It includes the compositor, shell, Settings, lock
screen, authentication agent, recording helper and desktop portal.
The release workflow builds an x86_64 RPM on Fedora 44 and an x86_64 Arch package.
COPR and AUR publication are separate steps; neither repository is set up by this
workflow.
## Install a release package
Download the package for your distribution from
[GitHub Releases](https://github.com/ferese-wm/ferese/releases), then install it:
```sh
# Fedora: select the ferese RPM, not the debuginfo or source RPM.
sudo dnf install ./ferese-<version>-1.fc44.x86_64.rpm
# Arch Linux
sudo pacman -U ./ferese-<version>-1-x86_64.pkg.tar.zst
```
Select Ferese in your login manager, or run `ferese-session` from a local TTY.
The package does not replace your login manager or modify your personal config.
To start with the supplied config:
```sh
mkdir -p ~/.config/ferese
cp -n /usr/libexec/ferese/config.example.kdl ~/.config/ferese/config.kdl
```
If you installed with `install.sh`, uninstall that copy before installing a
package. Its `/usr/local/bin` launchers take precedence over `/usr/bin`, and its
portal service can override the packaged service. From the original checkout:
```sh
sudo ./scripts/install-session.sh uninstall
```
Keep your personal config. Log out before changing the installed session, then
log back in once the package is installed.
## Build packages locally
With Python 3.11 or newer and the pinned Rust toolchain installed, prepare the
source archive and versioned recipes from a checkout:
```sh
python3 scripts/package.py source
```
This downloads the locked Rust dependencies and includes an offline Cargo cache.
Ferese uses two packages with the same name and version from different sources,
which `cargo vendor` cannot represent. The cache preserves those source identities
and includes the Git submodules needed by libcosmic. It contains no Cargo
credentials or user-level Cargo configuration.
The resulting files are in `target/dist`: a source archive, Fedora spec, Arch
PKGBUILD and source checksum. Package builds run offline. Dependency license
notices and a JSON report of their sources and SPDX licenses ship with the package.
### Fedora
```sh
sudo dnf install rpm-build dnf5-plugins
sudo dnf builddep target/dist/ferese.spec
mkdir -p ~/rpmbuild/SOURCES
cp target/dist/ferese-*.tar.gz ~/rpmbuild/SOURCES/
rpmbuild -ba target/dist/ferese.spec
```
The binary RPMs are in `~/rpmbuild/RPMS`; the source RPM is in `~/rpmbuild/SRPMS`.
Build with `--with resize_metrics` to enable resize-barrier measurements.
The source RPM can also be submitted to COPR once a project is configured.
### Arch Linux
Build as a regular user with the dependencies listed in the generated PKGBUILD:
```sh
cd target/dist
makepkg -s
```
For a development build, copy `packaging/arch/ferese-git/PKGBUILD` into an empty
build directory and run `makepkg -s`. It tracks `main` and conflicts with the
release package. Submit that recipe to AUR separately if you want AUR distribution.
## Publish a release
1. Set `workspace.package.version` in `Cargo.toml` and update `Cargo.lock`.
2. Commit the release changes and push them.
3. Tag that commit with the same version, prefixed by `v`, then push the tag:
```sh
git tag v0.1.0-beta
git push origin v0.1.0-beta
```
A tag/version mismatch fails before packaging. Tags with a prerelease suffix,
such as `v0.2.0-rc.1`, produce a GitHub prerelease.
The workflow builds and tests both packages before publishing. Release assets
include the offline source archive, binary and source RPMs, Arch package,
PKGBUILD, `.SRCINFO`, Fedora spec and SHA-256 checksums. A failed upload leaves a
draft; rerunning the workflow can finish that draft. Published releases are not
overwritten.
Label a release preparation PR `release` to run the package builds before
merging. You can also use **Run workflow** to test a branch without publishing
or creating a tag. Fedora and Arch jobs run the workspace tests under a private
D-Bus session and validate the installed desktop files. They do not test real
DRM hardware; check login, locking, authentication, screen sharing and recording
on each distribution before announcing a release.
+49
View File
@@ -0,0 +1,49 @@
pkgname=ferese
pkgver=@VERSION@
pkgrel=1
pkgdesc='Scrolling Wayland desktop with its shell, Settings and desktop portal'
arch=('x86_64')
url='https://github.com/ferese-wm/ferese'
license=('MIT' 'Apache-2.0' 'BSD-2-Clause' 'BSD-3-Clause' 'BSL-1.0' 'CC0-1.0' 'CDLA-Permissive-2.0' 'ISC' 'MPL-2.0' 'Unicode-3.0' 'Unlicense' 'Zlib' 'OFL-1.1' 'Apache-2.0 WITH LLVM-exception')
depends=('wayland' 'libxkbcommon' 'libinput' 'systemd-libs' 'seatd' 'mesa' 'libdrm'
'fontconfig' 'freetype2' 'expat' 'dbus' 'python' 'bash' 'util-linux' 'systemd' 'pam' 'polkit' 'foot' 'pipewire'
'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk')
makedepends=('cargo' 'cmake' 'clang' 'pkgconf' 'python' 'desktop-file-utils')
checkdepends=('dbus' 'vulkan-swrast' 'xorg-xauth')
optdepends=('brightnessctl: brightness controls' 'slurp: screenshot region selection'
'satty: screenshot editing' 'wl-clipboard: screenshot clipboard'
'gst-plugins-base: recording codecs' 'gst-plugins-good: recording codecs'
'gst-plugin-pipewire: PipeWire recording'
'xorg-xwayland: X11 applications' 'xwayland-satellite: X11 application integration (requires readiness support)')
# GCC LTO objects from the SPA C wrappers cannot be linked by Rust's lld.
options=('!lto')
backup=('etc/pam.d/ferese-lock')
conflicts=('ferese-git')
_source_version=@UPSTREAM_VERSION@
source=("$url/releases/download/v$_source_version/ferese-$_source_version.tar.gz")
sha256sums=('@SHA256@')
build() {
cd "$srcdir/ferese-$_source_version"
export CARGO_HOME="$PWD/cargo-home"
export CARGO_PROFILE_RELEASE_DEBUG=0
cargo build --release --frozen --offline --workspace
}
check() {
cd "$srcdir/ferese-$_source_version"
export CARGO_HOME="$PWD/cargo-home"
export CARGO_PROFILE_RELEASE_DEBUG=0
export TZ=UTC
export XDG_RUNTIME_DIR="$(mktemp -d)"
trap 'rm -r -- "$XDG_RUNTIME_DIR"' EXIT
python -m unittest discover -s scripts/tests -p test_package.py
dbus-run-session -- cargo test --release --frozen --offline --workspace
}
package() {
cd "$srcdir/ferese-$_source_version"
python scripts/package.py stage --destdir "$pkgdir" --pam arch
desktop-file-validate "$pkgdir/usr/share/wayland-sessions/ferese.desktop"
desktop-file-validate "$pkgdir/usr/share/applications/dev.ferese.Settings.desktop"
}
+71
View File
@@ -0,0 +1,71 @@
pkgbase = ferese-git
pkgdesc = Scrolling Wayland desktop with its shell, Settings and desktop portal (Git)
pkgver = 0.1.0beta.r0.g0000000
pkgrel = 1
url = https://github.com/ferese-wm/ferese
arch = x86_64
license = MIT
license = Apache-2.0
license = BSD-2-Clause
license = BSD-3-Clause
license = BSL-1.0
license = CC0-1.0
license = CDLA-Permissive-2.0
license = ISC
license = MPL-2.0
license = Unicode-3.0
license = Unlicense
license = Zlib
license = OFL-1.1
license = Apache-2.0 WITH LLVM-exception
checkdepends = dbus
checkdepends = vulkan-swrast
checkdepends = xorg-xauth
makedepends = cargo
makedepends = cmake
makedepends = clang
makedepends = pkgconf
makedepends = python
makedepends = git
makedepends = desktop-file-utils
depends = wayland
depends = libxkbcommon
depends = libinput
depends = systemd-libs
depends = seatd
depends = mesa
depends = libdrm
depends = fontconfig
depends = freetype2
depends = expat
depends = dbus
depends = python
depends = bash
depends = util-linux
depends = systemd
depends = pam
depends = polkit
depends = foot
depends = pipewire
depends = wireplumber
depends = libpipewire
depends = gstreamer
depends = xdg-desktop-portal
depends = xdg-desktop-portal-gtk
optdepends = brightnessctl: brightness controls
optdepends = slurp: screenshot region selection
optdepends = satty: screenshot editing
optdepends = wl-clipboard: screenshot clipboard
optdepends = gst-plugins-base: recording codecs
optdepends = gst-plugins-good: recording codecs
optdepends = gst-plugin-pipewire: PipeWire recording
optdepends = xorg-xwayland: X11 applications
optdepends = xwayland-satellite: X11 application integration (requires readiness support)
provides = ferese
conflicts = ferese
options = !lto
backup = etc/pam.d/ferese-lock
source = ferese::git+https://github.com/ferese-wm/ferese.git#branch=main
sha256sums = SKIP
pkgname = ferese-git
+63
View File
@@ -0,0 +1,63 @@
pkgname=ferese-git
pkgver=0.1.0beta.r0.g0000000
pkgrel=1
pkgdesc='Scrolling Wayland desktop with its shell, Settings and desktop portal (Git)'
arch=('x86_64')
url='https://github.com/ferese-wm/ferese'
license=('MIT' 'Apache-2.0' 'BSD-2-Clause' 'BSD-3-Clause' 'BSL-1.0' 'CC0-1.0' 'CDLA-Permissive-2.0' 'ISC' 'MPL-2.0' 'Unicode-3.0' 'Unlicense' 'Zlib' 'OFL-1.1' 'Apache-2.0 WITH LLVM-exception')
depends=('wayland' 'libxkbcommon' 'libinput' 'systemd-libs' 'seatd' 'mesa' 'libdrm'
'fontconfig' 'freetype2' 'expat' 'dbus' 'python' 'bash' 'util-linux' 'systemd' 'pam' 'polkit' 'foot' 'pipewire'
'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk')
makedepends=('cargo' 'cmake' 'clang' 'pkgconf' 'python' 'git' 'desktop-file-utils')
checkdepends=('dbus' 'vulkan-swrast' 'xorg-xauth')
optdepends=('brightnessctl: brightness controls' 'slurp: screenshot region selection'
'satty: screenshot editing' 'wl-clipboard: screenshot clipboard'
'gst-plugins-base: recording codecs' 'gst-plugins-good: recording codecs'
'gst-plugin-pipewire: PipeWire recording'
'xorg-xwayland: X11 applications' 'xwayland-satellite: X11 application integration (requires readiness support)')
provides=('ferese')
conflicts=('ferese')
# GCC LTO objects from the SPA C wrappers cannot be linked by Rust's lld.
options=('!lto')
backup=('etc/pam.d/ferese-lock')
source=('ferese::git+https://github.com/ferese-wm/ferese.git#branch=main')
sha256sums=('SKIP')
pkgver() {
cd "$srcdir/ferese"
local version
version=$(python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["workspace"]["package"]["version"])')
printf '%s.r%s.g%s' "${version//-/}" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
prepare() {
cd "$srcdir/ferese"
export CARGO_HOME="$srcdir/cargo-home"
cargo fetch --locked
}
build() {
cd "$srcdir/ferese"
export CARGO_HOME="$srcdir/cargo-home"
export CARGO_PROFILE_RELEASE_DEBUG=0
cargo build --release --frozen --offline --workspace
}
check() {
cd "$srcdir/ferese"
export CARGO_HOME="$srcdir/cargo-home"
export CARGO_PROFILE_RELEASE_DEBUG=0
export TZ=UTC
export XDG_RUNTIME_DIR="$(mktemp -d)"
trap 'rm -r -- "$XDG_RUNTIME_DIR"' EXIT
python -m unittest discover -s scripts/tests -p test_package.py
dbus-run-session -- cargo test --release --frozen --offline --workspace
}
package() {
cd "$srcdir/ferese"
export CARGO_HOME="$srcdir/cargo-home"
python scripts/package.py stage --destdir "$pkgdir" --pam arch
desktop-file-validate "$pkgdir/usr/share/wayland-sessions/ferese.desktop"
desktop-file-validate "$pkgdir/usr/share/applications/dev.ferese.Settings.desktop"
}
+78
View File
@@ -0,0 +1,78 @@
# dwz cannot compress the DWARF 5 sections emitted by Rust. Keep debug files
# without the optional DWARF compression pass.
%global _find_debuginfo_dwz_opts %{nil}
%global upstream_version @UPSTREAM_VERSION@
%bcond resize_metrics 0
Name: ferese
Version: @VERSION@
Release: 1%{?dist}
Summary: Scrolling Wayland desktop
# Includes vendored Rust libraries and theme palettes. Full notices ship
# under /usr/share/licenses/ferese, including per-dependency SPDX metadata.
License: MIT AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND BSL-1.0 AND CC0-1.0 AND CDLA-Permissive-2.0 AND ISC AND MPL-2.0 AND Unicode-3.0 AND Unlicense AND Zlib AND OFL-1.1 AND (Apache-2.0 WITH LLVM-exception)
URL: https://github.com/ferese-wm/ferese
Source0: %{url}/releases/download/v%{upstream_version}/ferese-%{upstream_version}.tar.gz
BuildRequires: cargo >= 1.95
BuildRequires: rust >= 1.95
BuildRequires: gcc-c++ cmake make clang clang-devel python3 pkgconf-pkg-config
BuildRequires: wayland-devel libxkbcommon-devel libinput-devel systemd-devel libseat-devel
BuildRequires: mesa-libgbm-devel mesa-libEGL-devel libdrm-devel fontconfig-devel freetype-devel expat-devel
BuildRequires: mesa-dri-drivers mesa-vulkan-drivers
BuildRequires: pipewire-devel gstreamer1-devel dbus-daemon desktop-file-utils polkit-libs pam
BuildRequires: xorg-x11-xauth
Requires: pam polkit polkit-libs foot pipewire wireplumber python3 bash util-linux dbus-daemon dbus-tools systemd
Requires: xdg-desktop-portal xdg-desktop-portal-gtk
# Loaded at runtime, so ELF dependency detection does not find these libraries.
Requires: libEGL.so.1()(64bit) fontconfig freetype
Recommends: brightnessctl slurp wl-clipboard satty
Recommends: gstreamer1-plugins-base gstreamer1-plugins-good pipewire-gstreamer
%description
Ferese includes a scrolling Wayland compositor, desktop shell, Settings,
lock screen, authentication agent and desktop portal in one package.
%prep
%autosetup -n ferese-%{upstream_version}
%build
export CARGO_HOME="$PWD/cargo-home"
export CARGO_PROFILE_RELEASE_DEBUG=0
%if %{with resize_metrics}
cargo build --release --frozen --offline --workspace --features ferese/resize-metrics
%else
cargo build --release --frozen --offline --workspace
%endif
%install
python3 scripts/package.py stage --destdir "%{buildroot}" --pam fedora
desktop-file-validate "%{buildroot}%{_datadir}/wayland-sessions/ferese.desktop"
desktop-file-validate "%{buildroot}%{_datadir}/applications/dev.ferese.Settings.desktop"
%check
export CARGO_HOME="$PWD/cargo-home"
export CARGO_PROFILE_RELEASE_DEBUG=0
export TZ=UTC
export XDG_RUNTIME_DIR="$(mktemp -d)"
trap 'rm -r -- "$XDG_RUNTIME_DIR"' EXIT
python3 -m unittest discover -s scripts/tests -p test_package.py
dbus-run-session -- cargo test --release --frozen --offline --workspace
%files
%{_bindir}/ferese*
%{_bindir}/xdg-desktop-portal-ferese
%{_libexecdir}/ferese/
%{_datadir}/ferese/
%{_datadir}/wayland-sessions/ferese.desktop
%{_datadir}/applications/dev.ferese.Settings.desktop
%{_datadir}/icons/hicolor/scalable/apps/ferese.svg
%{_datadir}/icons/hicolor/scalable/apps/dev.ferese.Settings.svg
%{_datadir}/xdg-desktop-portal/portals/ferese.portal
%{_datadir}/xdg-desktop-portal/ferese-portals.conf
%{_datadir}/dbus-1/services/org.freedesktop.impl.portal.desktop.ferese.service
/usr/lib/systemd/user/ferese-session.target
/usr/lib/systemd/user/xdg-desktop-portal-ferese.service
%config(noreplace) %{_sysconfdir}/pam.d/ferese-lock
%license %{_datadir}/licenses/ferese/
%doc %{_datadir}/doc/ferese/
+4
View File
@@ -0,0 +1,4 @@
#%PAM-1.0
# Authenticate the session owner without opening another PAM session.
auth include system-auth
account include system-auth
+1
View File
@@ -16,6 +16,7 @@ PAGES = [
('screenshots', 'Screenshots', ROOT / 'docs/screenshots.md'),
('why-ferese', 'Why Ferese', ROOT / 'docs/why-ferese.md'),
('installation', 'Installation', ROOT / 'docs/installation.md'),
('packaging', 'Packages and releases', ROOT / 'docs/packaging.md'),
('configuration', 'Configuration', ROOT / 'docs/configuration.md'),
('animation-model', 'Animation model', ROOT / 'docs/animation-model.md'),
('shortcuts', 'Shortcuts and gestures', ROOT / 'docs/shortcuts.md'),
+2
View File
@@ -147,6 +147,8 @@ def validate_bundle(bundle, *, installed=False):
# verifiable for upgrades and rollback without adding it to new bundles.
if allowed is None and installed and re.fullmatch(r'wallpapers/[A-Za-z0-9._-]+\.jpe?g', target):
allowed = {'target': target, 'mode': '0644'}
if allowed is None and installed and target in {'licenses/Cantarell-LICENSE.txt', 'licenses/Comfortaa-LICENSE.txt'}:
allowed = {'target': target, 'mode': '0644'}
if allowed is None and target == 'wallpapers/ferese.svg':
allowed = {'target': target, 'mode': '0644'}
if allowed is None and re.fullmatch(r'licenses/theme-[A-Za-z0-9._-]+\.txt', target):
-5
View File
@@ -85,11 +85,6 @@
"target": "config.example.kdl",
"mode": "0644"
},
{
"source": "assets/fonts/Comfortaa-LICENSE.txt",
"target": "licenses/Comfortaa-LICENSE.txt",
"mode": "0644"
},
{
"source": "assets/themes/licenses/*.txt",
"target": "licenses/theme-{name}",
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env python3
"""Prepare offline release sources and stage the complete distro package."""
import argparse
import gzip
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import tarfile
import tempfile
import tomllib
ROOT = Path(__file__).resolve().parents[1]
def run(*args, cwd=ROOT, env=None):
return subprocess.check_output(args, cwd=cwd, env=env, text=True)
def version():
return tomllib.loads((ROOT / 'Cargo.toml').read_text())['workspace']['package']['version']
def check_tag(tag, release_version):
if not re.fullmatch(r'\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?', release_version):
raise ValueError(f'Unsupported release version: {release_version}')
if tag and tag != f'v{release_version}':
raise ValueError(f'Tag {tag} does not match Cargo version {release_version}')
def licenses(destination, metadata):
destination.mkdir(parents=True, exist_ok=True)
report = []
for package in sorted(metadata['packages'], key=lambda p: (p['name'], p['version'])):
if package['source'] is None:
continue
directory = Path(package['manifest_path']).parent
source_hash = hashlib.sha256(package['source'].encode()).hexdigest()[:8]
name = f"{package['name']}-{package['version']}-{source_hash}"
files = set()
search = [directory]
if package['source'].startswith('git+'):
for ancestor in directory.parents:
search.append(ancestor)
if (ancestor / '.git').exists():
break
for parent in search:
for pattern in ('LICENSE*', 'LICENCE*', 'COPYING*', 'NOTICE*', 'UNLICENSE*'):
files.update(parent.glob(pattern))
if package.get('license_file'):
files.add(directory / package['license_file'])
for path in sorted(files):
if path.is_file():
repeated = sum(other.name == path.name for other in files) > 1
filename = f'{path.parent.name}-{path.name}' if repeated else path.name
target = destination / name / filename
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(path, target)
report.append({'name': package['name'], 'version': package['version'],
'source': package['source'], 'license': package['license'],
'notice_directory': name})
(destination / 'dependencies.json').write_text(json.dumps(report, indent=2) + '\n')
def stage(destdir, binary_dir, pam):
destdir = Path(destdir).absolute()
if destdir == Path('/') or destdir.is_symlink():
raise ValueError('DESTDIR must be a private package staging directory')
if destdir.exists() and any(destdir.iterdir()):
raise ValueError('DESTDIR must be empty')
inventory = json.loads((ROOT / 'scripts/installer/inventory.json').read_text())
planned = []
links = []
for entry in inventory['files']:
if entry['target'].startswith('pam/'):
continue
if entry['source'].startswith('target/release/'):
sources = [Path(binary_dir).absolute() / Path(entry['source']).name]
else:
sources = sorted(ROOT.glob(entry['source']))
if not sources:
raise ValueError(f"Missing package asset: {entry['source']}")
for source in sources:
target = entry['target'].replace('{name}', source.name)
if 'destination' in entry:
target = entry['destination'].replace('/usr/local/', '/usr/').lstrip('/')
elif target.startswith(('wallpapers/', 'licenses/')):
prefix = 'usr/share/ferese' if target.startswith('wallpapers/') else 'usr/share'
if target.startswith('licenses/'):
target = 'licenses/ferese/' + target.removeprefix('licenses/')
target = f'{prefix}/{target}'
else:
target = f'usr/libexec/ferese/{target}'
planned.append((source, target, int(entry['mode'], 8)))
if entry.get('link'):
links.append((f'usr/bin/{source.name}', f'/usr/libexec/ferese/{source.name}'))
planned.extend([(ROOT / f'packaging/pam.d/ferese-lock.{pam}', 'etc/pam.d/ferese-lock', 0o644),
(ROOT / 'LICENSE', 'usr/share/licenses/ferese/LICENSE', 0o644),
(ROOT / 'README.md', 'usr/share/doc/ferese/README.md', 0o644)])
# Validate the complete inventory before writing any part of the package.
for source, _, _ in planned:
if not source.is_file():
raise ValueError(f'Missing package file: {source}')
for source, target, mode in planned:
destination = destdir / target
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, destination)
destination.chmod(mode)
if source.suffix in ('.desktop', '.service', '.target', '.portal', '.conf'):
text = destination.read_text().replace('/usr/local/lib/ferese/current/', '/usr/libexec/ferese/')
destination.write_text(text.replace('/usr/local/', '/usr/'))
bundled = ROOT / 'bundled-licenses'
if bundled.exists():
shutil.copytree(bundled, destdir / 'usr/share/licenses/ferese/dependencies', dirs_exist_ok=True)
else:
metadata = json.loads(run('cargo', 'metadata', '--locked', '--offline', '--format-version', '1'))
licenses(destdir / 'usr/share/licenses/ferese/dependencies', metadata)
for path in (destdir / 'usr/share/licenses/ferese').rglob('*'):
if path.is_file():
path.chmod(0o644)
for path in destdir.rglob('*'):
if path.is_dir() and not path.is_symlink():
path.chmod(0o755)
links.append(('usr/libexec/ferese/wallpapers', '/usr/share/ferese/wallpapers'))
for target, source in links:
destination = destdir / target
destination.parent.mkdir(parents=True, exist_ok=True)
destination.symlink_to(source)
def source(output, tag, offline):
release_version = version()
check_tag(tag, release_version)
output = Path(output).absolute()
output.mkdir(parents=True, exist_ok=True)
epoch = int(os.environ.get('SOURCE_DATE_EPOCH', run('git', 'show', '-s', '--format=%ct', 'HEAD').strip()))
with tempfile.TemporaryDirectory(prefix='ferese-source-') as temporary:
tree = Path(temporary) / f'ferese-{release_version}'
tree.mkdir()
# Include new packaging files in local dry runs; release checkouts are clean.
files = run('git', 'ls-files', '-z', '--cached', '--others', '--exclude-standard').split('\0')
for filename in sorted(set(files) - {''}):
path = ROOT / filename
if not path.is_file():
continue
destination = tree / filename
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, destination)
fetch = ['cargo', 'fetch', '--locked']
if offline:
fetch.append('--offline')
run(*fetch, cwd=tree)
metadata = json.loads(run('cargo', 'metadata', '--locked', '--offline', '--format-version', '1', cwd=tree))
licenses(tree / 'bundled-licenses', metadata)
# cargo vendor cannot represent dpi 0.1.2 from both crates.io and Git.
# Preserve source identities with an isolated, credential-free Cargo cache.
cache = tree / 'cargo-home'
for package in metadata['packages']:
package_source = package['source']
if package_source is None:
continue
manifest = Path(package['manifest_path'])
if package_source.startswith('registry+'):
registry = manifest.parent.parent.name
registry_home = manifest.parent.parent.parent.parent
name = package['name']
crate = f"{name}-{package['version']}.crate"
for relative in (f'cache/{registry}/{crate}', f'index/{registry}/config.json'):
original = registry_home / relative
destination = cache / 'registry' / relative
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(original, destination)
if len(name) == 1:
index_path = f'1/{name}'
elif len(name) == 2:
index_path = f'2/{name}'
elif len(name) == 3:
index_path = f'3/{name[0]}/{name}'
else:
index_path = f'{name[:2]}/{name[2:4]}/{name}'
relative = f'index/{registry}/.cache/{index_path}'
destination = cache / 'registry' / relative
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(registry_home / relative, destination)
elif package_source.startswith('git+'):
checkout = next(parent for parent in manifest.parents if parent.parent.name == 'checkouts')
original = checkout.parent.parent / 'db' / checkout.name
destination = cache / 'git/db' / checkout.name
if not destination.exists():
shutil.copytree(original, destination)
# Cargo cannot fetch Git submodules in frozen mode. Carry the
# completed checkout, including .cargo-ok and submodule objects.
revision = next(parent for parent in manifest.parents if parent.parent == checkout)
if (revision / '.gitmodules').exists():
ready = cache / 'git/checkouts' / checkout.name / revision.name
if not ready.exists():
shutil.copytree(revision, ready)
else:
raise ValueError(f'Unsupported dependency source: {package_source}')
environment = dict(os.environ, CARGO_HOME=str(cache))
run('cargo', 'metadata', '--frozen', '--offline', '--format-version', '1', cwd=tree, env=environment)
# Cargo recreates these from the checked source archives and Git objects.
for relative in ('registry/src',):
shutil.rmtree(cache / relative, ignore_errors=True)
archive = output / f'ferese-{release_version}.tar.gz'
with archive.open('wb') as raw, gzip.GzipFile(filename='', fileobj=raw, mode='wb', mtime=epoch) as zipped:
with tarfile.open(fileobj=zipped, mode='w') as tar:
for path in [tree, *sorted(tree.rglob('*'))]:
info = tar.gettarinfo(str(path), arcname=str(path.relative_to(tree.parent)))
info.uid = info.gid = 0
info.uname = info.gname = ''
info.mtime = epoch
# The invoking user's umask must not change release permissions.
info.mode = 0o755 if info.isdir() or info.mode & 0o111 else 0o644
if info.isfile():
with path.open('rb') as contents:
tar.addfile(info, contents)
else:
tar.addfile(info)
digest = hashlib.sha256(archive.read_bytes()).hexdigest()
recipe = (ROOT / 'packaging/arch/PKGBUILD.in').read_text()
recipe = recipe.replace('@VERSION@', release_version.replace('-', ''))
recipe = recipe.replace('@UPSTREAM_VERSION@', release_version).replace('@SHA256@', digest)
(output / 'PKGBUILD').write_text(recipe)
spec = (ROOT / 'packaging/fedora/ferese.spec.in').read_text()
(output / 'ferese.spec').write_text(spec.replace('@VERSION@', release_version.replace('-', '~'))
.replace('@UPSTREAM_VERSION@', release_version))
(output / 'SHA256SUMS').write_text(f'{digest} {archive.name}\n')
print(archive)
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest='command', required=True)
stage_parser = commands.add_parser('stage')
stage_parser.add_argument('--destdir', required=True)
stage_parser.add_argument('--binary-dir', default=str(ROOT / 'target/release'))
stage_parser.add_argument('--pam', choices=['fedora', 'arch'], required=True)
source_parser = commands.add_parser('source')
source_parser.add_argument('--output', default='target/dist')
source_parser.add_argument('--tag')
source_parser.add_argument('--offline', action='store_true')
args = parser.parse_args()
if args.command == 'stage':
stage(args.destdir, args.binary_dir, args.pam)
else:
source(args.output, args.tag, args.offline)
if __name__ == '__main__':
main()
+112
View File
@@ -0,0 +1,112 @@
"""Package layout and release-input regressions; no root or network needed."""
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
SPEC = importlib.util.spec_from_file_location('package', Path(__file__).resolve().parents[1] / 'package.py')
PACKAGE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(PACKAGE)
class PackageTests(unittest.TestCase):
def test_release_tag_must_match_workspace_version(self):
PACKAGE.check_tag('v0.1.0', '0.1.0')
PACKAGE.check_tag('v0.2.0-rc.1', '0.2.0-rc.1')
with self.assertRaises(ValueError):
PACKAGE.check_tag('v0.2.0', '0.1.0')
def test_refuses_live_root(self):
with self.assertRaises(ValueError):
PACKAGE.stage('/', '/missing', 'fedora')
def test_refuses_existing_content_and_symlink_destinations(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
destination = root / 'dest'
destination.mkdir()
(destination / 'keep').write_text('existing')
link = root / 'link'
link.symlink_to(destination)
for path in (destination, link):
with self.assertRaises(ValueError):
PACKAGE.stage(path, root / 'missing', 'fedora')
self.assertEqual((destination / 'keep').read_text(), 'existing')
def test_missing_binary_fails_before_staging(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
with self.assertRaises(ValueError):
PACKAGE.stage(root / 'dest', root / 'missing', 'fedora')
self.assertFalse((root / 'dest').exists())
def test_complete_package_layout_and_both_pam_policies(self):
inventory = json.loads((PACKAGE.ROOT / 'scripts/installer/inventory.json').read_text())
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
binaries = root / 'bin'
binaries.mkdir()
for entry in inventory['files']:
if entry['source'].startswith('target/release/'):
(binaries / entry['target']).write_bytes(b'fixture')
for pam in ('fedora', 'arch'):
with self.subTest(pam=pam), patch.object(PACKAGE, 'licenses'), patch.object(PACKAGE, 'run', return_value='{}'):
dest = root / pam
PACKAGE.stage(dest, binaries, pam)
for entry in inventory['files']:
if entry.get('link'):
name = Path(entry['source']).name
link = dest / 'usr/bin' / name
self.assertEqual(link.readlink(), Path('/usr/libexec/ferese') / name)
self.assertTrue((dest / str(link.readlink()).lstrip('/')).is_file())
for file in dest.rglob('*'):
if file.is_file() and not file.is_symlink() and file.suffix in ('.desktop', '.service'):
self.assertNotIn('/usr/local/', file.read_text())
self.assertFalse((dest / 'usr/local').exists())
self.assertIn('system-auth', (dest / 'etc/pam.d/ferese-lock').read_text())
wallpaper = dest / 'usr/libexec/ferese/wallpapers'
self.assertEqual(wallpaper.readlink(), Path('/usr/share/ferese/wallpapers'))
self.assertTrue((dest / 'usr/share/ferese/wallpapers/ferese.png').is_file())
self.assertEqual((dest / 'usr/libexec/ferese/ferese-session').stat().st_mode & 0o777, 0o755)
self.assertEqual((dest / 'usr/libexec/ferese').stat().st_mode & 0o777, 0o755)
def test_same_version_from_different_sources_keeps_both_licenses(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
packages = []
for source in ('registry+first', 'registry+second'):
folder = root / source.removeprefix('registry+')
folder.mkdir()
(folder / 'LICENSE').write_text(source)
packages.append({'name': 'dpi', 'version': '0.1.2', 'source': source,
'manifest_path': str(folder / 'Cargo.toml'),
'license': 'MIT', 'license_file': None})
PACKAGE.licenses(root / 'notices', {'packages': packages})
texts = {path.read_text() for path in (root / 'notices').glob('dpi-*/LICENSE')}
self.assertEqual(texts, {'registry+first', 'registry+second'})
def test_dependency_license_report_contains_source_and_license(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / 'Cargo.toml').write_text('')
(root / 'LICENSE-MIT').write_text('test license')
metadata = {'packages': [{'name': 'fixture', 'version': '1.0.0',
'source': 'registry+test', 'manifest_path': str(root / 'Cargo.toml'),
'license': 'MIT', 'license_file': None}]}
PACKAGE.licenses(root / 'notices', metadata)
self.assertEqual(next((root / 'notices').glob('fixture-1.0.0-*/LICENSE-MIT')).read_text(), 'test license')
report = json.loads((root / 'notices/dependencies.json').read_text())
self.assertEqual(report[0]['license'], 'MIT')
self.assertEqual(report[0]['source'], 'registry+test')
if __name__ == '__main__':
unittest.main()
+10 -6
View File
@@ -317,14 +317,18 @@ fn appearance_scroll_preserves_progress_and_settles_visibility() {
#[test]
fn gpu_panel_text_survives_preview_clipping_and_scrolling() {
// Native startup preloads text fonts; headless renderers skip that step.
// Supply a bundled proportional font even on minimal CI images.
let matched = std::process::Command::new("fc-match")
.args(["--format=%{family}\n%{file}", "sans-serif"])
.output()
.expect("rendering tests require fontconfig");
assert!(matched.status.success());
let matched = String::from_utf8(matched.stdout).unwrap();
let (family, path) = matched.split_once('\n').expect("rendering tests require a system font");
let font = ferese_theme::font(Some(family.split(',').next().unwrap()));
cosmic::iced::advanced::graphics::text::font_system()
.write()
.unwrap()
.load_font(std::borrow::Cow::Borrowed(include_bytes!(
"../../../assets/fonts/Comfortaa-Regular.otf"
)));
.load_font(std::borrow::Cow::Owned(std::fs::read(path).unwrap()));
#[derive(Default)]
struct Labels {
@@ -416,7 +420,7 @@ fn gpu_panel_text_survives_preview_clipping_and_scrolling() {
),
)
.0;
app.font = Font::with_name("Comfortaa");
app.font = font;
app.panel_page = if selection == "panels" {
panel_controls::PanelPage::Panels
} else {
+3 -3
View File
@@ -622,10 +622,10 @@ mod tests {
use super::*;
#[test]
fn removed_desktop_widgets_are_rejected_at_startup_and_reload() {
fn removed_desktop_widgets_allow_startup_and_reload() {
let source = "desktop-widgets { clock { enabled #true; }; }";
assert!(parse_source(source).is_err());
assert!(parse_test_source(source).is_err());
parse_source(source).unwrap();
parse_test_source(source).unwrap();
}
fn parse_test_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {