mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-30 06:57:49 -04:00
feat(packaging): dual-series migration publishing & dms migrate dux
- Further banner notices will show once per/shell session in notifications, dms doctor and software updates - Doc updates to follow Port 2.0
This commit is contained in:
1 parent
d367e7e71b
commit
095d1c6fd5
43 files changed
+1879
-421
No files matched your search
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Download / publish an XBPS repository prefix on Cloudflare R2.
|
||||
# Shared by dux-shell and dux-repo (same prefix); keep both copies identical.
|
||||
# Shared by dux-shell and dux-repo, which both write dms/current; keep both copies identical.
|
||||
# ETag guard: publish aborts if another writer replaced x86_64-repodata mid-run.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "usage: $0 download|publish" >&2
|
||||
echo "usage: $0 download|is-indexed|index|publish" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
@@ -78,6 +77,60 @@ download() {
|
||||
cp -a "$REPOSITORY_DIR/current/." "$REPOSITORY_DIR/previous-current/"
|
||||
}
|
||||
|
||||
# Staged pkgvers count as indexed too.
|
||||
indexed_pkgvers() {
|
||||
local repodata="$REPOSITORY_DIR/current/x86_64-repodata" member
|
||||
[[ -s "$repodata" ]] || return 0
|
||||
for member in index.plist stage.plist; do
|
||||
tar -xOf "$repodata" "$member" 2>/dev/null | python3 -c '
|
||||
import plistlib, sys
|
||||
data = sys.stdin.buffer.read()
|
||||
for pkg in (plistlib.loads(data).values() if data.strip() else ()):
|
||||
print(pkg["pkgver"])
|
||||
' || true
|
||||
done
|
||||
}
|
||||
|
||||
# Also catches packages uploaded by a run that died before its repodata PUT.
|
||||
is_indexed() {
|
||||
local known file
|
||||
shopt -s nullglob
|
||||
known="$(indexed_pkgvers)"
|
||||
for file in "$REPOSITORY_DIR"/current/*.xbps; do
|
||||
if ! grep -qxF "$(xbps-uhelper binpkgver "$(basename "$file")")" <<<"$known"; then
|
||||
echo "$(basename "$file") is not in ${R2_PREFIX}/current/x86_64-repodata"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
index() {
|
||||
require_env XBPS_PRIVATE_KEY
|
||||
local dir="$REPOSITORY_DIR/current" sig
|
||||
|
||||
shopt -s nullglob
|
||||
local pkgs=("$dir"/*.xbps)
|
||||
if (( ${#pkgs[@]} == 0 )); then
|
||||
echo "error: no .xbps files to index in $dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f "$dir"/*.sig
|
||||
xbps-rindex -a "${pkgs[@]}"
|
||||
xbps-rindex --remove-obsoletes "$dir"
|
||||
|
||||
# Unchanged packages keep their .sig2 byte-for-byte; drop those of removed packages.
|
||||
for sig in "$dir"/*.sig2; do
|
||||
[[ -f "${sig%.sig2}" ]] || rm -f "$sig"
|
||||
done
|
||||
|
||||
XBPS_KEY_FILE="$(mktemp)"
|
||||
trap 'rm -f "$XBPS_KEY_FILE"' EXIT
|
||||
printf '%s\n' "$XBPS_PRIVATE_KEY" > "$XBPS_KEY_FILE"
|
||||
xbps-rindex --sign --signedby "AvengeMedia <[email protected]>" --privkey "$XBPS_KEY_FILE" "$dir"
|
||||
xbps-rindex --sign-pkg --privkey "$XBPS_KEY_FILE" "$dir"/*.xbps
|
||||
}
|
||||
|
||||
build_manifest() {
|
||||
local packages='[]' previous_sources='{}'
|
||||
local file filename pkgver name version revision sha size
|
||||
@@ -226,6 +279,8 @@ publish() {
|
||||
|
||||
case "${1:-}" in
|
||||
download) download ;;
|
||||
is-indexed) is_indexed ;;
|
||||
index) index ;;
|
||||
publish) publish ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
@@ -3,6 +3,7 @@ name: Prune Void R2 archive
|
||||
# Keeps the danklinux-void bucket inside the Cloudflare R2 free tier by
|
||||
# deleting the oldest archive/<repo>/<timestamp>/ snapshots written by
|
||||
# publish-void-r2.sh. Live repositories (<repo>/current/) are never touched.
|
||||
# <repo> comes from the listing, so new and legacy prefixes need no list.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
|
||||
+77
-121
@@ -18,185 +18,141 @@ on:
|
||||
description: 'Release number (e.g., 1, 2, 3 for hotfixes)'
|
||||
required: false
|
||||
default: '1'
|
||||
owners:
|
||||
description: 'Copr owners to publish to'
|
||||
required: false
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- duxlabs
|
||||
- avengemedia
|
||||
|
||||
jobs:
|
||||
determine-packages:
|
||||
plan:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
packages: ${{ steps.set-packages.outputs.packages }}
|
||||
matrix: ${{ steps.plan.outputs.matrix }}
|
||||
version: ${{ steps.plan.outputs.version }}
|
||||
release: ${{ steps.plan.outputs.release }}
|
||||
steps:
|
||||
- name: Set package list
|
||||
id: set-packages
|
||||
- name: Plan owners and version
|
||||
id: plan
|
||||
env:
|
||||
OWNERS: ${{ inputs.owners || 'all' }}
|
||||
PACKAGE: ${{ inputs.package || 'dms' }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
RELEASE: ${{ inputs.release || '1' }}
|
||||
run: |
|
||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||
MATRIX=$(jq -cn --arg owners "$OWNERS" --arg pkg "$PACKAGE" '
|
||||
[{owner: "duxlabs", suffix: ""}, {owner: "avengemedia", suffix: "_LEGACY"}]
|
||||
| map(select($owners == "all" or .owner == $owners) | .package = $pkg)
|
||||
| {include: .}')
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(curl -sL "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.tag_name' | sed 's/^v//')
|
||||
fi
|
||||
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
|
||||
echo "::error::Could not determine the latest release of ${{ github.repository }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "release=$RELEASE" >> "$GITHUB_OUTPUT"
|
||||
echo "Building $PACKAGE $VERSION-$RELEASE for: $(jq -r '[.include[].owner] | join(", ")' <<< "$MATRIX")"
|
||||
|
||||
build-and-upload:
|
||||
needs: determine-packages
|
||||
name: ${{ matrix.package }} to ${{ matrix.owner }}
|
||||
needs: plan
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.plan.outputs.matrix) }}
|
||||
env:
|
||||
VERSION: ${{ needs.plan.outputs.version }}
|
||||
RELEASE: ${{ needs.plan.outputs.release }}
|
||||
PACKAGE: ${{ matrix.package }}
|
||||
COPR_PROJECT: ${{ matrix.owner }}/dms
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
# Get version from manual input or latest release
|
||||
if [ -n "${{ github.event.inputs.version }}" ]; then
|
||||
VERSION="${{ github.event.inputs.version }}"
|
||||
echo "Using manual version: $VERSION"
|
||||
else
|
||||
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
echo "Using latest release version: $VERSION"
|
||||
fi
|
||||
|
||||
RELEASE="${{ github.event.inputs.release }}"
|
||||
if [ -z "$RELEASE" ]; then
|
||||
RELEASE="1"
|
||||
fi
|
||||
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "✅ Building ${{ matrix.package }} version: $VERSION-$RELEASE"
|
||||
|
||||
- name: Setup build environment
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm wget curl jq gzip
|
||||
sudo apt-get install -y rpm wget curl jq gzip python3-pip
|
||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
echo "✅ RPM build environment ready"
|
||||
|
||||
- name: Download release assets
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
cd ~/rpmbuild/SOURCES
|
||||
|
||||
echo "📦 Downloading DMS QML source for v${VERSION}..."
|
||||
|
||||
# Download DMS QML source
|
||||
wget "https://github.com/duxlabs-dev/dux-shell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||
wget "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||
echo "::error::Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "✅ Source downloaded"
|
||||
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
|
||||
ls -lh
|
||||
|
||||
- name: Generate stable spec file
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
RELEASE="${{ steps.version.outputs.release }}"
|
||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
# Copy spec file from repository
|
||||
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
# Replace placeholders with actual values
|
||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
|
||||
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
|
||||
echo ""
|
||||
echo "=== Spec file preview ==="
|
||||
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
SPEC=~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
cp "distro/fedora/${PACKAGE}.spec" "$SPEC"
|
||||
sed -i -e "s/VERSION_PLACEHOLDER/${VERSION}/g" \
|
||||
-e "s/RELEASE_PLACEHOLDER/${RELEASE}/g" \
|
||||
-e "s/CHANGELOG_DATE_PLACEHOLDER/$(date '+%a %b %d %Y')/g" "$SPEC"
|
||||
head -40 "$SPEC"
|
||||
|
||||
- name: Build SRPM
|
||||
id: build
|
||||
run: |
|
||||
cd ~/rpmbuild/SPECS
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
echo "🔨 Building SRPM for ${PACKAGE}..."
|
||||
rpmbuild -bs ${PACKAGE}.spec
|
||||
|
||||
rpmbuild -bs ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.src.rpm | tail -n 1)
|
||||
SRPM_NAME=$(basename "$SRPM")
|
||||
|
||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "✅ SRPM built: $SRPM_NAME"
|
||||
echo ""
|
||||
echo "=== SRPM Info ==="
|
||||
echo "srpm_path=$SRPM" >> "$GITHUB_OUTPUT"
|
||||
echo "srpm_name=$(basename "$SRPM")" >> "$GITHUB_OUTPUT"
|
||||
rpm -qpi "$SRPM"
|
||||
|
||||
# Per-owner name: two legs uploading the same artifact name get a 409.
|
||||
- name: Upload SRPM artifact
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||
name: ${{ matrix.package }}-stable-srpm-${{ needs.plan.outputs.version }}-${{ matrix.owner }}
|
||||
path: ${{ steps.build.outputs.srpm_path }}
|
||||
retention-days: 90
|
||||
|
||||
- name: Install Copr CLI
|
||||
env:
|
||||
COPR_LOGIN: ${{ secrets[format('COPR_LOGIN{0}', matrix.suffix)] }}
|
||||
COPR_TOKEN: ${{ secrets[format('COPR_TOKEN{0}', matrix.suffix)] }}
|
||||
run: |
|
||||
sudo apt-get install -y python3-pip
|
||||
: "${COPR_LOGIN:?COPR_LOGIN${{ matrix.suffix }} is not set}" "${COPR_TOKEN:?COPR_TOKEN${{ matrix.suffix }} is not set}"
|
||||
pip3 install copr-cli
|
||||
|
||||
mkdir -p ~/.config
|
||||
cat > ~/.config/copr << EOF
|
||||
[copr-cli]
|
||||
login = ${{ secrets.COPR_LOGIN }}
|
||||
username = avengemedia
|
||||
token = ${{ secrets.COPR_TOKEN }}
|
||||
copr_url = https://copr.fedorainfracloud.org
|
||||
EOF
|
||||
chmod 600 ~/.config/copr
|
||||
|
||||
echo "✅ Copr CLI configured"
|
||||
|
||||
- name: Determine Copr project
|
||||
id: copr_project
|
||||
run: |
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
if [ "$PACKAGE" = "dms" ]; then
|
||||
COPR_PROJECT="avengemedia/dms"
|
||||
else
|
||||
echo "❌ Unknown package: $PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
|
||||
echo "✅ Copr project: $COPR_PROJECT"
|
||||
umask 077
|
||||
printf '[copr-cli]\nlogin = %s\nusername = %s\ntoken = %s\ncopr_url = https://copr.fedorainfracloud.org\n' \
|
||||
"$COPR_LOGIN" "${{ matrix.owner }}" "$COPR_TOKEN" > ~/.config/copr
|
||||
|
||||
- name: Upload to Copr
|
||||
run: |
|
||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
|
||||
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
|
||||
echo " SRPM: $(basename $SRPM)"
|
||||
echo " Version: $VERSION"
|
||||
|
||||
echo "Uploading $(basename "$SRPM") to ${COPR_PROJECT}"
|
||||
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$SRPM" --nowait 2>&1)
|
||||
echo "$BUILD_OUTPUT"
|
||||
|
||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||
|
||||
if [ "$BUILD_ID" != "unknown" ]; then
|
||||
echo "✅ Build submitted successfully!"
|
||||
echo "🔗 https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/build/$BUILD_ID/"
|
||||
echo "https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/build/$BUILD_ID/"
|
||||
else
|
||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||
echo "::warning::Could not extract build ID, but upload may have succeeded"
|
||||
fi
|
||||
|
||||
- name: Build summary
|
||||
if: always()
|
||||
run: |
|
||||
PACKAGE="${{ matrix.package }}"
|
||||
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Package:** ${PACKAGE}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||
{
|
||||
echo "### ${PACKAGE} stable to ${{ matrix.owner }}"
|
||||
echo ""
|
||||
echo "- **Version:** ${VERSION}-${RELEASE}"
|
||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}"
|
||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -16,6 +16,15 @@ on:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||
required: false
|
||||
default: ""
|
||||
owners:
|
||||
description: "OBS owners to publish to"
|
||||
required: false
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- duxlabs
|
||||
- avengemedia
|
||||
schedule:
|
||||
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||
|
||||
@@ -27,7 +36,7 @@ jobs:
|
||||
|
||||
outputs:
|
||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||
packages: ${{ steps.check.outputs.packages }}
|
||||
matrix: ${{ steps.check.outputs.matrix }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
|
||||
steps:
|
||||
@@ -40,13 +49,13 @@ jobs:
|
||||
- name: Check for updates
|
||||
id: check
|
||||
env:
|
||||
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
|
||||
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
|
||||
# duxlabs is dispatch-only until its OBS account exists; drop this gate then
|
||||
OWNERS: ${{ github.event_name == 'workflow_dispatch' && inputs.owners || 'avengemedia' }}
|
||||
run: |
|
||||
# Helper function to check dms-git commit
|
||||
# The public source API needs no credentials, so one job checks every owner
|
||||
check_dms_git() {
|
||||
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
|
||||
local OBS_SPEC=$(curl -s "https://api.opensuse.org/public/source/$1:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
|
||||
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||
|
||||
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||
@@ -58,14 +67,13 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check dms stable tag
|
||||
# Sets LATEST_TAG variable in parent scope if update needed
|
||||
check_dms_stable() {
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/duxlabs-dev/dux-shell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||
LATEST_TAG=$(curl -sL https://api.github.com/repos/duxlabs-dev/dux-shell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
local OBS_SPEC=$(curl -s "https://api.opensuse.org/public/source/$1:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||
|
||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
|
||||
if [[ -n "$LATEST_TAG" && "${LATEST_TAG#v}" == "$OBS_VERSION" ]]; then
|
||||
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
|
||||
return 1 # No update needed
|
||||
else
|
||||
@@ -74,94 +82,85 @@ jobs:
|
||||
fi
|
||||
}
|
||||
|
||||
# Main logic
|
||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
VERSION=""
|
||||
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||
# Run from tag with no package specified - update stable package
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by tag: $VERSION (always update)"
|
||||
# Returns via OWNER_PACKAGES, and VERSION when a stable tag is due
|
||||
plan_owner() {
|
||||
OWNER_PACKAGES=""
|
||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "$PKG" ]]; then
|
||||
OWNER_PACKAGES="dms"
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "Triggered by tag: $VERSION (always update)"
|
||||
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - check dms-git only
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
if check_dms_git; then
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
if check_dms_git "$1"; then
|
||||
OWNER_PACKAGES="dms-git"
|
||||
fi
|
||||
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow trigger
|
||||
PKG="${{ github.event.inputs.package }}"
|
||||
elif [[ -n "$PKG" ]]; then
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
OWNER_PACKAGES="$PKG"
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
if [[ -n "$REBUILD" ]]; then
|
||||
# Rebuild requested - always proceed
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check the stable package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
elif [[ "$PKG" == "all" || "$PKG" == "dms" ]]; then
|
||||
# "all" only checks the stable package
|
||||
if check_dms_stable "$1"; then
|
||||
OWNER_PACKAGES="dms"
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
VERSION="$LATEST_TAG"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms" ]]; then
|
||||
if check_dms_stable; then
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
if check_dms_git "$1"; then
|
||||
OWNER_PACKAGES="$PKG"
|
||||
fi
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
else
|
||||
OWNER_PACKAGES="$PKG"
|
||||
echo "Manual trigger: $PKG"
|
||||
fi
|
||||
else
|
||||
# Unknown package - proceed anyway
|
||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: $PKG"
|
||||
OWNER_PACKAGES="all"
|
||||
fi
|
||||
}
|
||||
|
||||
# Planned per owner: the new projects may lag the legacy ones
|
||||
INCLUDE='[]'
|
||||
for T in duxlabs,,home:duxlabs avengemedia,_LEGACY,home:AvengeMedia; do
|
||||
IFS=, read -r OWNER SUFFIX PROJECT <<< "$T"
|
||||
[[ "$OWNERS" == all || "$OWNERS" == "$OWNER" ]] || continue
|
||||
echo "==> $OWNER ($PROJECT)"
|
||||
plan_owner "$PROJECT"
|
||||
if [[ -z "$OWNER_PACKAGES" ]]; then
|
||||
echo "✓ $OWNER: all packages up to date"
|
||||
continue
|
||||
fi
|
||||
echo "✓ $OWNER: packages to update: $OWNER_PACKAGES"
|
||||
INCLUDE=$(jq -c --arg o "$OWNER" --arg s "$SUFFIX" --arg p "$PROJECT" --arg k "$OWNER_PACKAGES" \
|
||||
'. + [{owner: $o, suffix: $s, project: $p, packages: $k}]' <<< "$INCLUDE")
|
||||
done
|
||||
|
||||
echo "matrix=$(jq -c '{include: .}' <<< "$INCLUDE")" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$INCLUDE" != "[]" ]]; then
|
||||
echo "has_updates=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
# Fallback - proceed
|
||||
echo "packages=all" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
update-obs:
|
||||
name: Upload to OBS
|
||||
name: Upload to OBS (${{ matrix.owner }})
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-updates.outputs.has_updates == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.check-updates.outputs.matrix) }}
|
||||
env:
|
||||
OBS_BASE_PROJECT: ${{ matrix.project }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -179,12 +178,12 @@ jobs:
|
||||
# Use check-updates outputs when available
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
# Use version from check-updates job
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "packages=${{ matrix.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
|
||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||
# Scheduled run - dms-git only
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "packages=${{ matrix.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Triggered by schedule: updating git package"
|
||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||
# Manual workflow dispatch
|
||||
@@ -192,7 +191,7 @@ jobs:
|
||||
# Determine version for dms stable using the API
|
||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||
LATEST_TAG=$(curl -s https://api.github.com/repos/duxlabs-dev/dux-shell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
LATEST_TAG=$(curl -sL https://api.github.com/repos/duxlabs-dev/dux-shell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||
echo "Using latest release from API: $LATEST_TAG"
|
||||
@@ -204,14 +203,14 @@ jobs:
|
||||
|
||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||
echo "packages=${{ matrix.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: all (filtered to: ${{ matrix.packages }})"
|
||||
else
|
||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||
fi
|
||||
else
|
||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||
echo "packages=${{ matrix.packages }}" >> $GITHUB_OUTPUT
|
||||
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -311,20 +310,18 @@ jobs:
|
||||
cache-dependency-path: core/go.sum
|
||||
|
||||
- name: Install OSC
|
||||
env:
|
||||
OBS_USERNAME: ${{ secrets[format('OBS_USERNAME{0}', matrix.suffix)] }}
|
||||
OBS_PASSWORD: ${{ secrets[format('OBS_PASSWORD{0}', matrix.suffix)] }}
|
||||
run: |
|
||||
: "${OBS_USERNAME:?OBS_USERNAME${{ matrix.suffix }} is not set}" "${OBS_PASSWORD:?OBS_PASSWORD${{ matrix.suffix }} is not set}"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y osc
|
||||
|
||||
mkdir -p ~/.config/osc
|
||||
cat > ~/.config/osc/oscrc << EOF
|
||||
[general]
|
||||
apiurl = https://api.opensuse.org
|
||||
|
||||
[https://api.opensuse.org]
|
||||
user = ${{ secrets.OBS_USERNAME }}
|
||||
pass = ${{ secrets.OBS_PASSWORD }}
|
||||
EOF
|
||||
chmod 600 ~/.config/osc/oscrc
|
||||
umask 077
|
||||
printf '[general]\napiurl = https://api.opensuse.org\n\n[https://api.opensuse.org]\nuser = %s\npass = %s\n' \
|
||||
"$OBS_USERNAME" "$OBS_PASSWORD" > ~/.config/osc/oscrc
|
||||
|
||||
# Cache OBS bundled Go toolchains
|
||||
- name: Cache OBS bundled Go toolchains (dms-git)
|
||||
@@ -405,7 +402,7 @@ jobs:
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### OBS Package Upload Summary (${{ matrix.owner }})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||
@@ -458,10 +455,10 @@ jobs:
|
||||
|
||||
case "$PKG" in
|
||||
dms)
|
||||
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/${OBS_BASE_PROJECT}:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
dms-git)
|
||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/${OBS_BASE_PROJECT}:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@@ -477,5 +474,5 @@ jobs:
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
|
||||
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/${OBS_BASE_PROJECT})." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -16,6 +16,15 @@ on:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
default: ""
|
||||
owners:
|
||||
description: "PPA owners to publish to"
|
||||
required: false
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- duxlabs
|
||||
- avengemedia
|
||||
schedule:
|
||||
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||
|
||||
@@ -44,6 +53,8 @@ jobs:
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
env:
|
||||
OWNERS: ${{ inputs.owners || 'all' }}
|
||||
run: |
|
||||
chmod +x distro/scripts/ppa-sync-plan.sh
|
||||
|
||||
@@ -59,10 +70,20 @@ jobs:
|
||||
ARGS+=(--rebuild "$REBUILD_RELEASE")
|
||||
fi
|
||||
|
||||
TARGETS_JSON=$(distro/scripts/ppa-sync-plan.sh "${ARGS[@]}" 2> ppa-audit.log)
|
||||
# Planned per owner: an empty new PPA needs everything, the old one may need nothing.
|
||||
TARGETS_JSON='[]'
|
||||
: > ppa-audit.log
|
||||
for T in duxlabs: avengemedia:_LEGACY; do
|
||||
OWNER="${T%%:*}" SUFFIX="${T#*:}"
|
||||
[[ "$OWNERS" == all || "$OWNERS" == "$OWNER" ]] || continue
|
||||
echo "~${OWNER}:" >> ppa-audit.log
|
||||
PLAN=$(PPA_OWNER="$OWNER" distro/scripts/ppa-sync-plan.sh "${ARGS[@]}" 2>> ppa-audit.log)
|
||||
TARGETS_JSON=$(jq -c --arg o "$OWNER" --arg s "$SUFFIX" --argjson p "$PLAN" \
|
||||
'. + [$p[] | {owner: $o, suffix: $s, target: .}]' <<< "$TARGETS_JSON")
|
||||
done
|
||||
cat ppa-audit.log
|
||||
|
||||
TARGETS=$(echo "$TARGETS_JSON" | jq -r 'join(" ")')
|
||||
TARGETS=$(jq -r 'map(.owner + ":" + .target) | join(" ")' <<< "$TARGETS_JSON")
|
||||
if [[ "$TARGETS_JSON" != "[]" ]]; then
|
||||
echo "has_updates=true" >> "$GITHUB_OUTPUT"
|
||||
echo "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
||||
@@ -76,7 +97,7 @@ jobs:
|
||||
fi
|
||||
|
||||
upload-ppa:
|
||||
name: Upload ${{ matrix.target }}
|
||||
name: Upload ${{ matrix.owner }} ${{ matrix.target }}
|
||||
needs: check-updates
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-updates.outputs.has_updates == 'true'
|
||||
@@ -84,9 +105,9 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target: ${{ fromJson(needs.check-updates.outputs.targets_json) }}
|
||||
include: ${{ fromJson(needs.check-updates.outputs.targets_json) }}
|
||||
concurrency:
|
||||
group: ppa-dms-${{ matrix.target }}
|
||||
group: ppa-dms-${{ matrix.owner }}-${{ matrix.target }}
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
@@ -118,8 +139,9 @@ jobs:
|
||||
|
||||
- name: Configure GPG
|
||||
env:
|
||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
GPG_KEY: ${{ secrets[format('GPG_PRIVATE_KEY{0}', matrix.suffix)] }}
|
||||
run: |
|
||||
: "${GPG_KEY:?GPG_PRIVATE_KEY${{ matrix.suffix }} is not set}"
|
||||
echo "$GPG_KEY" | gpg --import
|
||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> "$GITHUB_ENV"
|
||||
@@ -127,9 +149,11 @@ jobs:
|
||||
- name: Upload target
|
||||
env:
|
||||
TARGET: ${{ matrix.target }}
|
||||
LAUNCHPAD_SSH_PRIVATE_KEY: ${{ secrets.LAUNCHPAD_SSH_PRIVATE_KEY }}
|
||||
LAUNCHPAD_SSH_LOGIN: ${{ secrets.LAUNCHPAD_SSH_LOGIN }}
|
||||
PPA_OWNER: ${{ matrix.owner }}
|
||||
LAUNCHPAD_SSH_PRIVATE_KEY: ${{ secrets[format('LAUNCHPAD_SSH_PRIVATE_KEY{0}', matrix.suffix)] }}
|
||||
LAUNCHPAD_SSH_LOGIN: ${{ secrets[format('LAUNCHPAD_SSH_LOGIN{0}', matrix.suffix)] }}
|
||||
run: |
|
||||
: "${LAUNCHPAD_SSH_PRIVATE_KEY:?LAUNCHPAD_SSH_PRIVATE_KEY${{ matrix.suffix }} is not set}" "${LAUNCHPAD_SSH_LOGIN:?LAUNCHPAD_SSH_LOGIN${{ matrix.suffix }} is not set}"
|
||||
IFS=':' read -r PACKAGE UBUNTU_SERIES PPA_NUM <<< "$TARGET"
|
||||
|
||||
case "$PACKAGE" in
|
||||
@@ -138,7 +162,7 @@ jobs:
|
||||
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "Uploading $PACKAGE to $PPA_NAME/$UBUNTU_SERIES with ppa$PPA_NUM"
|
||||
echo "Uploading $PACKAGE to ~$PPA_OWNER/$PPA_NAME/$UBUNTU_SERIES with ppa$PPA_NUM"
|
||||
bash distro/scripts/ppa-upload.sh "$PACKAGE" "$PPA_NAME" "$UBUNTU_SERIES" "$PPA_NUM"
|
||||
|
||||
- name: Summary
|
||||
@@ -146,6 +170,7 @@ jobs:
|
||||
run: |
|
||||
echo "### PPA Package Upload" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **Owner:** ${{ matrix.owner }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **DMS PPA:** https://launchpad.net/~${{ matrix.owner }}/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "- **DMS-Git PPA:** https://launchpad.net/~${{ matrix.owner }}/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -43,11 +43,11 @@ jobs:
|
||||
- name: Check published commit
|
||||
id: check
|
||||
env:
|
||||
MANIFEST_URL: https://void.danklinux.com/dms/current/manifest.json
|
||||
MANIFEST_URL: https://void.duxlabs.dev/dms/current/manifest.json
|
||||
run: |
|
||||
SHOULD_BUILD=true
|
||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||
# dms/current is shared with dux-repo; read this repo's own entry
|
||||
# dux-repo's legacy greeter mirror also writes dms/current; read this repo's own entry
|
||||
PUBLISHED_COMMIT="$(curl -fsSL --retry 3 "$MANIFEST_URL" \
|
||||
| jq -r --arg repo "${{ github.repository }}" '.sources[$repo] // .source_commit // empty' 2>/dev/null || true)"
|
||||
if [ "$PUBLISHED_COMMIT" = "${{ github.sha }}" ]; then
|
||||
@@ -159,8 +159,8 @@ jobs:
|
||||
- name: Configure repositories
|
||||
run: |
|
||||
# Append the repository to repos-remote templates so xbps-src translates it automatically
|
||||
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
|
||||
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
|
||||
echo "repository=https://void.duxlabs.dev/dux-repo/current" >> void-packages/etc/xbps.d/repos-remote.conf
|
||||
echo "repository=https://void.duxlabs.dev/dux-repo/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
|
||||
|
||||
# Add any existing compiled packages to the build cache directory to avoid rebuilds
|
||||
if [ -d "r2-repo/current" ]; then
|
||||
@@ -278,36 +278,9 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Index and sign repository
|
||||
run: |
|
||||
cd r2-repo/current
|
||||
|
||||
rm -f *.sig
|
||||
|
||||
# Guard: nothing to index if no .xbps files exist
|
||||
if ! ls *.xbps 1>/dev/null 2>&1; then
|
||||
echo "⚠️ No .xbps files found to index, skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Regenerate repo index
|
||||
xbps-rindex -a $(pwd)/*.xbps
|
||||
xbps-rindex --remove-obsoletes $(pwd)
|
||||
|
||||
# Remove signatures left behind by obsolete packages. Signatures for
|
||||
# unchanged immutable packages are retained byte-for-byte.
|
||||
for sig in *.sig2; do
|
||||
[ -e "$sig" ] || break
|
||||
[ -f "${sig%.sig2}" ] || rm -f "$sig"
|
||||
done
|
||||
|
||||
# Sign repository
|
||||
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
|
||||
chmod 600 /tmp/xbps_privkey.pem
|
||||
|
||||
xbps-rindex --sign --signedby "AvengeMedia <[email protected]>" --privkey /tmp/xbps_privkey.pem $(pwd)
|
||||
xbps-rindex --sign-pkg --privkey /tmp/xbps_privkey.pem $(pwd)/*.xbps
|
||||
|
||||
rm -f /tmp/xbps_privkey.pem
|
||||
env:
|
||||
XBPS_PRIVATE_KEY: ${{ secrets.XBPS_PRIVATE_KEY }}
|
||||
run: bash .github/scripts/publish-void-r2.sh index
|
||||
|
||||
- name: Publish repository to R2
|
||||
run: bash .github/scripts/publish-void-r2.sh publish
|
||||
@@ -795,6 +795,7 @@ func getCommonCommands() []*cobra.Command {
|
||||
systemdCmd,
|
||||
switchUserCmd,
|
||||
switchCmd,
|
||||
migrateCmd,
|
||||
dmenuCmd,
|
||||
}...)
|
||||
}
|
||||
@@ -219,6 +219,7 @@ func runDoctor(cmd *cobra.Command, args []string) {
|
||||
checkSystemInfo(),
|
||||
checkVersions(qsMissingFeatures),
|
||||
checkDMSInstallation(),
|
||||
checkPackageRepos(),
|
||||
checkWindowManagers(),
|
||||
qsFeatures,
|
||||
checkOptionalDependencies(),
|
||||
@@ -804,6 +805,36 @@ func checkDMSInstallation() []checkResult {
|
||||
return results
|
||||
}
|
||||
|
||||
func checkPackageRepos() []checkResult {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
distro, err := distros.NewDistribution(osInfo.Distribution.ID, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return packageReposResult(distro)
|
||||
}
|
||||
|
||||
func packageReposResult(distro distros.Distribution) []checkResult {
|
||||
moves, ok := distros.PlanRepoMoves(distro)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
url := doctorDocsURL + "#package-repos"
|
||||
legacy := distros.LegacyRepoPaths(moves)
|
||||
if len(legacy) > 0 {
|
||||
return []checkResult{{catInstallation, "Package Repos", statusWarn, "Legacy repositories, run: " + migrateDuxCommand, strings.Join(legacy, ", "), url}}
|
||||
}
|
||||
for _, move := range moves {
|
||||
if move.NewPresent {
|
||||
return []checkResult{{catInstallation, "Package Repos", statusOK, "Current", "", url}}
|
||||
}
|
||||
}
|
||||
return []checkResult{{catInstallation, "Package Repos", statusInfo, "No DMS package repositories", "", url}}
|
||||
}
|
||||
|
||||
func checkWindowManagers() []checkResult {
|
||||
compositors := []struct {
|
||||
name, versionCmd, versionArg string
|
||||
|
||||
@@ -30,6 +30,10 @@ var greeterCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func greeterRepoURL(config distros.DistroConfig) string {
|
||||
return config.Constructor(config, nil).GetPackageMapping(0)["dms-greeter"].RepoURL
|
||||
}
|
||||
|
||||
func isLegacyWrapperScript(path string) bool {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -63,13 +67,14 @@ func greeterPackageInstallHint() string {
|
||||
case distros.FamilySUSE:
|
||||
return "Install with 'sudo zypper install dms-greeter' (requires Dux Labs OBS repo — see " + site.Docs + "/dms-greeter/installation#opensuse--derivatives)"
|
||||
case distros.FamilyUbuntu:
|
||||
return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)"
|
||||
repo := greeterRepoURL(config)
|
||||
return fmt.Sprintf("Install with 'sudo apt install dms-greeter' (requires %s: sudo add-apt-repository %s)", repo, repo)
|
||||
case distros.FamilyFedora:
|
||||
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
||||
return fmt.Sprintf("Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable %s)", greeterRepoURL(config))
|
||||
case distros.FamilyArch:
|
||||
return "Install from AUR with 'paru -S dux-greeter-bin' or 'yay -S dux-greeter-bin'"
|
||||
case distros.FamilyVoid:
|
||||
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=" + distros.VoidDMSRepo + "' | sudo tee /etc/xbps.d/dms.conf)"
|
||||
return "Install with 'sudo xbps-install -S dms-greeter' (requires DMS XBPS repo: echo 'repository=" + distros.VoidDepsRepo + "' | sudo tee /etc/xbps.d/dux.conf)"
|
||||
default:
|
||||
return "Install the dms-greeter package for your distribution"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/distros"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/privesc"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const migrateDuxCommand = "dms migrate dux"
|
||||
|
||||
var (
|
||||
migrateDryRun bool
|
||||
migrateAssumeYes bool
|
||||
)
|
||||
|
||||
var migrateCmd = &cobra.Command{
|
||||
Use: "migrate",
|
||||
Short: "One-time migrations for existing installs",
|
||||
}
|
||||
|
||||
var migrateDuxCmd = &cobra.Command{
|
||||
Use: "dux",
|
||||
Short: "Move DMS package repositories to the Dux Labs owner",
|
||||
Long: `Move the DMS package repositories from the legacy AvengeMedia owner to Dux Labs.
|
||||
|
||||
Adds the Dux Labs repository for every legacy one found, then removes the legacy
|
||||
one. Installed packages are not changed; the next update comes from the new repo.`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runMigrateDux(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrateDuxCmd.Flags().BoolVar(&migrateDryRun, "dry-run", false, "Print the commands without running them")
|
||||
migrateDuxCmd.Flags().BoolVarP(&migrateAssumeYes, "yes", "y", false, "Do not ask for confirmation")
|
||||
migrateCmd.AddCommand(migrateDuxCmd)
|
||||
}
|
||||
|
||||
func runMigrateDux() error {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("detecting distribution: %w", err)
|
||||
}
|
||||
distro, err := distros.NewDistribution(osInfo.Distribution.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
moves, ok := distros.PlanRepoMoves(distro)
|
||||
if !ok {
|
||||
fmt.Printf("Nothing to migrate on %s.\n", osInfo.PrettyName)
|
||||
return nil
|
||||
}
|
||||
mover := distro.(distros.RepoMover)
|
||||
commands := mover.RepoMoveCommands(moves)
|
||||
if len(commands) == 0 {
|
||||
fmt.Println("Already migrated.")
|
||||
return nil
|
||||
}
|
||||
if osInfo.Immutable {
|
||||
imageMover, ok := distro.(distros.ImageRepoMover)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s is an image-based system; move the repositories with your image tooling", osInfo.PrettyName)
|
||||
}
|
||||
printRepoMoves(moves, imageMover.ImageRepoMoveCommands(moves))
|
||||
fmt.Printf("\n%s is image-based, so run these yourself.\n", osInfo.PrettyName)
|
||||
return nil
|
||||
}
|
||||
|
||||
printRepoMoves(moves, commands)
|
||||
if migrateDryRun {
|
||||
return nil
|
||||
}
|
||||
if !migrateAssumeYes && !askYes("\nContinue? [y/N] ") {
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if privesc.CheckCached(ctx) != nil {
|
||||
if err := privesc.Run(ctx, "", "true"); err != nil {
|
||||
return fmt.Errorf("authentication failed: %w", err)
|
||||
}
|
||||
}
|
||||
err = runWithProgressOutput(func(progressChan chan<- distros.InstallProgressMsg) error {
|
||||
return mover.ApplyRepoMoves(ctx, moves, "", progressChan)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Done. 'dms doctor' shows the result; the shell notice clears on its next start.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func printRepoMoves(moves []distros.RepoMove, commands []string) {
|
||||
fmt.Println("Repositories to move:")
|
||||
for _, move := range moves {
|
||||
switch {
|
||||
case move.OldPresent:
|
||||
fmt.Printf(" %s → %s\n", move.Old, move.New)
|
||||
case !move.NewPresent && move.NewPath != "":
|
||||
fmt.Printf(" add %s\n", move.New)
|
||||
}
|
||||
}
|
||||
fmt.Println("\nCommands:")
|
||||
for _, command := range commands {
|
||||
fmt.Printf(" %s\n", command)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/distros"
|
||||
)
|
||||
|
||||
type repoMoverFake struct {
|
||||
distros.Distribution
|
||||
moves []distros.RepoMove
|
||||
}
|
||||
|
||||
func (f *repoMoverFake) RepoMoves() []distros.RepoMove { return f.moves }
|
||||
func (f *repoMoverFake) ApplyRepoMoves(context.Context, []distros.RepoMove, string, chan<- distros.InstallProgressMsg) error {
|
||||
return nil
|
||||
}
|
||||
func (f *repoMoverFake) RepoMoveCommands([]distros.RepoMove) []string { return nil }
|
||||
|
||||
type plainDistroFake struct{ distros.Distribution }
|
||||
|
||||
func TestRunWithProgressOutputPrintsRepeatedLinesOnce(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
err := runWithProgressOutputTo(&out, func(ch chan<- distros.InstallProgressMsg) error {
|
||||
ch <- distros.InstallProgressMsg{Step: "Removing COPR repo...", LogOutput: "Repository successfully removed."}
|
||||
ch <- distros.InstallProgressMsg{Step: "Removing COPR repo...", LogOutput: "Repository successfully removed."}
|
||||
ch <- distros.InstallProgressMsg{Step: "Removing COPR repo...", LogOutput: "Repository successfully removed."}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "Removing COPR repo...\n Repository successfully removed.\n"
|
||||
if out.String() != want {
|
||||
t.Fatalf("got %q, want %q", out.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPackageRepos(t *testing.T) {
|
||||
legacy := distros.RepoMove{Kind: distros.RepoTypeCOPR, Old: "avengemedia/dms", New: "duxlabs/dms", OldPath: "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:avengemedia:dms.repo", OldPresent: true}
|
||||
legacyDeps := distros.RepoMove{Kind: distros.RepoTypeCOPR, Old: "avengemedia/danklinux", New: "duxlabs/dux-repo", OldPath: "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:avengemedia:danklinux.repo", OldPresent: true}
|
||||
current := distros.RepoMove{Kind: distros.RepoTypeCOPR, Old: "avengemedia/dms", New: "duxlabs/dms", NewPresent: true}
|
||||
absent := distros.RepoMove{Kind: distros.RepoTypeCOPR, Old: "avengemedia/dms", New: "duxlabs/dms"}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
moves []distros.RepoMove
|
||||
status status
|
||||
message string
|
||||
}{
|
||||
{"legacy", []distros.RepoMove{legacy, legacyDeps, current}, statusWarn, "Legacy repositories, run: dms migrate dux"},
|
||||
{"current", []distros.RepoMove{current, absent}, statusOK, "Current"},
|
||||
{"none", []distros.RepoMove{absent}, statusInfo, "No DMS package repositories"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
results := packageReposResult(&repoMoverFake{moves: tc.moves})
|
||||
if len(results) != 1 || results[0].status != tc.status || results[0].message != tc.message {
|
||||
t.Fatalf("got %+v, want %v %q", results, tc.status, tc.message)
|
||||
}
|
||||
if tc.status == statusWarn && results[0].details != legacy.OldPath+", "+legacyDeps.OldPath {
|
||||
t.Fatalf("details = %q, want the legacy paths on one line", results[0].details)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
voidConf := "/etc/xbps.d/custom.conf"
|
||||
voidMoves := []distros.RepoMove{
|
||||
{Kind: distros.RepoTypeXBPS, Old: "https://void.danklinux.com/dms/current", New: "https://void.duxlabs.dev/dms/current", OldPath: voidConf, NewPath: voidConf, OldPresent: true},
|
||||
{Kind: distros.RepoTypeXBPS, Old: "https://void.danklinux.com/danklinux/current", New: "https://void.duxlabs.dev/dux-repo/current", OldPath: voidConf, NewPath: voidConf, OldPresent: true},
|
||||
}
|
||||
if results := packageReposResult(&repoMoverFake{moves: voidMoves}); len(results) != 1 || results[0].status != statusWarn || results[0].details != voidConf {
|
||||
t.Fatalf("got %+v, want one warning naming %s once", results, voidConf)
|
||||
}
|
||||
|
||||
if results := packageReposResult(&plainDistroFake{}); results != nil {
|
||||
t.Fatalf("printed %+v on a family without repo moves", results)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -142,7 +143,7 @@ func runSwitch(channel string, packages []string) error {
|
||||
if shellChanges {
|
||||
fmt.Println("The shell restarts when the switch is done.")
|
||||
}
|
||||
if !switchAssumeYes && !askSwitch("\nContinue? [y/N] ") {
|
||||
if !switchAssumeYes && !askYes("\nContinue? [y/N] ") {
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
|
||||
@@ -171,7 +172,7 @@ func runSwitch(channel string, packages []string) error {
|
||||
if snapshotLabel == "beta" && !switchAssumeYes {
|
||||
if path, ok := backup.LatestSnapshot("stable"); ok {
|
||||
fmt.Printf("\nSettings saved by beta may lose options on stable. Restore the backup from your last\nstable run (%s)? Your beta settings stay in the beta backup. [y/N] ", path)
|
||||
if askSwitch("") {
|
||||
if askYes("") {
|
||||
restore = path
|
||||
}
|
||||
}
|
||||
@@ -185,28 +186,39 @@ func runSwitch(channel string, packages []string) error {
|
||||
}
|
||||
|
||||
func runSwaps(ctx context.Context, swapper distros.VariantSwapper, swaps []distros.VariantSwap) error {
|
||||
return runWithProgressOutput(func(progressChan chan<- distros.InstallProgressMsg) error {
|
||||
return swapper.SwapVariants(ctx, swaps, "", progressChan)
|
||||
})
|
||||
}
|
||||
|
||||
func runWithProgressOutput(run func(chan<- distros.InstallProgressMsg) error) error {
|
||||
return runWithProgressOutputTo(os.Stdout, run)
|
||||
}
|
||||
|
||||
func runWithProgressOutputTo(out io.Writer, run func(chan<- distros.InstallProgressMsg) error) error {
|
||||
progressChan := make(chan distros.InstallProgressMsg, 100)
|
||||
swapErr := make(chan error, 1)
|
||||
runErr := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(progressChan)
|
||||
swapErr <- swapper.SwapVariants(ctx, swaps, "", progressChan)
|
||||
runErr <- run(progressChan)
|
||||
}()
|
||||
|
||||
// runWithProgress repeats the step on every output line, so only print it when it changes.
|
||||
lastStep := ""
|
||||
// runWithProgress repeats the step and the last output line on every tick, so only print changes.
|
||||
lastStep, lastLog := "", ""
|
||||
for msg := range progressChan {
|
||||
switch {
|
||||
case msg.Error != nil:
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", msg.Error)
|
||||
case msg.Step != "" && msg.Step != lastStep:
|
||||
fmt.Println(msg.Step)
|
||||
fmt.Fprintln(out, msg.Step)
|
||||
lastStep = msg.Step
|
||||
}
|
||||
if msg.LogOutput != "" {
|
||||
fmt.Printf(" %s\n", msg.LogOutput)
|
||||
if msg.LogOutput != "" && msg.LogOutput != lastLog {
|
||||
fmt.Fprintf(out, " %s\n", msg.LogOutput)
|
||||
lastLog = msg.LogOutput
|
||||
}
|
||||
}
|
||||
return <-swapErr
|
||||
return <-runErr
|
||||
}
|
||||
|
||||
// restartShellAfterSwitch stops the shell before restoring, or the running one would migrate the
|
||||
@@ -268,7 +280,7 @@ func switchLabel(m distros.PackageMapping) string {
|
||||
return m.RepoURL + "/" + m.Name
|
||||
}
|
||||
|
||||
func askSwitch(prompt string) bool {
|
||||
func askYes(prompt string) bool {
|
||||
if !stdinIsTTY() {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -502,7 +502,7 @@ func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressCha
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: endProgress,
|
||||
Step: "Installation step complete",
|
||||
Step: stepMessage,
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
@@ -541,7 +541,7 @@ func (b *BaseDistribution) runWithProgressStepTimeout(cmd *exec.Cmd, progressCha
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: phase,
|
||||
Progress: progress,
|
||||
Step: "Installing...",
|
||||
Step: stepMessage,
|
||||
IsComplete: false,
|
||||
LogOutput: lastOutput,
|
||||
}
|
||||
|
||||
@@ -159,11 +159,11 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
// DMS packages from OBS with variant support
|
||||
"dms (Dux Shell)": d.getDmsMapping(variants["dms (Dux Shell)"]),
|
||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeOBS, "home:AvengeMedia:danklinux"),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypeOBS, "home:AvengeMedia:danklinux"),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeOBS, obsDeps),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: obsDeps},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: obsDeps},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: obsDeps},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypeOBS, obsDeps),
|
||||
}
|
||||
|
||||
if wm == deps.WindowManagerNiri {
|
||||
@@ -177,14 +177,14 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
|
||||
func (d *DebianDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
|
||||
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: obsDMSGit}
|
||||
}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: obsDMS}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceQuickshellGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
// Debian 13 ships stable quickshell in trixie-backports only.
|
||||
if d.debianNeedsQuickshellBackports() {
|
||||
@@ -244,16 +244,16 @@ func (d *DebianDistribution) ensureQuickshellBackports(ctx context.Context, syst
|
||||
|
||||
func (d *DebianDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
@@ -491,6 +491,62 @@ func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []s
|
||||
return append(args, packages...)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) RepoMoves() []RepoMove {
|
||||
if !obsMigrationReady || d.isPika() {
|
||||
return nil
|
||||
}
|
||||
return obsAptChannel.moves(repoPathExists)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) ApplyRepoMoves(ctx context.Context, moves []RepoMove, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if err := d.enableOBSRepos(ctx, repoMoveAdds(moves), sudoPassword, progressChan); err != nil {
|
||||
return err
|
||||
}
|
||||
removes := repoMoveRemoves(moves)
|
||||
for _, move := range removes {
|
||||
command := fmt.Sprintf("rm -f %s %s", obsAptListFile(move.Old), obsAptKeyring(move.Old))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.30,
|
||||
Step: fmt.Sprintf("Removing OBS repo %s...", move.Old),
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo " + command,
|
||||
}
|
||||
if err := d.runWithProgress(privescExec(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.30, 0.35); err != nil {
|
||||
return fmt.Errorf("failed to remove OBS repo %s: %w", move.Old, err)
|
||||
}
|
||||
}
|
||||
if len(removes) == 0 {
|
||||
return nil
|
||||
}
|
||||
return d.aptUpdate(ctx, sudoPassword, progressChan)
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) RepoMoveCommands(moves []RepoMove) []string {
|
||||
osInfo, err := GetOSInfo()
|
||||
if err != nil {
|
||||
osInfo = &OSInfo{}
|
||||
}
|
||||
return debianRepoMoveCommands(moves, debianOBSRepoVersion(osInfo, readDebianAPTSuites()), debianRepoArchitecture(osInfo.Architecture))
|
||||
}
|
||||
|
||||
func debianRepoMoveCommands(moves []RepoMove, dist, arch string) []string {
|
||||
var commands []string
|
||||
for _, add := range repoMoveAdds(moves) {
|
||||
baseURL := obsBaseURL(add.RepoURL, dist)
|
||||
commands = append(commands,
|
||||
fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor | sudo tee %s", baseURL, obsAptKeyring(add.RepoURL)),
|
||||
fmt.Sprintf("echo 'deb [signed-by=%s arch=%s] %s/ /' | sudo tee %s", obsAptKeyring(add.RepoURL), arch, baseURL, obsAptListFile(add.RepoURL)))
|
||||
}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
commands = append(commands, fmt.Sprintf("sudo rm -f %s %s", obsAptListFile(move.Old), obsAptKeyring(move.Old)))
|
||||
}
|
||||
if len(commands) > 0 {
|
||||
commands = append(commands, "sudo apt-get update")
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
enabledRepos := make(map[string]bool)
|
||||
|
||||
@@ -506,20 +562,17 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||
d.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||
|
||||
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||
baseURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s", repoPath, obsRepo)
|
||||
baseURL := obsBaseURL(pkg.RepoURL, obsRepo)
|
||||
|
||||
// Check if repository already exists
|
||||
listFile := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", repoName)
|
||||
listFile := obsAptListFile(pkg.RepoURL)
|
||||
if debianOBSListFileMatches(listFile, obsRepo) {
|
||||
d.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
continue
|
||||
}
|
||||
|
||||
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||
keyringPath := obsAptKeyring(pkg.RepoURL)
|
||||
|
||||
// Create keyrings directory if it doesn't exist
|
||||
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||
|
||||
@@ -167,7 +167,7 @@ func fedoraInstalledFromCOPR(pkg, coprRepo string) bool {
|
||||
if err != nil {
|
||||
return rpmLooksLikeGitBuild(pkg)
|
||||
}
|
||||
return strings.HasSuffix(strings.TrimSpace(string(out)), ":"+strings.ReplaceAll(coprRepo, "/", ":"))
|
||||
return coprFromRepoMatches(string(out), coprRepo)
|
||||
}
|
||||
|
||||
// niri-git carries "git" in its version; dms-git only differs by its Epoch of 2.
|
||||
@@ -187,15 +187,20 @@ func (f *FedoraDistribution) SwapVariants(ctx context.Context, swaps []VariantSw
|
||||
|
||||
for _, swap := range swaps {
|
||||
command := fmt.Sprintf("dnf swap -y %s %s", swap.From.Name, swap.To.Name)
|
||||
disabledRepo := ""
|
||||
var disabledRepos []string
|
||||
if swap.From.Name == swap.To.Name {
|
||||
// Left enabled, the old COPR wins distro-sync whenever its version sorts higher (dms-git has Epoch 2).
|
||||
// An un-migrated host still has it under the legacy owner, so every alias goes.
|
||||
if swap.From.Repository == RepoTypeCOPR {
|
||||
disable := privesc.ExecCommand(ctx, sudoPassword, "dnf copr disable -y "+swap.From.RepoURL)
|
||||
if err := f.runWithProgress(disable, progressChan, PhaseSystemPackages, 0.30, 0.35); err != nil {
|
||||
return fmt.Errorf("failed to disable COPR repo %s: %w", swap.From.RepoURL, err)
|
||||
for _, repo := range coprAliases(swap.From.RepoURL) {
|
||||
if !repoPathExists(coprRepoFile(repo)) {
|
||||
continue
|
||||
}
|
||||
if err := f.coprRepoOp(ctx, "disable", repo, sudoPassword, progressChan); err != nil {
|
||||
return err
|
||||
}
|
||||
disabledRepos = append(disabledRepos, repo)
|
||||
}
|
||||
disabledRepo = swap.From.RepoURL
|
||||
}
|
||||
command = "dnf distro-sync -y " + swap.To.Name
|
||||
}
|
||||
@@ -207,11 +212,10 @@ func (f *FedoraDistribution) SwapVariants(ctx context.Context, swaps []VariantSw
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo " + command,
|
||||
}
|
||||
if err := f.runWithProgress(privesc.ExecCommand(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.40, 0.85); err != nil {
|
||||
if disabledRepo != "" {
|
||||
reenable := privesc.ExecCommand(ctx, sudoPassword, "dnf copr enable -y "+disabledRepo)
|
||||
if reErr := reenable.Run(); reErr != nil {
|
||||
f.log(fmt.Sprintf("Warning: could not re-enable COPR repo %s: %v", disabledRepo, reErr))
|
||||
if err := f.runWithProgress(privescExec(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.40, 0.85); err != nil {
|
||||
for _, repo := range disabledRepos {
|
||||
if reErr := privescExec(ctx, sudoPassword, "dnf copr enable -y "+repo).Run(); reErr != nil {
|
||||
f.log(fmt.Sprintf("Warning: could not re-enable COPR repo %s: %v", repo, reErr))
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to switch %s to %s: %w", swap.From.Name, swap.To.Name, err)
|
||||
@@ -220,6 +224,60 @@ func (f *FedoraDistribution) SwapVariants(ctx context.Context, swaps []VariantSw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) coprRepoOp(ctx context.Context, verb, repo, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
command := fmt.Sprintf("dnf copr %s -y %s", verb, repo)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.30,
|
||||
Step: fmt.Sprintf("Running dnf copr %s %s...", verb, repo),
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo " + command,
|
||||
}
|
||||
if err := f.runWithProgress(privescExec(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.30, 0.35); err != nil {
|
||||
return fmt.Errorf("failed to %s COPR repo %s: %w", verb, repo, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) RepoMoves() []RepoMove {
|
||||
return coprChannel.moves(repoPathExists)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) ApplyRepoMoves(ctx context.Context, moves []RepoMove, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if err := f.enableCOPRRepos(ctx, repoMoveAdds(moves), sudoPassword, progressChan); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
if err := f.coprRepoOp(ctx, "remove", move.Old, sudoPassword, progressChan); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) RepoMoveCommands(moves []RepoMove) []string {
|
||||
var commands []string
|
||||
for _, add := range repoMoveAdds(moves) {
|
||||
commands = append(commands, "sudo dnf copr enable -y "+add.RepoURL)
|
||||
}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
commands = append(commands, "sudo dnf copr remove -y "+move.Old)
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
// Shown to the user, never run: image-based Fedora has no dnf copr.
|
||||
func (f *FedoraDistribution) ImageRepoMoveCommands(moves []RepoMove) []string {
|
||||
var commands []string
|
||||
for _, add := range repoMoveAdds(moves) {
|
||||
commands = append(commands, f.coprRepoFileCommand(add.RepoURL))
|
||||
}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
commands = append(commands, "sudo rm -f "+move.OldPath)
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return f.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
@@ -228,7 +286,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
packages := map[string]PackageMapping{
|
||||
// Standard DNF packages
|
||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: coprDeps},
|
||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||
@@ -236,11 +294,11 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
|
||||
// COPR packages
|
||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeCOPR, "avengemedia/danklinux"),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeCOPR, coprDeps),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: coprDeps},
|
||||
"dms (Dux Shell)": f.getDmsMapping(variants["dms (Dux Shell)"]),
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypeCOPR, "avengemedia/danklinux"),
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeCOPR, RepoURL: coprDeps},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypeCOPR, coprDeps),
|
||||
}
|
||||
|
||||
switch wm {
|
||||
@@ -259,10 +317,10 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
}
|
||||
|
||||
if f.isEL() {
|
||||
// EPEL 10 has neither, so the danklinux COPR builds them for the epel-10 chroots.
|
||||
// EPEL 10 has neither, so the deps COPR builds them for the epel-10 chroots.
|
||||
for _, name := range []string{"alacritty", "xwayland-satellite"} {
|
||||
if _, ok := packages[name]; ok {
|
||||
packages[name] = PackageMapping{Name: name, Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
packages[name] = PackageMapping{Name: name, Repository: RepoTypeCOPR, RepoURL: coprDeps}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,16 +330,16 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
|
||||
func (f *FedoraDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceQuickshellGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeCOPR, RepoURL: coprDeps}
|
||||
}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeCOPR, RepoURL: coprDeps}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms-git"}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: coprDMSGit}
|
||||
}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: coprDMS}
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||
@@ -648,7 +706,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||
}
|
||||
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
cmd := privescExec(ctx, sudoPassword,
|
||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -662,7 +720,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
||||
// Special handling for niri COPR repo - set priority=1
|
||||
if pkg.RepoURL == "yalter/niri-git" {
|
||||
f.log("Setting priority=1 for niri-git COPR repo...")
|
||||
repoFile := "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:yalter:niri-git.repo"
|
||||
repoFile := coprRepoFile("yalter/niri-git")
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.22,
|
||||
@@ -672,7 +730,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||
}
|
||||
|
||||
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||
priorityCmd := privescExec(ctx, sudoPassword,
|
||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -761,11 +819,6 @@ func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []st
|
||||
}
|
||||
|
||||
// Image-based Fedora has rpm-ostree but no dnf, so the .repo file is fetched directly.
|
||||
func coprRepoFilePath(repo string) string {
|
||||
owner, project, _ := strings.Cut(repo, "/")
|
||||
return fmt.Sprintf("/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:%s:%s.repo", owner, project)
|
||||
}
|
||||
|
||||
func (f *FedoraDistribution) coprRepoFileCommand(repo string) string {
|
||||
owner, project, _ := strings.Cut(repo, "/")
|
||||
chroot := "fedora-$(rpm -E %fedora)"
|
||||
@@ -773,7 +826,7 @@ func (f *FedoraDistribution) coprRepoFileCommand(repo string) string {
|
||||
chroot = "epel-$(rpm -E %rhel)"
|
||||
}
|
||||
return fmt.Sprintf("sudo curl -fsSLo %s https://copr.fedorainfracloud.org/coprs/%s/%s/repo/%s/%s-%s-%s.repo",
|
||||
coprRepoFilePath(repo), owner, project, chroot, owner, project, chroot)
|
||||
coprRepoFile(repo), owner, project, chroot, owner, project, chroot)
|
||||
}
|
||||
|
||||
// Shown to the user, never run. Terra-only packages have no rpm-ostree path.
|
||||
@@ -796,7 +849,11 @@ func (f *FedoraDistribution) LayerCommands(dependencies []deps.Dependency, wm de
|
||||
}
|
||||
uninstall = append(uninstall, swap.From.Name)
|
||||
if swap.From.Name == swap.To.Name && swap.From.Repository == RepoTypeCOPR {
|
||||
commands = append(commands, "sudo rm -f "+coprRepoFilePath(swap.From.RepoURL))
|
||||
var files []string
|
||||
for _, repo := range coprAliases(swap.From.RepoURL) {
|
||||
files = append(files, coprRepoFile(repo))
|
||||
}
|
||||
commands = append(commands, "sudo rm -f "+strings.Join(files, " "))
|
||||
}
|
||||
}
|
||||
dnfPkgs, coprPkgs, manualPkgs, _ := f.categorizePackages(dependencies, wm, reinstallFlags, skip)
|
||||
@@ -811,7 +868,7 @@ func (f *FedoraDistribution) LayerCommands(dependencies []deps.Dependency, wm de
|
||||
seen[pkg.RepoURL] = true
|
||||
commands = append(commands, f.coprRepoFileCommand(pkg.RepoURL))
|
||||
if pkg.RepoURL == "yalter/niri-git" {
|
||||
commands = append(commands, "echo priority=1 | sudo tee -a /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:yalter:niri-git.repo")
|
||||
commands = append(commands, "echo priority=1 | sudo tee -a "+coprRepoFile("yalter/niri-git"))
|
||||
}
|
||||
}
|
||||
for _, dep := range dependencies {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
func TestFedoraPackageMapping_EnterpriseLinuxUsesCOPRForMissingEPELPackages(t *testing.T) {
|
||||
el := NewFedoraDistribution(Registry["rhel"], nil).GetPackageMapping(deps.WindowManagerNiri)
|
||||
for _, name := range []string{"alacritty", "xwayland-satellite"} {
|
||||
if el[name].Repository != RepoTypeCOPR || el[name].RepoURL != "avengemedia/danklinux" {
|
||||
t.Errorf("rhel %s = %+v, want avengemedia/danklinux COPR", name, el[name])
|
||||
if el[name].Repository != RepoTypeCOPR || el[name].RepoURL != "duxlabs/dux-repo" {
|
||||
t.Errorf("rhel %s = %+v, want duxlabs/dux-repo COPR", name, el[name])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestFedoraLayerCommands(t *testing.T) {
|
||||
got := strings.Join(f.LayerCommands(dependencies, deps.WindowManagerNiri, nil, map[string]bool{"danksearch": true}), "\n")
|
||||
|
||||
for _, want := range []string{
|
||||
"_copr:copr.fedorainfracloud.org:avengemedia:dms.repo https://copr.fedorainfracloud.org/coprs/avengemedia/dms/repo/fedora-$(rpm -E %fedora)/avengemedia-dms-fedora-$(rpm -E %fedora).repo",
|
||||
"_copr:copr.fedorainfracloud.org:duxlabs:dms.repo https://copr.fedorainfracloud.org/coprs/duxlabs/dms/repo/fedora-$(rpm -E %fedora)/duxlabs-dms-fedora-$(rpm -E %fedora).repo",
|
||||
"_copr:copr.fedorainfracloud.org:yalter:niri-git.repo",
|
||||
"echo priority=1 | sudo tee -a",
|
||||
"sudo rpm-ostree install xwayland-satellite dms niri matugen",
|
||||
@@ -163,8 +163,8 @@ func TestFedoraLayerCommands(t *testing.T) {
|
||||
t.Fatalf("unexpected %q in:\n%s", unwanted, got)
|
||||
}
|
||||
}
|
||||
if strings.Count(got, "avengemedia:danklinux.repo https") != 1 {
|
||||
t.Fatalf("danklinux repo must be fetched exactly once:\n%s", got)
|
||||
if strings.Count(got, "duxlabs:dux-repo.repo https") != 1 {
|
||||
t.Fatalf("deps repo must be fetched exactly once:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ func TestFedoraLayerCommandsSwapVariants(t *testing.T) {
|
||||
installed := map[string]bool{"dms-greeter": true, "dms": true}
|
||||
f.rpmInstalled = func(pkg string) bool { return installed[pkg] }
|
||||
orig := installedFromCOPR
|
||||
installedFromCOPR = func(pkg, repo string) bool { return pkg == "dms" && repo == "avengemedia/dms-git" }
|
||||
installedFromCOPR = func(pkg, repo string) bool { return pkg == "dms" && repo == "duxlabs/dms-git" }
|
||||
t.Cleanup(func() { installedFromCOPR = orig })
|
||||
|
||||
dependencies := []deps.Dependency{
|
||||
@@ -193,7 +193,7 @@ func TestFedoraLayerCommandsSwapVariants(t *testing.T) {
|
||||
got := strings.Join(f.LayerCommands(dependencies, deps.WindowManagerNiri, reinstall, nil), "\n")
|
||||
|
||||
for _, want := range []string{
|
||||
"sudo rm -f /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:avengemedia:dms-git.repo",
|
||||
"sudo rm -f /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:duxlabs:dms-git.repo /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:avengemedia:dms-git.repo",
|
||||
"sudo rpm-ostree install dms dms-greeter-git --uninstall dms dms-greeter",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
|
||||
@@ -132,6 +132,70 @@ func (o *OpenSUSEDistribution) SwapVariants(ctx context.Context, swaps []Variant
|
||||
return o.runWithProgress(privesc.ExecCommand(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.40, 0.85)
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) RepoMoves() []RepoMove {
|
||||
if !obsMigrationReady {
|
||||
return nil
|
||||
}
|
||||
return obsZypperChannel.moves(repoPathExists)
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) ApplyRepoMoves(ctx context.Context, moves []RepoMove, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if err := o.enableOBSRepos(ctx, repoMoveAdds(moves), sudoPassword, progressChan); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
command := "zypper removerepo " + obsZypperAlias(move.Old)
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.30,
|
||||
Step: fmt.Sprintf("Removing OBS repo %s...", move.Old),
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo " + command,
|
||||
}
|
||||
if err := o.runWithProgress(privescExec(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.30, 0.35); err != nil {
|
||||
return fmt.Errorf("failed to remove OBS repo %s: %w", move.Old, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) RepoMoveCommands(moves []RepoMove) []string {
|
||||
osInfo, err := GetOSInfo()
|
||||
if err != nil {
|
||||
osInfo = &OSInfo{}
|
||||
}
|
||||
return opensuseRepoMoveCommands(moves, opensuseOBSDist(osInfo))
|
||||
}
|
||||
|
||||
func opensuseRepoMoveCommands(moves []RepoMove, dist string) []string {
|
||||
var commands []string
|
||||
adds := repoMoveAdds(moves)
|
||||
for _, add := range adds {
|
||||
commands = append(commands, "sudo zypper addrepo -f "+obsZypperRepoURL(add.RepoURL, dist))
|
||||
}
|
||||
if len(adds) > 0 {
|
||||
commands = append(commands, "sudo zypper --gpg-auto-import-keys refresh")
|
||||
}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
commands = append(commands, "sudo zypper removerepo "+obsZypperAlias(move.Old))
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func opensuseOBSDist(osInfo *OSInfo) string {
|
||||
switch osInfo.Distribution.ID {
|
||||
case "opensuse-leap":
|
||||
return "openSUSE_Leap_" + osInfo.VersionID
|
||||
case "opensuse-slowroll":
|
||||
return "openSUSE_Slowroll"
|
||||
}
|
||||
return "openSUSE_Tumbleweed"
|
||||
}
|
||||
|
||||
func obsZypperRepoURL(repo, dist string) string {
|
||||
return obsBaseURL(repo, dist) + "/" + repo + ".repo"
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
|
||||
gitInstalled := o.packageInstalled("dms-greeter-git")
|
||||
dep := o.detectOptionalPackage("dms-greeter", "Dux Shell greetd greeter", gitInstalled || o.packageInstalled("dms-greeter"))
|
||||
@@ -154,11 +218,11 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
// DMS packages from OBS
|
||||
"dms (Dux Shell)": o.getDmsMapping(variants["dms (Dux Shell)"]),
|
||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeOBS, "home:AvengeMedia:danklinux"),
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypeOBS, "home:AvengeMedia:danklinux"),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeOBS, obsDeps),
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: obsDeps},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: obsDeps},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeOBS, RepoURL: obsDeps},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypeOBS, obsDeps),
|
||||
}
|
||||
|
||||
switch wm {
|
||||
@@ -179,28 +243,28 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
||||
|
||||
func (o *OpenSUSEDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms-git"}
|
||||
return PackageMapping{Name: "dms-git", Repository: RepoTypeOBS, RepoURL: obsDMSGit}
|
||||
}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:dms"}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypeOBS, RepoURL: obsDMS}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceQuickshellGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "niri-git", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
||||
}
|
||||
|
||||
func (o *OpenSUSEDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}
|
||||
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypeOBS, RepoURL: obsDeps}
|
||||
}
|
||||
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypeSystem}
|
||||
}
|
||||
@@ -490,26 +554,16 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
||||
return fmt.Errorf("failed to get OS info: %w", err)
|
||||
}
|
||||
|
||||
obsDistroVersion := "openSUSE_Tumbleweed"
|
||||
switch osInfo.Distribution.ID {
|
||||
case "opensuse-leap":
|
||||
obsDistroVersion = fmt.Sprintf("openSUSE_Leap_%s", osInfo.VersionID)
|
||||
case "opensuse-slowroll":
|
||||
obsDistroVersion = "openSUSE_Slowroll"
|
||||
}
|
||||
obsDistroVersion := opensuseOBSDist(osInfo)
|
||||
|
||||
for _, pkg := range obsPkgs {
|
||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||
|
||||
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s/%s.repo",
|
||||
repoPath, obsDistroVersion, pkg.RepoURL)
|
||||
repoURL := obsZypperRepoURL(pkg.RepoURL, obsDistroVersion)
|
||||
|
||||
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
||||
if checkCmd.Run() == nil {
|
||||
// zypper stores the repo under the alias from the .repo section header, home_<owner>_<project>.
|
||||
if repoPathExists(obsZypperRepoFile(pkg.RepoURL)) {
|
||||
o.log(fmt.Sprintf("OBS repo %s already exists, skipping", pkg.RepoURL))
|
||||
enabledRepos[pkg.RepoURL] = true
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/privesc"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/site"
|
||||
)
|
||||
|
||||
const (
|
||||
coprOwner = "duxlabs"
|
||||
obsHome = "home:duxlabs"
|
||||
ppaOwner = "duxlabs"
|
||||
|
||||
legacyCoprOwner = "avengemedia"
|
||||
legacyObsHome = "home:AvengeMedia"
|
||||
legacyPpaOwner = "avengemedia"
|
||||
|
||||
projectDMS = "dms"
|
||||
projectDMSGit = "dms-git"
|
||||
projectDeps = "dux-repo"
|
||||
legacyProjectDeps = "danklinux"
|
||||
// The 2.0 docs told users to enable duxlabs/dux before the deps slug became dux-repo.
|
||||
retiredProjectDeps = "dux"
|
||||
|
||||
voidHost = site.VoidRepo
|
||||
legacyVoidHost = "https://void.danklinux.com"
|
||||
voidConfGlob = "/etc/xbps.d/*.conf"
|
||||
)
|
||||
|
||||
// The duxlabs OBS projects are not published yet, so Debian and openSUSE stay on the legacy ones.
|
||||
const obsMigrationReady = false
|
||||
|
||||
var dmsProjects = []string{projectDMS, projectDMSGit, projectDeps}
|
||||
|
||||
func legacyProject(project string) string {
|
||||
if project == projectDeps {
|
||||
return legacyProjectDeps
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
func coprRepo(project string) string { return coprOwner + "/" + project }
|
||||
func legacyCoprRepo(project string) string { return legacyCoprOwner + "/" + project }
|
||||
func ppaRepo(project string) string { return "ppa:" + ppaOwner + "/" + project }
|
||||
func legacyPpaRepo(project string) string { return "ppa:" + legacyPpaOwner + "/" + project }
|
||||
func legacyObsRepo(project string) string { return legacyObsHome + ":" + project }
|
||||
func newObsRepo(project string) string { return obsHome + ":" + project }
|
||||
|
||||
// obsRepo is what the mappings install from; the move tables use newObsRepo regardless of the gate.
|
||||
func obsRepo(project string) string {
|
||||
if !obsMigrationReady {
|
||||
return legacyObsRepo(legacyProject(project))
|
||||
}
|
||||
return newObsRepo(project)
|
||||
}
|
||||
|
||||
var (
|
||||
coprDMS, coprDMSGit, coprDeps = coprRepo(projectDMS), coprRepo(projectDMSGit), coprRepo(projectDeps)
|
||||
obsDMS, obsDMSGit, obsDeps = obsRepo(projectDMS), obsRepo(projectDMSGit), obsRepo(projectDeps)
|
||||
ppaDMS, ppaDMSGit, ppaDeps = ppaRepo(projectDMS), ppaRepo(projectDMSGit), ppaRepo(projectDeps)
|
||||
)
|
||||
|
||||
// Each must match what the package manager itself derives from the RepoURL.
|
||||
func coprRepoID(repo string) string {
|
||||
return "copr:copr.fedorainfracloud.org:" + strings.ReplaceAll(repo, "/", ":")
|
||||
}
|
||||
func coprRepoFile(repo string) string { return "/etc/yum.repos.d/_" + coprRepoID(repo) + ".repo" }
|
||||
func obsAptName(repo string) string { return strings.ReplaceAll(repo, ":", "-") }
|
||||
func obsAptListFile(repo string) string {
|
||||
return "/etc/apt/sources.list.d/" + obsAptName(repo) + ".list"
|
||||
}
|
||||
func obsAptKeyring(repo string) string { return "/etc/apt/keyrings/" + obsAptName(repo) + ".gpg" }
|
||||
func obsZypperAlias(repo string) string { return strings.ReplaceAll(repo, ":", "_") }
|
||||
func obsZypperRepoFile(repo string) string {
|
||||
return "/etc/zypp/repos.d/" + obsZypperAlias(repo) + ".repo"
|
||||
}
|
||||
func obsBaseURL(repo, dist string) string {
|
||||
return "https://download.opensuse.org/repositories/" + strings.ReplaceAll(repo, ":", ":/") + "/" + dist
|
||||
}
|
||||
|
||||
// add-apt-repository names the file <owner>-ubuntu-<project>-<series>.sources (24.04+) or .list.
|
||||
func ppaSourcesGlobs(repo string) []string {
|
||||
owner, project, _ := strings.Cut(strings.TrimPrefix(repo, "ppa:"), "/")
|
||||
stem := "/etc/apt/sources.list.d/" + owner + "-ubuntu-" + project + "-*"
|
||||
return []string{stem + ".sources", stem + ".list"}
|
||||
}
|
||||
|
||||
// A current COPR mapping may still be installed from its legacy owner.
|
||||
func coprAliases(repo string) []string {
|
||||
owner, project, _ := strings.Cut(repo, "/")
|
||||
if owner != coprOwner {
|
||||
return []string{repo}
|
||||
}
|
||||
return []string{repo, legacyCoprRepo(legacyProject(project))}
|
||||
}
|
||||
|
||||
func coprRepoIDs(repo string) []string {
|
||||
var ids []string
|
||||
for _, alias := range coprAliases(repo) {
|
||||
ids = append(ids, coprRepoID(alias))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func coprFromRepoMatches(fromRepo, repo string) bool {
|
||||
return slices.Contains(coprRepoIDs(repo), strings.TrimSpace(fromRepo))
|
||||
}
|
||||
|
||||
// Test seams.
|
||||
var (
|
||||
repoGlob = func(pattern string) []string {
|
||||
matches, _ := filepath.Glob(pattern)
|
||||
return matches
|
||||
}
|
||||
repoPathExists = func(pattern string) bool { return len(repoGlob(pattern)) > 0 }
|
||||
repoReadFile = os.ReadFile
|
||||
privescExec = privesc.ExecCommand
|
||||
)
|
||||
|
||||
// The series is one word, so the retired dux-* glob must not count the dux-repo-* sibling.
|
||||
func ppaSourcesExists(pattern string) bool {
|
||||
return slices.ContainsFunc(repoGlob(pattern), func(path string) bool { return ppaSeriesMatch(pattern, path) })
|
||||
}
|
||||
|
||||
func ppaSeriesMatch(pattern, path string) bool {
|
||||
stem, ext, _ := strings.Cut(pattern, "*")
|
||||
series := strings.TrimSuffix(strings.TrimPrefix(path, stem), ext)
|
||||
return series != "" && !strings.Contains(series, "-")
|
||||
}
|
||||
|
||||
type RepoMove struct {
|
||||
Kind RepositoryType
|
||||
Old, New string // RepoURL form, e.g. "avengemedia/danklinux" → "duxlabs/dux-repo"
|
||||
OldPath, NewPath string // file (or glob) whose presence means the repo is enabled
|
||||
OldPresent, NewPresent bool
|
||||
}
|
||||
|
||||
type RepoMover interface {
|
||||
RepoMoves() []RepoMove
|
||||
ApplyRepoMoves(ctx context.Context, moves []RepoMove, sudoPassword string, progressChan chan<- InstallProgressMsg) error
|
||||
RepoMoveCommands(moves []RepoMove) []string
|
||||
}
|
||||
|
||||
// For image-based hosts, where the package manager cannot run the moves.
|
||||
type ImageRepoMover interface {
|
||||
ImageRepoMoveCommands(moves []RepoMove) []string
|
||||
}
|
||||
|
||||
func PlanRepoMoves(d Distribution) ([]RepoMove, bool) {
|
||||
mover, ok := d.(RepoMover)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
moves := mover.RepoMoves()
|
||||
return moves, len(moves) > 0
|
||||
}
|
||||
|
||||
type repoChannel struct {
|
||||
kind RepositoryType
|
||||
repo, legacyRepo func(project string) string
|
||||
paths func(repo string) []string
|
||||
retiredDeps bool // the channel also published the deps project under retiredProjectDeps
|
||||
}
|
||||
|
||||
func (c repoChannel) legacyRepos(project string) []string {
|
||||
repos := []string{c.legacyRepo(legacyProject(project))}
|
||||
if project == projectDeps && c.retiredDeps {
|
||||
repos = append(repos, c.repo(retiredProjectDeps))
|
||||
}
|
||||
return repos
|
||||
}
|
||||
|
||||
func (c repoChannel) moves(exists func(string) bool) []RepoMove {
|
||||
var moves []RepoMove
|
||||
for _, project := range dmsProjects {
|
||||
for _, old := range c.legacyRepos(project) {
|
||||
move := RepoMove{Kind: c.kind, Old: old, New: c.repo(project)}
|
||||
move.OldPath, move.OldPresent = firstExisting(exists, c.paths(move.Old))
|
||||
move.NewPath, move.NewPresent = firstExisting(exists, c.paths(move.New))
|
||||
moves = append(moves, move)
|
||||
}
|
||||
}
|
||||
return moves
|
||||
}
|
||||
|
||||
func firstExisting(exists func(string) bool, paths []string) (string, bool) {
|
||||
for _, path := range paths {
|
||||
if exists(path) {
|
||||
return path, true
|
||||
}
|
||||
}
|
||||
return paths[0], false
|
||||
}
|
||||
|
||||
var (
|
||||
coprChannel = repoChannel{RepoTypeCOPR, coprRepo, legacyCoprRepo, func(r string) []string { return []string{coprRepoFile(r)} }, true}
|
||||
obsAptChannel = repoChannel{RepoTypeOBS, newObsRepo, legacyObsRepo, func(r string) []string { return []string{obsAptListFile(r)} }, false}
|
||||
obsZypperChannel = repoChannel{RepoTypeOBS, newObsRepo, legacyObsRepo, func(r string) []string { return []string{obsZypperRepoFile(r)} }, false}
|
||||
ppaChannel = repoChannel{RepoTypePPA, ppaRepo, legacyPpaRepo, ppaSourcesGlobs, true}
|
||||
)
|
||||
|
||||
func (m RepoMove) needsAdd() bool { return m.OldPresent && !m.NewPresent }
|
||||
func (m RepoMove) needsRemove() bool { return m.OldPresent }
|
||||
|
||||
// The enable*Repos helpers only read RepoURL; two legacy rows can share a target.
|
||||
func repoMoveAdds(moves []RepoMove) []PackageMapping {
|
||||
var adds []PackageMapping
|
||||
seen := map[string]bool{}
|
||||
for _, move := range moves {
|
||||
if !move.needsAdd() || seen[move.New] {
|
||||
continue
|
||||
}
|
||||
seen[move.New] = true
|
||||
adds = append(adds, PackageMapping{Repository: move.Kind, RepoURL: move.New})
|
||||
}
|
||||
return adds
|
||||
}
|
||||
|
||||
func repoMoveRemoves(moves []RepoMove) []RepoMove {
|
||||
var removes []RepoMove
|
||||
for _, move := range moves {
|
||||
if move.needsRemove() {
|
||||
removes = append(removes, move)
|
||||
}
|
||||
}
|
||||
return removes
|
||||
}
|
||||
|
||||
func (b *BaseDistribution) aptUpdate(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.36,
|
||||
Step: "Updating package lists...",
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo apt-get update",
|
||||
}
|
||||
if err := b.runWithProgress(privescExec(ctx, sudoPassword, "apt-get update"), progressChan, PhaseSystemPackages, 0.36, 0.40); err != nil {
|
||||
return fmt.Errorf("failed to update package lists: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LegacyRepoPaths(moves []RepoMove) []string {
|
||||
paths := []string{}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
if !slices.Contains(paths, move.OldPath) {
|
||||
paths = append(paths, move.OldPath)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package distros
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/deps"
|
||||
)
|
||||
|
||||
func TestOBSZypperAlias(t *testing.T) {
|
||||
if got := obsZypperAlias("home:AvengeMedia:dms"); got != "home_AvengeMedia_dms" {
|
||||
t.Fatalf("alias = %q, want the section header spelling home_AvengeMedia_dms", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCOPRFromRepoMatches(t *testing.T) {
|
||||
cases := []struct {
|
||||
fromRepo, repo string
|
||||
want bool
|
||||
}{
|
||||
{"copr:copr.fedorainfracloud.org:duxlabs:dms-git\n", coprDMSGit, true},
|
||||
{"copr:copr.fedorainfracloud.org:avengemedia:dms-git", coprDMSGit, true},
|
||||
{"copr:copr.fedorainfracloud.org:avengemedia:danklinux", coprDeps, true},
|
||||
{"copr:copr.fedorainfracloud.org:duxlabs:dms", coprDMSGit, false},
|
||||
{"copr:copr.fedorainfracloud.org:avengemedia:dms", coprDMSGit, false},
|
||||
{"copr:copr.fedorainfracloud.org:duxlabs:dms-git", coprDMS, false},
|
||||
{"copr:copr.fedorainfracloud.org:yalter:niri-git", "yalter/niri-git", true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := coprFromRepoMatches(tc.fromRepo, tc.repo); got != tc.want {
|
||||
t.Errorf("coprFromRepoMatches(%q, %q) = %v, want %v", tc.fromRepo, tc.repo, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func existsIn(paths ...string) func(string) bool {
|
||||
return func(p string) bool { return slices.Contains(paths, p) }
|
||||
}
|
||||
|
||||
func TestPlanRepoMoves(t *testing.T) {
|
||||
legacyDeps := coprRepoFile("avengemedia/danklinux")
|
||||
retiredDeps := coprRepoFile("duxlabs/dux")
|
||||
newDeps := coprRepoFile("duxlabs/dux-repo")
|
||||
cases := []struct {
|
||||
name string
|
||||
exists func(string) bool
|
||||
wantAdds []string
|
||||
wantRemoves []string
|
||||
wantLegacyIdx []string
|
||||
}{
|
||||
{"legacy only", existsIn(legacyDeps), []string{"duxlabs/dux-repo"}, []string{"avengemedia/danklinux"}, []string{legacyDeps}},
|
||||
{"both present", existsIn(legacyDeps, newDeps), nil, []string{"avengemedia/danklinux"}, []string{legacyDeps}},
|
||||
{"new only", existsIn(newDeps), nil, nil, nil},
|
||||
{"none", existsIn(), nil, nil, nil},
|
||||
{"retired dux slug", existsIn(retiredDeps), []string{"duxlabs/dux-repo"}, []string{"duxlabs/dux"}, []string{retiredDeps}},
|
||||
{"both legacy deps", existsIn(legacyDeps, retiredDeps), []string{"duxlabs/dux-repo"}, []string{"avengemedia/danklinux", "duxlabs/dux"}, []string{legacyDeps, retiredDeps}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
moves := coprChannel.moves(tc.exists)
|
||||
if len(moves) != 4 {
|
||||
t.Fatalf("got %d rows, want dms, dms-git and the two legacy deps rows", len(moves))
|
||||
}
|
||||
var adds, removes []string
|
||||
for _, add := range repoMoveAdds(moves) {
|
||||
adds = append(adds, add.RepoURL)
|
||||
}
|
||||
for _, move := range repoMoveRemoves(moves) {
|
||||
removes = append(removes, move.Old)
|
||||
}
|
||||
if !slices.Equal(adds, tc.wantAdds) || !slices.Equal(removes, tc.wantRemoves) {
|
||||
t.Fatalf("adds %v removes %v, want %v / %v", adds, removes, tc.wantAdds, tc.wantRemoves)
|
||||
}
|
||||
legacy := LegacyRepoPaths(moves)
|
||||
if len(legacy) == 0 && len(tc.wantLegacyIdx) == 0 {
|
||||
return
|
||||
}
|
||||
if !slices.Equal(legacy, tc.wantLegacyIdx) {
|
||||
t.Fatalf("legacy paths %v, want %v", legacy, tc.wantLegacyIdx)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanRepoMovesPPAGlob(t *testing.T) {
|
||||
moves := ppaChannel.moves(func(p string) bool {
|
||||
return p == "/etc/apt/sources.list.d/avengemedia-ubuntu-dms-*.list"
|
||||
})
|
||||
legacy := LegacyRepoPaths(moves)
|
||||
if len(legacy) != 1 || legacy[0] != "/etc/apt/sources.list.d/avengemedia-ubuntu-dms-*.list" {
|
||||
t.Fatalf("legacy = %v, want the 22.04 .list glob for dms only", legacy)
|
||||
}
|
||||
}
|
||||
|
||||
// filepath.Glob over a fake listing, where dux-* also matches the dux-repo-* sibling.
|
||||
func globIn(files ...string) func(string) []string {
|
||||
return func(pattern string) []string {
|
||||
var matches []string
|
||||
for _, file := range files {
|
||||
if ok, _ := filepath.Match(pattern, file); ok {
|
||||
matches = append(matches, file)
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
}
|
||||
|
||||
func TestUbuntuRepoMovesIgnoreRetiredSiblingGlob(t *testing.T) {
|
||||
orig := repoGlob
|
||||
repoGlob = globIn(
|
||||
"/etc/apt/sources.list.d/duxlabs-ubuntu-dux-repo-resolute.sources",
|
||||
"/etc/apt/sources.list.d/avengemedia-ubuntu-danklinux-resolute.sources")
|
||||
t.Cleanup(func() { repoGlob = orig })
|
||||
|
||||
moves := NewUbuntuDistribution(Registry["ubuntu"], nil).RepoMoves()
|
||||
want := []string{"/etc/apt/sources.list.d/avengemedia-ubuntu-danklinux-*.sources"}
|
||||
if got := LegacyRepoPaths(moves); !slices.Equal(got, want) {
|
||||
t.Fatalf("legacy = %v, want %v", got, want)
|
||||
}
|
||||
if adds := repoMoveAdds(moves); len(adds) != 0 {
|
||||
t.Fatalf("planned adds %v while dux-repo is present", adds)
|
||||
}
|
||||
}
|
||||
|
||||
func fakeXBPSConfs(t *testing.T, files map[string]string) {
|
||||
t.Helper()
|
||||
origGlob, origRead := repoGlob, repoReadFile
|
||||
var paths []string
|
||||
for path := range files {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
slices.Sort(paths)
|
||||
repoGlob = globIn(paths...)
|
||||
repoReadFile = func(path string) ([]byte, error) {
|
||||
content, ok := files[path]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return []byte(content), nil
|
||||
}
|
||||
t.Cleanup(func() { repoGlob, repoReadFile = origGlob, origRead })
|
||||
}
|
||||
|
||||
func TestVoidRepoMoves(t *testing.T) {
|
||||
const (
|
||||
legacyDMS = "repository=https://void.danklinux.com/dms/current\n"
|
||||
legacyDeps = "repository=https://void.danklinux.com/danklinux/current\n"
|
||||
newDMS = "repository=https://void.duxlabs.dev/dms/current\n"
|
||||
newDeps = "repository=https://void.duxlabs.dev/dux-repo/current\n"
|
||||
)
|
||||
cases := []struct {
|
||||
name string
|
||||
files map[string]string
|
||||
wantMoves []string
|
||||
wantLegacy []string
|
||||
wantNew bool
|
||||
}{
|
||||
{
|
||||
"both legacy",
|
||||
map[string]string{"/etc/xbps.d/dms.conf": legacyDMS, "/etc/xbps.d/danklinux.conf": "# dms deps\n" + legacyDeps, "/etc/xbps.d/00-repository-main.conf": "repository=https://repo-default.voidlinux.org/current\n"},
|
||||
[]string{
|
||||
"https://void.danklinux.com/danklinux/current -> https://void.duxlabs.dev/dux-repo/current",
|
||||
"https://void.danklinux.com/dms/current -> https://void.duxlabs.dev/dms/current",
|
||||
},
|
||||
[]string{"/etc/xbps.d/danklinux.conf", "/etc/xbps.d/dms.conf"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"one migrated",
|
||||
map[string]string{"/etc/xbps.d/dms.conf": newDMS, "/etc/xbps.d/dux.conf": legacyDeps},
|
||||
[]string{"https://void.danklinux.com/danklinux/current -> https://void.duxlabs.dev/dux-repo/current"},
|
||||
[]string{"/etc/xbps.d/dux.conf"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"both lines in one file",
|
||||
map[string]string{"/etc/xbps.d/custom.conf": legacyDMS + legacyDeps},
|
||||
[]string{
|
||||
"https://void.danklinux.com/dms/current -> https://void.duxlabs.dev/dms/current",
|
||||
"https://void.danklinux.com/danklinux/current -> https://void.duxlabs.dev/dux-repo/current",
|
||||
},
|
||||
[]string{"/etc/xbps.d/custom.conf"},
|
||||
false,
|
||||
},
|
||||
{"greeter only", map[string]string{"/etc/xbps.d/dms.conf": legacyDMS}, []string{"https://void.danklinux.com/dms/current -> https://void.duxlabs.dev/dms/current"}, []string{"/etc/xbps.d/dms.conf"}, false},
|
||||
{"all migrated", map[string]string{"/etc/xbps.d/dms.conf": newDMS, "/etc/xbps.d/dux.conf": newDeps}, nil, nil, true},
|
||||
{"none", map[string]string{"/etc/xbps.d/00-repository-main.conf": "repository=https://repo-default.voidlinux.org/current\n"}, nil, nil, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeXBPSConfs(t, tc.files)
|
||||
v := NewVoidDistribution(Registry["void"], nil)
|
||||
moves, _ := PlanRepoMoves(v)
|
||||
var got []string
|
||||
newPresent := false
|
||||
for _, move := range moves {
|
||||
newPresent = newPresent || move.NewPresent
|
||||
if move.OldPresent {
|
||||
if move.NewPath != move.OldPath {
|
||||
t.Fatalf("%s must be edited in place, got NewPath %s", move.OldPath, move.NewPath)
|
||||
}
|
||||
got = append(got, move.Old+" -> "+move.New)
|
||||
}
|
||||
}
|
||||
if !slices.Equal(got, tc.wantMoves) {
|
||||
t.Fatalf("moves %q, want %q", got, tc.wantMoves)
|
||||
}
|
||||
if legacy := LegacyRepoPaths(moves); !slices.Equal(legacy, tc.wantLegacy) && len(legacy)+len(tc.wantLegacy) > 0 {
|
||||
t.Fatalf("legacy paths %v, want %v", legacy, tc.wantLegacy)
|
||||
}
|
||||
if newPresent != tc.wantNew {
|
||||
t.Fatalf("new repo present = %v, want %v", newPresent, tc.wantNew)
|
||||
}
|
||||
if commands := v.RepoMoveCommands(moves); (len(commands) == 0) != (len(tc.wantMoves) == 0) {
|
||||
t.Fatalf("commands %q for moves %q", commands, tc.wantMoves)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVoidMappingsUseNewHost(t *testing.T) {
|
||||
v := NewVoidDistribution(Registry["void"], nil)
|
||||
for _, variants := range []map[string]deps.PackageVariant{
|
||||
{"dms (Dux Shell)": deps.VariantStable, "dms-greeter": deps.VariantStable},
|
||||
{"dms (Dux Shell)": deps.VariantGit, "dms-greeter": deps.VariantGit},
|
||||
} {
|
||||
packages := v.GetPackageMappingWithVariants(deps.WindowManagerNiri, variants)
|
||||
want := map[string]string{
|
||||
"dms (Dux Shell)": "https://void.duxlabs.dev/dms/current",
|
||||
"dms-greeter": "https://void.duxlabs.dev/dux-repo/current",
|
||||
"danksearch": "https://void.duxlabs.dev/dux-repo/current",
|
||||
"dankcalendar": "https://void.duxlabs.dev/dux-repo/current",
|
||||
}
|
||||
for dep, url := range want {
|
||||
if got := packages[dep].RepoURL; got != url {
|
||||
t.Errorf("%s (%s) RepoURL = %q, want %q", dep, packages[dep].Name, got, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoMoveCommands(t *testing.T) {
|
||||
withOrig := func(c repoChannel, present ...string) []RepoMove {
|
||||
return c.moves(existsIn(present...))
|
||||
}
|
||||
|
||||
t.Run("fedora", func(t *testing.T) {
|
||||
f := NewFedoraDistribution(Registry["fedora"], nil)
|
||||
moves := withOrig(coprChannel, coprRepoFile("avengemedia/dms"), coprRepoFile("avengemedia/danklinux"), coprRepoFile("duxlabs/dms"))
|
||||
want := []string{
|
||||
"sudo dnf copr enable -y duxlabs/dux-repo",
|
||||
"sudo dnf copr remove -y avengemedia/dms",
|
||||
"sudo dnf copr remove -y avengemedia/danklinux",
|
||||
}
|
||||
if got := f.RepoMoveCommands(moves); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fedora image-based", func(t *testing.T) {
|
||||
f := NewFedoraDistribution(Registry["fedora"], nil)
|
||||
moves := withOrig(coprChannel, coprRepoFile("avengemedia/danklinux"))
|
||||
want := []string{
|
||||
"sudo curl -fsSLo /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:duxlabs:dux-repo.repo https://copr.fedorainfracloud.org/coprs/duxlabs/dux-repo/repo/fedora-$(rpm -E %fedora)/duxlabs-dux-repo-fedora-$(rpm -E %fedora).repo",
|
||||
"sudo rm -f /etc/yum.repos.d/_copr:copr.fedorainfracloud.org:avengemedia:danklinux.repo",
|
||||
}
|
||||
if got := f.ImageRepoMoveCommands(moves); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fedora retired dux slug", func(t *testing.T) {
|
||||
f := NewFedoraDistribution(Registry["fedora"], nil)
|
||||
moves := withOrig(coprChannel, coprRepoFile("duxlabs/dux"))
|
||||
want := []string{
|
||||
"sudo dnf copr enable -y duxlabs/dux-repo",
|
||||
"sudo dnf copr remove -y duxlabs/dux",
|
||||
}
|
||||
if got := f.RepoMoveCommands(moves); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ubuntu", func(t *testing.T) {
|
||||
u := NewUbuntuDistribution(Registry["ubuntu"], nil)
|
||||
moves := withOrig(ppaChannel, "/etc/apt/sources.list.d/avengemedia-ubuntu-danklinux-*.sources", "/etc/apt/sources.list.d/duxlabs-ubuntu-dux-*.sources")
|
||||
want := []string{
|
||||
"sudo add-apt-repository -y ppa:duxlabs/dux-repo",
|
||||
"sudo add-apt-repository -r -y ppa:avengemedia/danklinux",
|
||||
"sudo add-apt-repository -r -y ppa:duxlabs/dux",
|
||||
"sudo apt-get update",
|
||||
}
|
||||
if got := u.RepoMoveCommands(moves); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("debian", func(t *testing.T) {
|
||||
moves := withOrig(obsAptChannel, obsAptListFile("home:AvengeMedia:dms-git"))
|
||||
want := []string{
|
||||
"curl -fsSL https://download.opensuse.org/repositories/home:/duxlabs:/dms-git/Debian_13/Release.key | gpg --dearmor | sudo tee /etc/apt/keyrings/home-duxlabs-dms-git.gpg",
|
||||
"echo 'deb [signed-by=/etc/apt/keyrings/home-duxlabs-dms-git.gpg arch=amd64] https://download.opensuse.org/repositories/home:/duxlabs:/dms-git/Debian_13/ /' | sudo tee /etc/apt/sources.list.d/home-duxlabs-dms-git.list",
|
||||
"sudo rm -f /etc/apt/sources.list.d/home-AvengeMedia-dms-git.list /etc/apt/keyrings/home-AvengeMedia-dms-git.gpg",
|
||||
"sudo apt-get update",
|
||||
}
|
||||
if got := debianRepoMoveCommands(moves, "Debian_13", "amd64"); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("void", func(t *testing.T) {
|
||||
fakeXBPSConfs(t, map[string]string{
|
||||
"/etc/xbps.d/dms.conf": "repository=https://void.danklinux.com/dms/current\n",
|
||||
"/etc/xbps.d/dux.conf": "repository=https://void.danklinux.com/danklinux/current\n",
|
||||
})
|
||||
v := NewVoidDistribution(Registry["void"], nil)
|
||||
want := []string{
|
||||
"sudo sed -i 's|repository=https://void.danklinux.com/dms/current|repository=https://void.duxlabs.dev/dms/current|' /etc/xbps.d/dms.conf",
|
||||
"sudo sed -i 's|repository=https://void.danklinux.com/danklinux/current|repository=https://void.duxlabs.dev/dux-repo/current|' /etc/xbps.d/dux.conf",
|
||||
"sudo xbps-install -S",
|
||||
}
|
||||
if got := v.RepoMoveCommands(v.RepoMoves()); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("void greeter only", func(t *testing.T) {
|
||||
fakeXBPSConfs(t, map[string]string{"/etc/xbps.d/dms.conf": "repository=https://void.danklinux.com/dms/current\n"})
|
||||
v := NewVoidDistribution(Registry["void"], nil)
|
||||
want := []string{
|
||||
"sudo sed -i 's|repository=https://void.danklinux.com/dms/current|repository=https://void.duxlabs.dev/dms/current|' /etc/xbps.d/dms.conf",
|
||||
"sudo sh -c 'echo repository=https://void.duxlabs.dev/dux-repo/current >> /etc/xbps.d/dux.conf'",
|
||||
"sudo xbps-install -S",
|
||||
}
|
||||
if got := v.RepoMoveCommands(v.RepoMoves()); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
// Either spelling of the deps line already serves the greeter, so no second dux.conf entry.
|
||||
for name, deps := range map[string]string{
|
||||
"legacy": "repository=https://void.danklinux.com/danklinux/current\n",
|
||||
"new": "repository=https://void.duxlabs.dev/dux-repo/current\n",
|
||||
} {
|
||||
t.Run("void deps already "+name, func(t *testing.T) {
|
||||
fakeXBPSConfs(t, map[string]string{
|
||||
"/etc/xbps.d/dms.conf": "repository=https://void.danklinux.com/dms/current\n",
|
||||
"/etc/xbps.d/danklinux.conf": deps,
|
||||
})
|
||||
v := NewVoidDistribution(Registry["void"], nil)
|
||||
for _, command := range v.RepoMoveCommands(v.RepoMoves()) {
|
||||
if strings.Contains(command, "dux.conf") {
|
||||
t.Fatalf("added a duplicate deps repo: %q", command)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("opensuse", func(t *testing.T) {
|
||||
moves := withOrig(obsZypperChannel, obsZypperRepoFile("home:AvengeMedia:danklinux"))
|
||||
want := []string{
|
||||
"sudo zypper addrepo -f https://download.opensuse.org/repositories/home:/duxlabs:/dux-repo/openSUSE_Tumbleweed/home:duxlabs:dux-repo.repo",
|
||||
"sudo zypper --gpg-auto-import-keys refresh",
|
||||
"sudo zypper removerepo home_AvengeMedia_danklinux",
|
||||
}
|
||||
if got := opensuseRepoMoveCommands(moves, "openSUSE_Tumbleweed"); !slices.Equal(got, want) {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Until OBS publishes, movers must plan nothing even with legacy files present, or doctor and migrate nag.
|
||||
func TestOBSMigrationGate(t *testing.T) {
|
||||
if obsMigrationReady {
|
||||
t.Skip("gate open")
|
||||
}
|
||||
orig := repoPathExists
|
||||
repoPathExists = func(string) bool { return true }
|
||||
t.Cleanup(func() { repoPathExists = orig })
|
||||
|
||||
if moves := NewDebianDistribution(Registry["debian"], nil).RepoMoves(); moves != nil {
|
||||
t.Fatalf("debian planned %v while the gate is closed", moves)
|
||||
}
|
||||
if moves := NewOpenSUSEDistribution(Registry["opensuse-tumbleweed"], nil).RepoMoves(); moves != nil {
|
||||
t.Fatalf("opensuse planned %v while the gate is closed", moves)
|
||||
}
|
||||
if got := NewDebianDistribution(Registry["debian"], nil).GetPackageMapping(0)["dms-greeter"].RepoURL; got != "home:AvengeMedia:danklinux" {
|
||||
t.Fatalf("debian mapping = %q, must stay on the legacy OBS project while the gate is closed", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwapVariantsDisablesLegacyCOPR(t *testing.T) {
|
||||
var commands []string
|
||||
origExec, origExists := privescExec, repoPathExists
|
||||
privescExec = func(_ context.Context, _, command string) *exec.Cmd {
|
||||
commands = append(commands, command)
|
||||
return exec.Command("true")
|
||||
}
|
||||
repoPathExists = existsIn(coprRepoFile("avengemedia/dms"))
|
||||
t.Cleanup(func() { privescExec, repoPathExists = origExec, origExists })
|
||||
|
||||
f := NewFedoraDistribution(Registry["fedora"], nil)
|
||||
swaps := []VariantSwap{{Dep: "dms (Dux Shell)", From: f.getDmsMapping(0), To: f.getDmsMapping(1)}}
|
||||
progress := make(chan InstallProgressMsg, 100)
|
||||
if err := f.SwapVariants(context.Background(), swaps, "", progress); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
joined := strings.Join(commands, "\n")
|
||||
if !strings.Contains(joined, "dnf copr disable -y avengemedia/dms") {
|
||||
t.Fatalf("legacy COPR left enabled on an un-migrated host:\n%s", joined)
|
||||
}
|
||||
if strings.Contains(joined, "dnf copr disable -y duxlabs/dms") {
|
||||
t.Fatalf("disabled a COPR whose file does not exist:\n%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "dnf copr enable -y duxlabs/dms-git") {
|
||||
t.Fatalf("new git COPR not enabled:\n%s", joined)
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,51 @@ func (u *UbuntuDistribution) SwapVariants(ctx context.Context, swaps []VariantSw
|
||||
return u.installAPTPackages(ctx, to, sudoPassword, progressChan)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) RepoMoves() []RepoMove {
|
||||
return ppaChannel.moves(ppaSourcesExists)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) ApplyRepoMoves(ctx context.Context, moves []RepoMove, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
if adds := repoMoveAdds(moves); len(adds) > 0 {
|
||||
if err := u.enablePPARepos(ctx, adds, sudoPassword, progressChan); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
removes := repoMoveRemoves(moves)
|
||||
for _, move := range removes {
|
||||
command := "add-apt-repository -r -y " + move.Old
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.30,
|
||||
Step: fmt.Sprintf("Removing PPA %s...", move.Old),
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo " + command,
|
||||
}
|
||||
if err := u.runWithProgress(privescExec(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.30, 0.35); err != nil {
|
||||
return fmt.Errorf("failed to remove PPA %s: %w", move.Old, err)
|
||||
}
|
||||
}
|
||||
if len(removes) == 0 {
|
||||
return nil
|
||||
}
|
||||
return u.aptUpdate(ctx, sudoPassword, progressChan)
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) RepoMoveCommands(moves []RepoMove) []string {
|
||||
var commands []string
|
||||
for _, add := range repoMoveAdds(moves) {
|
||||
commands = append(commands, "sudo add-apt-repository -y "+add.RepoURL)
|
||||
}
|
||||
removes := repoMoveRemoves(moves)
|
||||
for _, move := range removes {
|
||||
commands = append(commands, "sudo add-apt-repository -r -y "+move.Old)
|
||||
}
|
||||
if len(removes) > 0 {
|
||||
commands = append(commands, "sudo apt-get update")
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||
return u.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||
}
|
||||
@@ -145,11 +190,11 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
// DMS packages from PPAs
|
||||
"dms (Dux Shell)": u.getDmsMapping(variants["dms (Dux Shell)"]),
|
||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypePPA, "ppa:avengemedia/danklinux"),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypePPA, "ppa:avengemedia/danklinux"),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypePPA, ppaDeps),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: ppaDeps},
|
||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: ppaDeps},
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypePPA, RepoURL: ppaDeps},
|
||||
"dankcalendar": variantMapping("dankcalendar", variants["dankcalendar"], RepoTypePPA, ppaDeps),
|
||||
}
|
||||
|
||||
switch wm {
|
||||
@@ -170,30 +215,30 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
||||
|
||||
func (u *UbuntuDistribution) getDmsMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "dms-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms-git"}
|
||||
return PackageMapping{Name: "dms-git", Repository: RepoTypePPA, RepoURL: ppaDMSGit}
|
||||
}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/dms"}
|
||||
return PackageMapping{Name: "dms", Repository: RepoTypePPA, RepoURL: ppaDMS}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) getQuickshellMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if forceQuickshellGit || variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypePPA, RepoURL: ppaDeps}
|
||||
}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "quickshell", Repository: RepoTypePPA, RepoURL: ppaDeps}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "niri-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "niri-git", Repository: RepoTypePPA, RepoURL: ppaDeps}
|
||||
}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "niri", Repository: RepoTypePPA, RepoURL: ppaDeps}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) getXwaylandSatelliteMapping(variant deps.PackageVariant) PackageMapping {
|
||||
if variant == deps.VariantGit {
|
||||
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "xwayland-satellite-git", Repository: RepoTypePPA, RepoURL: ppaDeps}
|
||||
}
|
||||
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}
|
||||
return PackageMapping{Name: "xwayland-satellite", Repository: RepoTypePPA, RepoURL: ppaDeps}
|
||||
}
|
||||
|
||||
func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
|
||||
@@ -15,7 +15,7 @@ type swapFake struct {
|
||||
|
||||
func (f *swapFake) GetPackageMappingWithVariants(_ deps.WindowManager, variants map[string]deps.PackageVariant) map[string]PackageMapping {
|
||||
return map[string]PackageMapping{
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeCOPR, "avengemedia/danklinux"),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeCOPR, "duxlabs/dux-repo"),
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/deps"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/privesc"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/site"
|
||||
)
|
||||
|
||||
const (
|
||||
VoidDMSRepo = site.VoidRepo + "/dms/current"
|
||||
VoidDankLinuxRepo = site.VoidRepo + "/danklinux/current"
|
||||
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
|
||||
VoidDMSRepo = voidHost + "/" + projectDMS + "/current"
|
||||
VoidDepsRepo = voidHost + "/" + projectDeps + "/current"
|
||||
VoidHyprlandRepo = "https://mirror.black-hole.dev/x86_64"
|
||||
|
||||
voidDepsConf = "/etc/xbps.d/dux.conf"
|
||||
|
||||
voidRunitSvDir = "/etc/sv"
|
||||
voidRunitServiceDir = "/var/service"
|
||||
@@ -193,6 +194,103 @@ func (v *VoidDistribution) SwapVariants(ctx context.Context, swaps []VariantSwap
|
||||
return fmt.Errorf("failed to install %s, restored %s: %w", strings.Join(to, ", "), strings.Join(from, ", "), installErr)
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) RepoMoves() []RepoMove {
|
||||
var moves []RepoMove
|
||||
legacy, hasDeps := false, false
|
||||
for _, path := range repoGlob(voidConfGlob) {
|
||||
for _, url := range xbpsRepositoryLines(path) {
|
||||
move, ok := voidRepoMove(path, url)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
legacy = legacy || move.OldPresent
|
||||
hasDeps = hasDeps || strings.HasPrefix(move.New, voidHost+"/"+projectDeps+"/")
|
||||
moves = append(moves, move)
|
||||
}
|
||||
}
|
||||
// Greeter-only hosts got dms-greeter from dms/current, which no longer carries it.
|
||||
if legacy && !hasDeps {
|
||||
moves = append(moves, RepoMove{Kind: RepoTypeXBPS, New: VoidDepsRepo, NewPath: voidDepsConf})
|
||||
}
|
||||
return moves
|
||||
}
|
||||
|
||||
// Matches on the repository line because older installs named the conf files differently.
|
||||
func xbpsRepositoryLines(path string) []string {
|
||||
content, err := repoReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var urls []string
|
||||
for line := range strings.Lines(string(content)) {
|
||||
key, value, ok := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if !ok || strings.TrimSpace(key) != "repository" {
|
||||
continue
|
||||
}
|
||||
urls = append(urls, strings.TrimSpace(value))
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func voidRepoMove(path, url string) (RepoMove, bool) {
|
||||
if strings.HasPrefix(url, voidHost+"/") {
|
||||
return RepoMove{Kind: RepoTypeXBPS, New: url, NewPath: path, NewPresent: true}, true
|
||||
}
|
||||
rest, ok := strings.CutPrefix(url, legacyVoidHost+"/")
|
||||
if !ok {
|
||||
return RepoMove{}, false
|
||||
}
|
||||
if deps, ok := strings.CutPrefix(rest, legacyProjectDeps+"/"); ok {
|
||||
rest = projectDeps + "/" + deps
|
||||
}
|
||||
return RepoMove{Kind: RepoTypeXBPS, Old: url, New: voidHost + "/" + rest, OldPath: path, NewPath: path, OldPresent: true}, true
|
||||
}
|
||||
|
||||
func voidAddsRepo(move RepoMove) bool {
|
||||
return !move.OldPresent && !move.NewPresent && move.NewPath != ""
|
||||
}
|
||||
|
||||
func voidMoveCommands(moves []RepoMove) []string {
|
||||
var commands []string
|
||||
for _, move := range moves {
|
||||
switch {
|
||||
case move.OldPresent:
|
||||
commands = append(commands, fmt.Sprintf("sed -i 's|repository=%s|repository=%s|' %s", move.Old, move.New, move.OldPath))
|
||||
case voidAddsRepo(move):
|
||||
// Appends so an unrelated file that happens to share the name is not clobbered.
|
||||
commands = append(commands, fmt.Sprintf("sh -c 'echo repository=%s >> %s'", move.New, move.NewPath))
|
||||
}
|
||||
}
|
||||
if len(commands) > 0 {
|
||||
commands = append(commands, "xbps-install -S")
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) ApplyRepoMoves(ctx context.Context, moves []RepoMove, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
for _, command := range voidMoveCommands(moves) {
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
Progress: 0.30,
|
||||
Step: "Moving XBPS repositories...",
|
||||
NeedsSudo: true,
|
||||
CommandInfo: "sudo " + command,
|
||||
}
|
||||
if err := v.runWithProgress(privescExec(ctx, sudoPassword, command), progressChan, PhaseSystemPackages, 0.30, 0.40); err != nil {
|
||||
return fmt.Errorf("failed to run %s: %w", command, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) RepoMoveCommands(moves []RepoMove) []string {
|
||||
var commands []string
|
||||
for _, command := range voidMoveCommands(moves) {
|
||||
commands = append(commands, "sudo "+command)
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func (v *VoidDistribution) packageVersion(pkg string) string {
|
||||
output, err := exec.Command("xbps-query", "-p", "pkgver", pkg).Output()
|
||||
if err != nil {
|
||||
@@ -220,9 +318,9 @@ func (v *VoidDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
||||
"quickshell": {Name: "quickshell", Repository: RepoTypeSystem},
|
||||
"matugen": {Name: "matugen", Repository: RepoTypeSystem},
|
||||
"dms (Dux Shell)": v.getDmsMapping(variants["dms (Dux Shell)"]),
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeXBPS, VoidDMSRepo),
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||
"dankcalendar": {Name: "dankcalendar", Repository: RepoTypeXBPS, RepoURL: VoidDankLinuxRepo},
|
||||
"dms-greeter": variantMapping("dms-greeter", variants["dms-greeter"], RepoTypeXBPS, VoidDepsRepo),
|
||||
"danksearch": {Name: "danksearch", Repository: RepoTypeXBPS, RepoURL: VoidDepsRepo},
|
||||
"dankcalendar": {Name: "dankcalendar", Repository: RepoTypeXBPS, RepoURL: VoidDepsRepo},
|
||||
}
|
||||
|
||||
switch wm {
|
||||
@@ -521,8 +619,8 @@ func (v *VoidDistribution) xbpsRepoName(repoURL string) string {
|
||||
switch repoURL {
|
||||
case VoidDMSRepo:
|
||||
return "dms"
|
||||
case VoidDankLinuxRepo:
|
||||
return "danklinux"
|
||||
case VoidDepsRepo:
|
||||
return "dux"
|
||||
case VoidHyprlandRepo:
|
||||
return "hyprland"
|
||||
default:
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/duxlabs-dev/dux-go/ipc"
|
||||
"github.com/duxlabs-dev/dux-go/ipc/params"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/distros"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/server/models"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,8 @@ func HandleRequest(ctx context.Context, conn *ipc.ConnWriter, req ipc.Request, m
|
||||
case "sysupdate.releases":
|
||||
force := params.BoolOpt(req.Params, "force", false)
|
||||
conn.Respond(req.ID, m.Releases(force))
|
||||
case "sysupdate.repoStatus":
|
||||
conn.Respond(req.ID, hostRepoStatus())
|
||||
case "sysupdate.setInterval":
|
||||
seconds, err := params.Int(req.Params, "seconds")
|
||||
if err != nil {
|
||||
@@ -62,6 +65,39 @@ func handleUpgrade(conn *ipc.ConnWriter, req ipc.Request, m *Manager) {
|
||||
conn.Respond(req.ID, m.GetState())
|
||||
}
|
||||
|
||||
type RepoStatus struct {
|
||||
Legacy []string `json:"legacy"`
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
const repoMigrateCommand = "dms migrate dux"
|
||||
|
||||
// Per call and file stats only: the shell asks once per connect.
|
||||
func hostRepoStatus() RepoStatus {
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err != nil {
|
||||
return repoStatus(nil)
|
||||
}
|
||||
distro, err := distros.NewDistribution(osInfo.Distribution.ID, nil)
|
||||
if err != nil {
|
||||
return repoStatus(nil)
|
||||
}
|
||||
return repoStatus(distro)
|
||||
}
|
||||
|
||||
func repoStatus(distro distros.Distribution) RepoStatus {
|
||||
status := RepoStatus{Legacy: []string{}, Command: repoMigrateCommand}
|
||||
if distro == nil {
|
||||
return status
|
||||
}
|
||||
moves, ok := distros.PlanRepoMoves(distro)
|
||||
if !ok {
|
||||
return status
|
||||
}
|
||||
status.Legacy = distros.LegacyRepoPaths(moves)
|
||||
return status
|
||||
}
|
||||
|
||||
func stringSliceOpt(p map[string]any, key string) []string {
|
||||
val, ok := params.Any(p, key)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package sysupdate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/duxlabs-dev/dux-go/ipc"
|
||||
"github.com/duxlabs-dev/dux-shell/core/internal/distros"
|
||||
)
|
||||
|
||||
type repoMoverFake struct {
|
||||
distros.Distribution
|
||||
moves []distros.RepoMove
|
||||
}
|
||||
|
||||
func (f *repoMoverFake) RepoMoves() []distros.RepoMove { return f.moves }
|
||||
func (f *repoMoverFake) ApplyRepoMoves(context.Context, []distros.RepoMove, string, chan<- distros.InstallProgressMsg) error {
|
||||
return nil
|
||||
}
|
||||
func (f *repoMoverFake) RepoMoveCommands([]distros.RepoMove) []string { return nil }
|
||||
|
||||
type plainDistroFake struct{ distros.Distribution }
|
||||
|
||||
func TestRepoStatus(t *testing.T) {
|
||||
const legacyPath = "/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:avengemedia:danklinux.repo"
|
||||
mover := &repoMoverFake{moves: []distros.RepoMove{
|
||||
{Kind: distros.RepoTypeCOPR, Old: "avengemedia/danklinux", New: "duxlabs/dux-repo", OldPath: legacyPath, OldPresent: true},
|
||||
{Kind: distros.RepoTypeCOPR, Old: "avengemedia/dms", New: "duxlabs/dms", NewPresent: true},
|
||||
}}
|
||||
got, _ := json.Marshal(repoStatus(mover))
|
||||
want := `{"legacy":["` + legacyPath + `"],"command":"dms migrate dux"}`
|
||||
if string(got) != want {
|
||||
t.Fatalf("got %s, want %s", got, want)
|
||||
}
|
||||
|
||||
for name, distro := range map[string]distros.Distribution{"no mover": &plainDistroFake{}, "nil": nil} {
|
||||
got, _ := json.Marshal(repoStatus(distro))
|
||||
if string(got) != `{"legacy":[],"command":"dms migrate dux"}` {
|
||||
t.Fatalf("%s: got %s, want an empty list, never null", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The route needs no manager: a shell on a host without a mover must still get an empty list.
|
||||
func TestRepoStatusRoute(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer client.Close()
|
||||
go func() {
|
||||
HandleRequest(context.Background(), ipc.NewConnWriter(server), ipc.Request{ID: 7, Method: "sysupdate.repoStatus"}, nil)
|
||||
server.Close()
|
||||
}()
|
||||
var resp ipc.Response[RepoStatus]
|
||||
if err := json.NewDecoder(client).Decode(&resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.ID != 7 || resp.Error != "" || resp.Result == nil || resp.Result.Command != "dms migrate dux" || resp.Result.Legacy == nil {
|
||||
t.Fatalf("got %+v", resp)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,6 @@ const (
|
||||
Domain = "duxlabs.dev"
|
||||
Web = "https://" + Domain
|
||||
Docs = Web + "/docs"
|
||||
API = "https://api.danklinux.com"
|
||||
VoidRepo = "https://void.danklinux.com"
|
||||
API = "https://api.duxlabs.dev"
|
||||
VoidRepo = "https://void.duxlabs.dev"
|
||||
)
|
||||
@@ -66,7 +66,7 @@ case "%{_arch}" in
|
||||
esac
|
||||
|
||||
# Download dms-cli for target architecture
|
||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/duxlabs-dev/dux-shell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/duxlabs-dev/dux-shell/releases/download/v%{version}/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ PACKAGE="${1:-dms}"
|
||||
VERSION="${2:-}"
|
||||
RELEASE="${3:-1}"
|
||||
|
||||
COPR_OWNER="${COPR_OWNER:-duxlabs}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
# Determine Copr project based on package
|
||||
if [ "$PACKAGE" = "dms" ]; then
|
||||
COPR_PROJECT="avengemedia/dms"
|
||||
COPR_PROJECT="${COPR_OWNER}/dms"
|
||||
else
|
||||
echo "❌ Unknown package: $PACKAGE"
|
||||
echo "Supported packages: dms"
|
||||
@@ -25,7 +26,7 @@ fi
|
||||
# Get version from latest release if not provided
|
||||
if [ -z "$VERSION" ]; then
|
||||
echo "📦 Determining latest version..."
|
||||
VERSION=$(curl -s https://api.github.com/repos/duxlabs-dev/dux-shell/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
VERSION=$(curl -sL https://api.github.com/repos/duxlabs-dev/dux-shell/releases/latest | jq -r '.tag_name' | sed 's/^v//')
|
||||
if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
|
||||
echo "❌ Failed to determine version. Please specify manually."
|
||||
exit 1
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# ./distro/scripts/obs-status.sh # Check all packages
|
||||
# ./distro/scripts/obs-status.sh dms # Check specific package
|
||||
|
||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||
OBS_BASE_PROJECT="${OBS_BASE_PROJECT:-home:AvengeMedia}"
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
|
||||
ALL_PACKAGES=(dms dms-git)
|
||||
|
||||
@@ -66,7 +66,7 @@ if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
OBS_BASE_PROJECT="home:AvengeMedia"
|
||||
OBS_BASE_PROJECT="${OBS_BASE_PROJECT:-home:AvengeMedia}"
|
||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
||||
AVAILABLE_PACKAGES=(dms dms-git)
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
PPA_OWNER="${PPA_OWNER:-duxlabs}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
@@ -152,7 +154,7 @@ check_ppa_version_exists() {
|
||||
local DISTRO_SERIES="${5:-}"
|
||||
|
||||
# Query Launchpad API (optionally scoped to one Ubuntu series so the same version can ship to resolute and stonking)
|
||||
local API_URL="https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published"
|
||||
local API_URL="https://api.launchpad.net/1.0/~${PPA_OWNER}/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published"
|
||||
if [[ -n "$DISTRO_SERIES" ]]; then
|
||||
API_URL+="&distro_series=https://api.launchpad.net/1.0/ubuntu/${DISTRO_SERIES}"
|
||||
fi
|
||||
@@ -595,7 +597,8 @@ else
|
||||
fi
|
||||
|
||||
# -d skips dependency checking (we're building on Fedora, not Ubuntu)
|
||||
if yes | DEBIAN_FRONTEND=noninteractive debuild -S $DEBUILD_SOURCE_FLAG -d; then
|
||||
# -k: debsign otherwise picks the key from the changelog maintainer, which need not match the CI key
|
||||
if yes | DEBIAN_FRONTEND=noninteractive debuild -S $DEBUILD_SOURCE_FLAG -d ${DEBSIGN_KEYID:+-k"$DEBSIGN_KEYID"}; then
|
||||
echo
|
||||
success "Source package built successfully!"
|
||||
|
||||
@@ -617,10 +620,10 @@ if yes | DEBIAN_FRONTEND=noninteractive debuild -S $DEBUILD_SOURCE_FLAG -d; then
|
||||
echo " ls -lh ${SOURCE_NAME}_${CHANGELOG_VERSION}*"
|
||||
echo
|
||||
echo " 2. Upload to PPA (stable):"
|
||||
echo " dput ppa:avengemedia/dms ${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes"
|
||||
echo " dput ppa:${PPA_OWNER}/dms ${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes"
|
||||
echo
|
||||
echo " 3. Or upload to PPA (nightly):"
|
||||
echo " dput ppa:avengemedia/dms-git ${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes"
|
||||
echo " dput ppa:${PPA_OWNER}/dms-git ${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes"
|
||||
echo
|
||||
echo " 4. Or use the upload script:"
|
||||
echo " ./upload-ppa.sh $PACKAGE_PARENT/${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes dms"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# ./distro/scripts/ppa-status.sh dms # Check dms package
|
||||
# ./distro/scripts/ppa-status.sh all dms-git # Check all packages in dms-git PPA
|
||||
|
||||
PPA_OWNER="avengemedia"
|
||||
PPA_OWNER="${PPA_OWNER:-duxlabs}"
|
||||
LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
||||
# Supported Ubuntu series for PPA builds (26.04 LTS resolute + 26.10 stonking)
|
||||
DISTRO_SERIES_LIST=(resolute stonking)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PPA_OWNER="avengemedia"
|
||||
PPA_OWNER="${PPA_OWNER:-duxlabs}"
|
||||
LAUNCHPAD_API="https://api.launchpad.net/1.0"
|
||||
SERIES_LIST=(resolute stonking)
|
||||
PACKAGE_FILTER="dms-git"
|
||||
|
||||
@@ -28,6 +28,7 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
AVAILABLE_PACKAGES=(dms dms-git)
|
||||
export PPA_OWNER="${PPA_OWNER:-duxlabs}"
|
||||
|
||||
KEEP_BUILDS=false
|
||||
REBUILD_RELEASE=""
|
||||
@@ -217,16 +218,15 @@ PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
|
||||
PARENT_DIR=$(dirname "$PACKAGE_DIR")
|
||||
|
||||
setup_launchpad_sftp() {
|
||||
if [[ -z "${LAUNCHPAD_SSH_PRIVATE_KEY:-}" ]]; then
|
||||
error "LAUNCHPAD_SSH_PRIVATE_KEY is required for CI SFTP uploads."
|
||||
error "Add a GitHub Actions secret containing a private SSH key whose public key is registered in Launchpad."
|
||||
error "Optional: set LAUNCHPAD_SSH_LOGIN if the Launchpad login is not 'avengemedia'."
|
||||
if [[ -z "${LAUNCHPAD_SSH_PRIVATE_KEY:-}" || -z "${LAUNCHPAD_SSH_LOGIN:-}" ]]; then
|
||||
error "LAUNCHPAD_SSH_PRIVATE_KEY and LAUNCHPAD_SSH_LOGIN are required for CI SFTP uploads."
|
||||
error "The key's public half must be registered on the Launchpad account named by LAUNCHPAD_SSH_LOGIN."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local ssh_dir="$HOME/.ssh"
|
||||
local key_file="$ssh_dir/launchpad_ppa"
|
||||
local login="${LAUNCHPAD_SSH_LOGIN:-avengemedia}"
|
||||
local login="$LAUNCHPAD_SSH_LOGIN"
|
||||
local strict_host_key_checking="yes"
|
||||
|
||||
mkdir -p "$ssh_dir"
|
||||
@@ -296,7 +296,7 @@ UBUNTU_SERIES="${SERIES_LIST[0]}"
|
||||
|
||||
info "Building and uploading: $PACKAGE_NAME"
|
||||
info "Package directory: $PACKAGE_DIR"
|
||||
info "PPA: ppa:avengemedia/$PPA_NAME"
|
||||
info "PPA: ppa:${PPA_OWNER}/$PPA_NAME"
|
||||
info "Ubuntu series: $UBUNTU_SERIES"
|
||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||
info "Rebuild release number: ppa$REBUILD_RELEASE"
|
||||
@@ -369,16 +369,16 @@ if [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
|
||||
setup_launchpad_sftp
|
||||
DPUT_CONFIG=$(mktemp)
|
||||
cat >"$DPUT_CONFIG" <<EOF
|
||||
[avengemedia-${PPA_NAME}]
|
||||
[${PPA_OWNER}-${PPA_NAME}]
|
||||
fqdn = ppa.launchpad.net
|
||||
method = sftp
|
||||
incoming = ~avengemedia/ubuntu/${PPA_NAME}/
|
||||
login = ${LAUNCHPAD_SSH_LOGIN:-avengemedia}
|
||||
incoming = ~${PPA_OWNER}/ubuntu/${PPA_NAME}/
|
||||
login = ${LAUNCHPAD_SSH_LOGIN}
|
||||
allow_unsigned_uploads = 0
|
||||
EOF
|
||||
|
||||
info "Using dput for CI upload (SFTP)"
|
||||
if dput -c "$DPUT_CONFIG" "avengemedia-${PPA_NAME}" "$CHANGES_FILE"; then
|
||||
if dput -c "$DPUT_CONFIG" "${PPA_OWNER}-${PPA_NAME}" "$CHANGES_FILE"; then
|
||||
success "Upload successful!"
|
||||
rm -f "$DPUT_CONFIG"
|
||||
else
|
||||
@@ -389,7 +389,7 @@ EOF
|
||||
else
|
||||
LFTP_SCRIPT=$(mktemp)
|
||||
cat >"$LFTP_SCRIPT" <<EOF
|
||||
cd ~avengemedia/ubuntu/$PPA_NAME/
|
||||
cd ~${PPA_OWNER}/ubuntu/$PPA_NAME/
|
||||
lcd $BUILD_DIR
|
||||
mput $CHANGES_BASENAME
|
||||
mput $DSC_FILE
|
||||
@@ -418,7 +418,7 @@ fi
|
||||
echo
|
||||
success "Package uploaded successfully!"
|
||||
info "Monitor build progress at:"
|
||||
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
|
||||
echo " https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/$PPA_NAME/+packages"
|
||||
echo
|
||||
|
||||
if [ "$KEEP_BUILDS" = "false" ]; then
|
||||
|
||||
@@ -5,32 +5,35 @@ XBPS templates for Dux Shell on [Void Linux](https://voidlinux.org).
|
||||
| Package | Source repo | Template |
|
||||
| --- | --- | --- |
|
||||
| `dms` | Dux Shell | [`srcpkgs/dms/template`](srcpkgs/dms/template) |
|
||||
| `dgop` | duxlabs-dev/dux-gop | maintained in the **danklinux** repo (`distro/void/srcpkgs/dgop`) |
|
||||
| `danksearch` | duxlabs-dev/dux-search | maintained in the **danklinux** repo (`distro/void/srcpkgs/danksearch`) |
|
||||
| `dgop` | duxlabs-dev/dux-gop | maintained in the **dux-repo** repo (`distro/void/srcpkgs/dgop`) |
|
||||
| `danksearch` | duxlabs-dev/dux-search | maintained in the **dux-repo** repo (`distro/void/srcpkgs/danksearch`) |
|
||||
|
||||
All build from source.
|
||||
|
||||
## Distribution
|
||||
|
||||
This is a DMS maintained repo for VoidLinux until these packages are officially merged upstream in the Void Linux repositories, you can install them from our self-hosted custom XBPS repositories served via GitHub Pages.
|
||||
This is a DMS maintained repo for VoidLinux until these packages are officially merged upstream in the Void Linux repositories, you can install them from our self-hosted custom XBPS repositories.
|
||||
|
||||
### Using the Self-Hosted Repositories
|
||||
|
||||
We serve both stable release and development packages from Cloudflare R2 at
|
||||
`void.danklinux.com`.
|
||||
`void.duxlabs.dev`: `dms` and `dms-git` from `dms/current`, their dependencies
|
||||
and `dms-greeter` from `dux-repo/current`.
|
||||
|
||||
> **Repository migration:** the former GitHub Pages repositories will be
|
||||
> frozen for 14 days at cutover. Their retirement date will be announced when
|
||||
> the snapshots are frozen. Replace any existing `avengemedia.github.io`
|
||||
> entries with the URLs below.
|
||||
>
|
||||
> Existing installs: run `dms migrate dux`.
|
||||
|
||||
#### 1. Add Repository Configurations
|
||||
|
||||
Create configuration files in `/etc/xbps.d/` pointing to our repositories (needed for both stable and git/nightly variants):
|
||||
|
||||
```sh
|
||||
echo "repository=https://void.danklinux.com/dms/current" | sudo tee /etc/xbps.d/dms.conf
|
||||
echo "repository=https://void.danklinux.com/danklinux/current" | sudo tee /etc/xbps.d/danklinux.conf
|
||||
echo "repository=https://void.duxlabs.dev/dms/current" | sudo tee /etc/xbps.d/dms.conf
|
||||
echo "repository=https://void.duxlabs.dev/dux-repo/current" | sudo tee /etc/xbps.d/dux.conf
|
||||
```
|
||||
|
||||
#### 2. Install DMS
|
||||
|
||||
@@ -44,6 +44,7 @@ Singleton {
|
||||
property string terminalOverride: ""
|
||||
property int updaterNotifiedUnix: 0
|
||||
property int updaterNotifiedCount: 0
|
||||
property string repoMoveNoticeVersion: ""
|
||||
property bool isSwitchingMode: false
|
||||
property bool suppressOSD: true
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Singleton {
|
||||
readonly property string domain: "duxlabs.dev"
|
||||
readonly property string web: "https://" + domain
|
||||
readonly property string docs: web + "/docs"
|
||||
readonly property string api: "https://api.danklinux.com"
|
||||
readonly property string api: "https://api.duxlabs.dev"
|
||||
readonly property string plugins: "https://plugins." + domain
|
||||
readonly property string wallpaperCreator: "https://wallpaper." + domain
|
||||
}
|
||||
@@ -30,6 +30,9 @@ var LOCAL_SPEC = {
|
||||
updaterNotifiedCount: {
|
||||
def: 0
|
||||
},
|
||||
repoMoveNoticeVersion: {
|
||||
def: ""
|
||||
},
|
||||
terminalOverride: {
|
||||
def: ""
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
@@ -422,6 +423,27 @@ Item {
|
||||
iconColor: Theme.warning
|
||||
subtitle: SystemUpdateService.shellInstalled ? I18n.tr("Installed %1, still running %2", "%1 is the installed version, %2 the running one").arg(SystemUpdateService.shellInstalled).arg(SystemUpdateService.shellRunning) : I18n.tr("A newer dms binary is installed.", "software updates row subtitle, the running shell is older than the installed binary")
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
visible: SystemUpdateService.legacyRepos.length > 0
|
||||
title: I18n.tr("Package repositories moved", "notification title, the DMS package repositories moved to a new owner")
|
||||
subtitle: I18n.tr("Run %1 in a terminal.", "%1 is a shell command").arg(SystemUpdateService.migrateCommand)
|
||||
iconName: "swap_horiz"
|
||||
iconColor: Theme.warning
|
||||
|
||||
DActionButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: Theme.buttonHeightS
|
||||
iconName: "content_copy"
|
||||
iconColor: Theme.surfaceText
|
||||
backgroundColor: SettingsMetrics.controlSurface
|
||||
tooltipText: I18n.tr("Copy")
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["dms", "cl", "copy", SystemUpdateService.migrateCommand]);
|
||||
ToastService.showInfo(I18n.tr("Copied to clipboard"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
|
||||
@@ -823,4 +823,8 @@ Singleton {
|
||||
"force": force === true
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function sysupdateRepoStatus(callback) {
|
||||
sendRequest("sysupdate.repoStatus", null, callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.pragma library
|
||||
|
||||
// Full version on purpose: every release re-arms the notice once.
|
||||
function repoNoticeDue(legacy, seenVersion, currentVersion) {
|
||||
return (legacy?.length ?? 0) > 0 && currentVersion !== "" && seenVersion !== currentVersion;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import "RepoMoveNotice.js" as RepoMoveNotice
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -61,6 +62,9 @@ Singleton {
|
||||
// -1 while unknown (stable channel or no feed yet)
|
||||
property int commitsBehind: -1
|
||||
property var releases: null
|
||||
// Repo files, not URLs: what `dms migrate dux` would move.
|
||||
property var legacyRepos: []
|
||||
property string migrateCommand: "dms migrate dux"
|
||||
|
||||
readonly property int updateCount: availableUpdates.length
|
||||
readonly property var systemUpdates: availableUpdates.filter(p => !shellUpdate || p.name !== shellUpdate.name)
|
||||
@@ -116,6 +120,20 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SessionData
|
||||
function onLoaded() {
|
||||
root._maybeNotifyRepoMove();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ShellVersionService
|
||||
function onSemverVersionChanged() {
|
||||
root._maybeNotifyRepoMove();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onUpdaterCheckOnStartChanged() {
|
||||
@@ -151,6 +169,7 @@ Singleton {
|
||||
const has = DMSService.capabilities.includes("sysupdate");
|
||||
if (has && !sysupdateAvailable) {
|
||||
sysupdateAvailable = true;
|
||||
requestRepoStatus();
|
||||
if (pollWanted) {
|
||||
requestState();
|
||||
// The daemon persists its last check but not the interval; re-apply it on every fresh connection.
|
||||
@@ -173,6 +192,39 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
function requestRepoStatus() {
|
||||
DMSService.sysupdateRepoStatus(resp => {
|
||||
const result = resp?.result;
|
||||
if (!result)
|
||||
return;
|
||||
root.legacyRepos = result.legacy || [];
|
||||
root.migrateCommand = result.command || "dms migrate dux";
|
||||
root._maybeNotifyRepoMove();
|
||||
});
|
||||
}
|
||||
|
||||
function _maybeNotifyRepoMove() {
|
||||
if (_repoNoticeInFlight || !SessionData._hasLoaded)
|
||||
return;
|
||||
const current = ShellVersionService.semverVersion;
|
||||
if (!RepoMoveNotice.repoNoticeDue(legacyRepos, SessionData.repoMoveNoticeVersion, current))
|
||||
return;
|
||||
_repoNoticeInFlight = true;
|
||||
DMSService.notifySend({
|
||||
"summary": I18n.tr("Package repositories moved", "notification title, the DMS package repositories moved to a new owner"),
|
||||
"body": I18n.tr("Run %1 in a terminal.", "%1 is a shell command").arg(migrateCommand),
|
||||
"actionLabel": I18n.tr("Settings"),
|
||||
"actionArgs": ["ipc", "call", "settings", "openWith", "updater"]
|
||||
}, resp => {
|
||||
root._repoNoticeInFlight = false;
|
||||
if (!resp || resp.error)
|
||||
return;
|
||||
SessionData.set("repoMoveNoticeVersion", current);
|
||||
});
|
||||
}
|
||||
|
||||
property bool _repoNoticeInFlight: false
|
||||
|
||||
function _applyState(data) {
|
||||
if (!data) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import QtQuick
|
||||
import QtTest
|
||||
import "../../Services/RepoMoveNotice.js" as RepoMoveNotice
|
||||
|
||||
TestCase {
|
||||
name: "SysupdateRepoNotice"
|
||||
|
||||
readonly property var legacy: ["/etc/yum.repos.d/_copr:copr.fedorainfracloud.org:avengemedia:danklinux.repo"]
|
||||
|
||||
function test_repoNoticeDue_data() {
|
||||
return [
|
||||
{ tag: "legacy on a new version", legacy: legacy, seen: "", current: "1.4.0", due: true },
|
||||
{ tag: "release re-arms", legacy: legacy, seen: "1.4.0", current: "1.4.1", due: true },
|
||||
{ tag: "already shown for this version", legacy: legacy, seen: "1.4.1", current: "1.4.1", due: false },
|
||||
{ tag: "nothing to migrate", legacy: [], seen: "", current: "1.4.0", due: false },
|
||||
{ tag: "version not read yet", legacy: legacy, seen: "1.4.0", current: "", due: false }
|
||||
];
|
||||
}
|
||||
|
||||
function test_repoNoticeDue(row) {
|
||||
compare(RepoMoveNotice.repoNoticeDue(row.legacy, row.seen, row.current), row.due);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user