diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5d9102d6..49dd3c99 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -386,6 +386,68 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ trigger-obs-update:
+ runs-on: ubuntu-latest
+ needs: release
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install OSC
+ run: |
+ 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
+
+ - name: Update OBS packages
+ run: |
+ VERSION="${{ github.ref_name }}"
+ cd distro
+ bash scripts/obs-upload.sh dms "Update to $VERSION"
+
+ trigger-ppa-update:
+ runs-on: ubuntu-latest
+ needs: release
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ debhelper \
+ devscripts \
+ dput \
+ lftp \
+ build-essential \
+ fakeroot \
+ dpkg-dev
+
+ - name: Configure GPG
+ env:
+ GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
+ run: |
+ 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
+
+ - name: Upload to PPA
+ run: |
+ VERSION="${{ github.ref_name }}"
+ cd distro/ubuntu/ppa
+ bash create-and-upload.sh ../dms dms questing
+
copr-build:
runs-on: ubuntu-latest
needs: release
diff --git a/.github/workflows/copr-release.yml b/.github/workflows/run-copr.yml
similarity index 100%
rename from .github/workflows/copr-release.yml
rename to .github/workflows/run-copr.yml
diff --git a/.github/workflows/run-obs.yml b/.github/workflows/run-obs.yml
new file mode 100644
index 00000000..8e249c44
--- /dev/null
+++ b/.github/workflows/run-obs.yml
@@ -0,0 +1,108 @@
+name: Update OBS Packages
+
+on:
+ workflow_dispatch:
+ inputs:
+ package:
+ description: 'Package to update (dms, dms-git, or all)'
+ required: false
+ default: 'all'
+ push:
+ tags:
+ - 'v*'
+ schedule:
+ - cron: '0 */3 * * *' # Every 3 hours for dms-git builds
+
+jobs:
+ update-obs:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Determine packages to update
+ id: packages
+ run: |
+ if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
+ echo "packages=dms" >> $GITHUB_OUTPUT
+ VERSION="${GITHUB_REF#refs/tags/}"
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+ echo "Triggered by tag: $VERSION"
+ elif [[ "${{ github.event_name }}" == "schedule" ]]; then
+ echo "packages=dms-git" >> $GITHUB_OUTPUT
+ echo "Triggered by schedule: updating git package"
+ elif [[ -n "${{ github.event.inputs.package }}" ]]; then
+ echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
+ echo "Manual trigger: ${{ github.event.inputs.package }}"
+ else
+ echo "packages=all" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Update version in packaging files
+ if: steps.packages.outputs.version != ''
+ run: |
+ VERSION="${{ steps.packages.outputs.version }}"
+ VERSION_NO_V="${VERSION#v}"
+ echo "Updating packaging to version $VERSION_NO_V"
+
+ # Update openSUSE spec files
+ sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/*.spec
+
+ # Update Debian _service files
+ for service in distro/debian/*/_service; do
+ if [[ -f "$service" ]]; then
+ sed -i "s|v[0-9.]*|$VERSION|" "$service"
+ fi
+ done
+
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add distro/
+ git commit -m "chore: update packaging to $VERSION" || echo "No changes to commit"
+
+ - name: Install OSC
+ run: |
+ 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
+
+ - name: Upload to OBS
+ run: |
+ PACKAGES="${{ steps.packages.outputs.packages }}"
+ MESSAGE="Automated update from GitHub Actions"
+
+ if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
+ MESSAGE="Update to ${{ steps.packages.outputs.version }}"
+ fi
+
+ cd distro
+
+ if [[ "$PACKAGES" == "all" ]]; then
+ bash scripts/obs-upload.sh dms "$MESSAGE"
+ bash scripts/obs-upload.sh dms-git "Automated git update"
+ else
+ bash scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
+ fi
+
+ - name: Summary
+ run: |
+ echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
+ if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
+ echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
+ fi
+ echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
diff --git a/.github/workflows/run-ppa.yml b/.github/workflows/run-ppa.yml
new file mode 100644
index 00000000..bef8f70c
--- /dev/null
+++ b/.github/workflows/run-ppa.yml
@@ -0,0 +1,101 @@
+name: Update PPA Packages
+
+on:
+ workflow_dispatch:
+ inputs:
+ package:
+ description: 'Package to upload (dms, dms-git, or all)'
+ required: false
+ default: 'dms-git'
+ schedule:
+ - cron: '0 */3 * * *' # Every 3 hours for dms-git builds
+
+jobs:
+ upload-ppa:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y \
+ debhelper \
+ devscripts \
+ dput \
+ lftp \
+ build-essential \
+ fakeroot \
+ dpkg-dev
+
+ - name: Configure GPG
+ env:
+ GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
+ run: |
+ 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
+
+ - name: Determine packages to upload
+ id: packages
+ run: |
+ if [[ "${{ github.event_name }}" == "schedule" ]]; then
+ echo "packages=dms-git" >> $GITHUB_OUTPUT
+ echo "Triggered by schedule: uploading git package"
+ elif [[ -n "${{ github.event.inputs.package }}" ]]; then
+ echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
+ echo "Manual trigger: ${{ github.event.inputs.package }}"
+ else
+ echo "packages=dms-git" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Upload to PPA
+ run: |
+ PACKAGES="${{ steps.packages.outputs.packages }}"
+
+ cd distro/ubuntu/ppa
+
+ if [[ "$PACKAGES" == "all" ]]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "Uploading dms to PPA..."
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ bash create-and-upload.sh "../dms" dms questing
+
+ echo ""
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "Uploading dms-git to PPA..."
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ bash create-and-upload.sh "../dms-git" dms-git questing
+ else
+ PPA_NAME="$PACKAGES"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "Uploading $PACKAGES to PPA..."
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ bash create-and-upload.sh "../$PACKAGES" "$PPA_NAME" questing
+ fi
+
+ - name: Summary
+ run: |
+ echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
+
+ PACKAGES="${{ steps.packages.outputs.packages }}"
+ if [[ "$PACKAGES" == "all" ]]; then
+ echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
+ echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
+ elif [[ "$PACKAGES" == "dms" ]]; then
+ echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
+ elif [[ "$PACKAGES" == "dms-git" ]]; then
+ echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
+ fi
+
+ if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
+ echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
+ fi
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
diff --git a/distro/debian/dms-git/_service b/distro/debian/dms-git/_service
new file mode 100644
index 00000000..3d6a6791
--- /dev/null
+++ b/distro/debian/dms-git/_service
@@ -0,0 +1,24 @@
+
+
+
+ git
+ https://github.com/AvengeMedia/DankMaterialShell.git
+ master
+ dms-git-source
+
+
+ *.tar
+ gz
+
+
+
+ https
+ github.com
+ /AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-amd64.gz
+
+
+ https
+ github.com
+ /AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz
+
+
diff --git a/distro/debian/dms-git/debian/changelog b/distro/debian/dms-git/debian/changelog
new file mode 100644
index 00000000..7579c3a8
--- /dev/null
+++ b/distro/debian/dms-git/debian/changelog
@@ -0,0 +1,8 @@
+dms-git (0.6.2+git) nightly; urgency=medium
+
+ * Build dms binary from source for true git version strings
+ * Match Fedora COPR git build behavior
+ * Now shows proper git version (e.g., v0.6.2-11-g12e91534)
+ * Add golang-go and make as build dependencies
+
+ -- Avenge Media Fri, 22 Nov 2025 00:00:00 -0500
diff --git a/distro/debian/dms-git/debian/control b/distro/debian/dms-git/debian/control
new file mode 100644
index 00000000..197c8360
--- /dev/null
+++ b/distro/debian/dms-git/debian/control
@@ -0,0 +1,50 @@
+Source: dms-git
+Section: x11
+Priority: optional
+Maintainer: Avenge Media
+Build-Depends: debhelper-compat (= 13)
+Standards-Version: 4.6.2
+Homepage: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
+
+Package: dms-git
+Architecture: amd64 arm64
+Depends: ${misc:Depends},
+ quickshell-git | quickshell,
+ accountsservice,
+ cava,
+ cliphist,
+ danksearch,
+ dgop,
+ matugen,
+ qml6-module-qtcore,
+ qml6-module-qtmultimedia,
+ qml6-module-qtqml,
+ qml6-module-qtquick,
+ qml6-module-qtquick-controls,
+ qml6-module-qtquick-dialogs,
+ qml6-module-qtquick-effects,
+ qml6-module-qtquick-layouts,
+ qml6-module-qtquick-templates,
+ qml6-module-qtquick-window,
+ qt6ct,
+ wl-clipboard
+Provides: dms
+Conflicts: dms
+Replaces: dms
+Description: DankMaterialShell - Modern Wayland Desktop Shell (git nightly)
+ DMS (DankMaterialShell) is a feature-rich desktop shell built on
+ Quickshell, providing a modern and customizable user interface for
+ Wayland compositors like niri, hyprland, and sway.
+ .
+ This is the nightly/git version built from the latest master branch.
+ .
+ Features include:
+ - Material Design inspired UI
+ - Customizable themes and appearance
+ - Built-in application launcher
+ - System tray and notifications
+ - Network and Bluetooth management
+ - Audio controls
+ - Systemd integration
diff --git a/distro/debian/dms-git/debian/copyright b/distro/debian/dms-git/debian/copyright
new file mode 100644
index 00000000..1e7bce73
--- /dev/null
+++ b/distro/debian/dms-git/debian/copyright
@@ -0,0 +1,27 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: dms
+Upstream-Contact: Avenge Media LLC
+Source: https://github.com/AvengeMedia/DankMaterialShell
+
+Files: *
+Copyright: 2025 Avenge Media LLC
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/distro/debian/dms-git/debian/files b/distro/debian/dms-git/debian/files
new file mode 100644
index 00000000..55b5f32b
--- /dev/null
+++ b/distro/debian/dms-git/debian/files
@@ -0,0 +1 @@
+dms-git_0.6.0+git2061.5ddea836ppa1_source.buildinfo x11 optional
diff --git a/distro/debian/dms-git/debian/rules b/distro/debian/dms-git/debian/rules
new file mode 100755
index 00000000..3d5cf6fc
--- /dev/null
+++ b/distro/debian/dms-git/debian/rules
@@ -0,0 +1,54 @@
+#!/usr/bin/make -f
+
+DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
+UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
+DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
+
+%:
+ dh $@
+
+override_dh_auto_build:
+ if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
+ if [ -f dms-distropkg-amd64.gz ]; then \
+ gunzip -c dms-distropkg-amd64.gz > dms; \
+ elif [ -f ../SOURCES/dms-distropkg-amd64.gz ]; then \
+ gunzip -c ../SOURCES/dms-distropkg-amd64.gz > dms; \
+ else \
+ echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1; \
+ fi \
+ elif [ "$(DEB_HOST_ARCH)" = "arm64" ]; then \
+ if [ -f dms-distropkg-arm64.gz ]; then \
+ gunzip -c dms-distropkg-arm64.gz > dms; \
+ elif [ -f ../SOURCES/dms-distropkg-arm64.gz ]; then \
+ gunzip -c ../SOURCES/dms-distropkg-arm64.gz > dms; \
+ else \
+ echo "ERROR: dms-distropkg-arm64.gz not found!" && exit 1; \
+ fi \
+ else \
+ echo "Unsupported architecture: $(DEB_HOST_ARCH)" && exit 1; \
+ fi
+ chmod +x dms
+
+override_dh_auto_install:
+ install -Dm755 dms debian/dms-git/usr/bin/dms
+
+ mkdir -p debian/dms-git/usr/share/quickshell/dms debian/dms-git/usr/lib/systemd/user
+ if [ -d quickshell ]; then \
+ cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
+ install -Dm644 quickshell/assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
+ elif [ -d dms-git-source/quickshell ]; then \
+ cp -r dms-git-source/quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
+ install -Dm644 dms-git-source/quickshell/assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
+ else \
+ echo "ERROR: quickshell directory not found (checked root and dms-git-source/)!" && \
+ echo "Contents of current directory:" && ls -la && \
+ exit 1; \
+ fi
+
+ rm -rf debian/dms-git/usr/share/quickshell/dms/core \
+ debian/dms-git/usr/share/quickshell/dms/distro
+
+override_dh_auto_clean:
+ rm -f dms
+ [ ! -d dms-git-source ] || rm -rf dms-git-source
+ dh_auto_clean
diff --git a/distro/debian/dms-git/debian/source/format b/distro/debian/dms-git/debian/source/format
new file mode 100644
index 00000000..89ae9db8
--- /dev/null
+++ b/distro/debian/dms-git/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/distro/debian/dms-git/debian/source/include-binaries b/distro/debian/dms-git/debian/source/include-binaries
new file mode 100644
index 00000000..77d7423a
--- /dev/null
+++ b/distro/debian/dms-git/debian/source/include-binaries
@@ -0,0 +1 @@
+dms-distropkg-amd64.gz
diff --git a/distro/debian/dms-git/debian/source/options b/distro/debian/dms-git/debian/source/options
new file mode 100644
index 00000000..ec74bb70
--- /dev/null
+++ b/distro/debian/dms-git/debian/source/options
@@ -0,0 +1,4 @@
+# Include files that are normally excluded by .gitignore
+# These are needed for the build process on Launchpad
+tar-ignore = !dms-distropkg-amd64.gz
+tar-ignore = !dms-git-repo
diff --git a/distro/debian/dms/_service b/distro/debian/dms/_service
new file mode 100644
index 00000000..16d5ac5f
--- /dev/null
+++ b/distro/debian/dms/_service
@@ -0,0 +1,21 @@
+
+
+
+ https
+ github.com
+ /AvengeMedia/DankMaterialShell/archive/refs/tags/v0.6.2.tar.gz
+ dms-source.tar.gz
+
+
+
+ https
+ github.com
+ /AvengeMedia/DankMaterialShell/releases/download/v0.6.2/dms-distropkg-amd64.gz
+
+
+
+ https
+ github.com
+ /AvengeMedia/DankMaterialShell/releases/download/v0.6.2/dms-distropkg-arm64.gz
+
+
diff --git a/distro/debian/dms/debian/changelog b/distro/debian/dms/debian/changelog
new file mode 100644
index 00000000..5cea7c04
--- /dev/null
+++ b/distro/debian/dms/debian/changelog
@@ -0,0 +1,7 @@
+dms (0.6.2) stable; urgency=medium
+
+ * Update to v0.6.2 release
+ * Fix binary download paths for OBS builds
+ * Native format: removed revisions
+
+ -- Avenge Media Tue, 19 Nov 2025 10:00:00 -0500
diff --git a/distro/debian/dms/debian/control b/distro/debian/dms/debian/control
new file mode 100644
index 00000000..91e63601
--- /dev/null
+++ b/distro/debian/dms/debian/control
@@ -0,0 +1,47 @@
+Source: dms
+Section: x11
+Priority: optional
+Maintainer: Avenge Media
+Build-Depends: debhelper-compat (= 13)
+Standards-Version: 4.6.2
+Homepage: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
+
+Package: dms
+Architecture: amd64
+Depends: ${misc:Depends},
+ quickshell-git | quickshell,
+ accountsservice,
+ cava,
+ cliphist,
+ danksearch,
+ dgop,
+ matugen,
+ qml6-module-qtcore,
+ qml6-module-qtmultimedia,
+ qml6-module-qtqml,
+ qml6-module-qtquick,
+ qml6-module-qtquick-controls,
+ qml6-module-qtquick-dialogs,
+ qml6-module-qtquick-effects,
+ qml6-module-qtquick-layouts,
+ qml6-module-qtquick-templates,
+ qml6-module-qtquick-window,
+ qt6ct,
+ wl-clipboard
+Conflicts: dms-git
+Replaces: dms-git
+Description: DankMaterialShell - Modern Wayland Desktop Shell
+ DMS (DankMaterialShell) is a feature-rich desktop shell built on
+ Quickshell, providing a modern and customizable user interface for
+ Wayland compositors like niri, hyprland, and sway.
+ .
+ Features include:
+ - Material Design inspired UI
+ - Customizable themes and appearance
+ - Built-in application launcher
+ - System tray and notifications
+ - Network and Bluetooth management
+ - Audio controls
+ - Systemd integration
diff --git a/distro/debian/dms/debian/copyright b/distro/debian/dms/debian/copyright
new file mode 100644
index 00000000..1e7bce73
--- /dev/null
+++ b/distro/debian/dms/debian/copyright
@@ -0,0 +1,27 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: dms
+Upstream-Contact: Avenge Media LLC
+Source: https://github.com/AvengeMedia/DankMaterialShell
+
+Files: *
+Copyright: 2025 Avenge Media LLC
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/distro/debian/dms/debian/files b/distro/debian/dms/debian/files
new file mode 100644
index 00000000..1ff65a54
--- /dev/null
+++ b/distro/debian/dms/debian/files
@@ -0,0 +1 @@
+dms_0.6.0ppa2_source.buildinfo x11 optional
diff --git a/distro/debian/dms/debian/rules b/distro/debian/dms/debian/rules
new file mode 100755
index 00000000..cbcd12c3
--- /dev/null
+++ b/distro/debian/dms/debian/rules
@@ -0,0 +1,64 @@
+#!/usr/bin/make -f
+
+DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
+UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
+DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
+
+%:
+ dh $@
+
+override_dh_auto_build:
+ if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
+ if [ -f dms-distropkg-amd64.gz ]; then \
+ gunzip -c dms-distropkg-amd64.gz > dms; \
+ elif [ -f ../SOURCES/dms-distropkg-amd64.gz ]; then \
+ gunzip -c ../SOURCES/dms-distropkg-amd64.gz > dms; \
+ elif [ -f ../../SOURCES/dms-distropkg-amd64.gz ]; then \
+ gunzip -c ../../SOURCES/dms-distropkg-amd64.gz > dms; \
+ else \
+ echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1; \
+ fi \
+ elif [ "$(DEB_HOST_ARCH)" = "arm64" ]; then \
+ if [ -f dms-distropkg-arm64.gz ]; then \
+ gunzip -c dms-distropkg-arm64.gz > dms; \
+ elif [ -f ../SOURCES/dms-distropkg-arm64.gz ]; then \
+ gunzip -c ../SOURCES/dms-distropkg-arm64.gz > dms; \
+ elif [ -f ../../SOURCES/dms-distropkg-arm64.gz ]; then \
+ gunzip -c ../../SOURCES/dms-distropkg-arm64.gz > dms; \
+ else \
+ echo "ERROR: dms-distropkg-arm64.gz not found!" && exit 1; \
+ fi \
+ else \
+ echo "Unsupported architecture: $(DEB_HOST_ARCH)" && exit 1; \
+ fi
+ chmod +x dms
+
+ if [ ! -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
+ if [ -f ../SOURCES/dms-source.tar.gz ]; then \
+ tar -xzf ../SOURCES/dms-source.tar.gz; \
+ elif [ -f dms-source.tar.gz ]; then \
+ tar -xzf dms-source.tar.gz; \
+ fi; \
+ fi
+
+
+override_dh_auto_install:
+ install -Dm755 dms debian/dms/usr/bin/dms
+
+ mkdir -p debian/dms/usr/share/quickshell/dms debian/dms/usr/lib/systemd/user
+ if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
+ cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
+ install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
+ else \
+ echo "ERROR: DankMaterialShell-$(UPSTREAM_VERSION) directory not found!" && \
+ echo "Contents of current directory:" && ls -la && \
+ exit 1; \
+ fi
+
+ rm -rf debian/dms/usr/share/quickshell/dms/core \
+ debian/dms/usr/share/quickshell/dms/distro
+
+override_dh_auto_clean:
+ rm -f dms
+ rm -rf DankMaterialShell-$(UPSTREAM_VERSION)
+ dh_auto_clean
diff --git a/distro/debian/dms/debian/source/format b/distro/debian/dms/debian/source/format
new file mode 100644
index 00000000..89ae9db8
--- /dev/null
+++ b/distro/debian/dms/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/distro/debian/dms/debian/source/include-binaries b/distro/debian/dms/debian/source/include-binaries
new file mode 100644
index 00000000..bac25ab7
--- /dev/null
+++ b/distro/debian/dms/debian/source/include-binaries
@@ -0,0 +1,2 @@
+dms-distropkg-amd64.gz
+dms-source.tar.gz
diff --git a/distro/debian/dms/debian/source/options b/distro/debian/dms/debian/source/options
new file mode 100644
index 00000000..27bae919
--- /dev/null
+++ b/distro/debian/dms/debian/source/options
@@ -0,0 +1,4 @@
+# Include files that are normally excluded by .gitignore
+# These are needed for the build process on Launchpad
+tar-ignore = !dms-distropkg-amd64.gz
+tar-ignore = !dms-source.tar.gz
diff --git a/distro/opensuse/_service b/distro/opensuse/_service
new file mode 100644
index 00000000..47192af1
--- /dev/null
+++ b/distro/opensuse/_service
@@ -0,0 +1,24 @@
+
+
+
+ git
+ https://github.com/AvengeMedia/DankMaterialShell.git
+ master
+ dms-git-source
+
+
+ *.tar
+ gz
+
+
+
+ https
+ github.com
+ /AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-amd64.gz
+
+
+ https
+ github.com
+ /AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz
+
+
diff --git a/distro/opensuse/dms-git.spec b/distro/opensuse/dms-git.spec
new file mode 100644
index 00000000..e1cc8951
--- /dev/null
+++ b/distro/opensuse/dms-git.spec
@@ -0,0 +1,107 @@
+%global debug_package %{nil}
+
+Name: dms-git
+Version: 0.6.2+git
+Release: 5%{?dist}
+Epoch: 1
+Summary: DankMaterialShell - Material 3 inspired shell (git nightly)
+
+License: MIT
+URL: https://github.com/AvengeMedia/DankMaterialShell
+Source0: dms-git-source.tar.gz
+Source1: dms-distropkg-amd64.gz
+Source2: dms-distropkg-arm64.gz
+
+BuildRequires: gzip
+BuildRequires: systemd-rpm-macros
+
+Requires: (quickshell-git or quickshell)
+Requires: accountsservice
+Requires: dgop
+
+Recommends: cava
+Recommends: cliphist
+Recommends: danksearch
+Recommends: matugen
+Recommends: quickshell-git
+Recommends: wl-clipboard
+
+Recommends: NetworkManager
+Recommends: qt6-qtmultimedia
+Suggests: qt6ct
+
+Provides: dms
+Conflicts: dms
+Obsoletes: dms
+
+%description
+DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
+and optimized for niri, Hyprland, Sway, and other wlroots compositors.
+
+This git version tracks the master branch and includes the latest features
+and fixes. Includes pre-built dms CLI binary and QML shell files.
+
+%prep
+%setup -q -n dms-git-source
+
+%ifarch x86_64
+gunzip -c %{SOURCE1} > dms
+%endif
+%ifarch aarch64
+gunzip -c %{SOURCE2} > dms
+%endif
+chmod +x dms
+
+%build
+
+%install
+install -Dm755 dms %{buildroot}%{_bindir}/dms
+
+install -d %{buildroot}%{_datadir}/bash-completion/completions
+install -d %{buildroot}%{_datadir}/zsh/site-functions
+install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
+./dms completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
+./dms completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
+./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
+
+install -Dm644 quickshell/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
+
+install -dm755 %{buildroot}%{_datadir}/quickshell/dms
+cp -r quickshell/* %{buildroot}%{_datadir}/quickshell/dms/
+
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
+rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
+
+%posttrans
+if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
+ rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
+ rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
+fi
+
+if [ "$1" -ge 2 ]; then
+ pkill -USR1 -x dms >/dev/null 2>&1 || true
+fi
+
+%files
+%license LICENSE
+%doc CONTRIBUTING.md
+%doc quickshell/README.md
+%{_bindir}/dms
+%dir %{_datadir}/fish
+%dir %{_datadir}/fish/vendor_completions.d
+%{_datadir}/fish/vendor_completions.d/dms.fish
+%dir %{_datadir}/zsh
+%dir %{_datadir}/zsh/site-functions
+%{_datadir}/zsh/site-functions/_dms
+%{_datadir}/bash-completion/completions/dms
+%dir %{_datadir}/quickshell
+%{_datadir}/quickshell/dms/
+%{_userunitdir}/dms.service
+
+%changelog
+* Fri Nov 22 2025 AvengeMedia - 0.6.2+git-5
+- Git nightly build from master branch
+- Multi-arch support (x86_64, aarch64)
diff --git a/distro/opensuse/dms.spec b/distro/opensuse/dms.spec
new file mode 100644
index 00000000..e6a6babd
--- /dev/null
+++ b/distro/opensuse/dms.spec
@@ -0,0 +1,107 @@
+# Spec for DMS for OpenSUSE/OBS
+
+%global debug_package %{nil}
+
+Name: dms
+Version: 0.6.2
+Release: 1%{?dist}
+Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
+
+License: MIT
+URL: https://github.com/AvengeMedia/DankMaterialShell
+Source0: dms-source.tar.gz
+Source1: dms-distropkg-amd64.gz
+Source2: dms-distropkg-arm64.gz
+
+BuildRequires: gzip
+BuildRequires: systemd-rpm-macros
+
+# Core requirements
+Requires: (quickshell-git or quickshell)
+Requires: accountsservice
+Requires: dgop
+
+# Core utilities (Highly recommended for DMS functionality)
+Recommends: cava
+Recommends: cliphist
+Recommends: danksearch
+Recommends: matugen
+Recommends: NetworkManager
+Recommends: qt6-qtmultimedia
+Recommends: wl-clipboard
+Suggests: qt6ct
+
+%description
+DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
+and optimized for niri, Hyprland, Sway, and other wlroots compositors. Features
+notifications, app launcher, wallpaper customization, and plugin system.
+
+Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
+process monitoring, notification center, clipboard history, dock, control center,
+lock screen, and comprehensive plugin system.
+
+%prep
+%setup -q -n DankMaterialShell-%{version}
+
+%ifarch x86_64
+gunzip -c %{SOURCE1} > dms
+%endif
+%ifarch aarch64
+gunzip -c %{SOURCE2} > dms
+%endif
+chmod +x dms
+
+%build
+
+%install
+install -Dm755 dms %{buildroot}%{_bindir}/dms
+
+install -d %{buildroot}%{_datadir}/bash-completion/completions
+install -d %{buildroot}%{_datadir}/zsh/site-functions
+install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
+./dms completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
+./dms completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
+./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
+
+install -Dm644 quickshell/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
+
+install -dm755 %{buildroot}%{_datadir}/quickshell/dms
+cp -r quickshell/* %{buildroot}%{_datadir}/quickshell/dms/
+
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
+rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
+rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
+
+%posttrans
+if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
+ rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
+ rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
+ rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
+fi
+
+if [ "$1" -ge 2 ]; then
+ pkill -USR1 -x dms >/dev/null 2>&1 || true
+fi
+
+%files
+%license LICENSE
+%doc CONTRIBUTING.md
+%doc quickshell/README.md
+%{_bindir}/dms
+%dir %{_datadir}/fish
+%dir %{_datadir}/fish/vendor_completions.d
+%{_datadir}/fish/vendor_completions.d/dms.fish
+%dir %{_datadir}/zsh
+%dir %{_datadir}/zsh/site-functions
+%{_datadir}/zsh/site-functions/_dms
+%{_datadir}/bash-completion/completions/dms
+%dir %{_datadir}/quickshell
+%{_datadir}/quickshell/dms/
+%{_userunitdir}/dms.service
+
+%changelog
+* Fri Nov 22 2025 AvengeMedia - 0.6.2-1
+- Stable release build with pre-built binaries
+- Multi-arch support (x86_64, aarch64)
diff --git a/distro/scripts/obs-status.sh b/distro/scripts/obs-status.sh
new file mode 100644
index 00000000..f4952350
--- /dev/null
+++ b/distro/scripts/obs-status.sh
@@ -0,0 +1,106 @@
+#!/bin/bash
+# Unified OBS status checker for dms packages
+# Checks all platforms (Debian, OpenSUSE) and architectures (x86_64, aarch64)
+# Only pulls logs if build failed
+# Usage: ./distro/scripts/obs-status.sh [package-name]
+#
+# Examples:
+# ./distro/scripts/obs-status.sh # Check all packages
+# ./distro/scripts/obs-status.sh dms # Check specific package
+
+OBS_BASE_PROJECT="home:AvengeMedia"
+OBS_BASE="$HOME/.cache/osc-checkouts"
+
+ALL_PACKAGES=(dms dms-git)
+
+REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
+ARCHES=("x86_64" "aarch64")
+
+if [[ -n "$1" ]]; then
+ PACKAGES=("$1")
+else
+ PACKAGES=("${ALL_PACKAGES[@]}")
+fi
+
+cd "$OBS_BASE"
+
+for pkg in "${PACKAGES[@]}"; do
+ case "$pkg" in
+ dms)
+ PROJECT="$OBS_BASE_PROJECT:dms"
+ ;;
+ dms-git)
+ PROJECT="$OBS_BASE_PROJECT:dms-git"
+ ;;
+ *)
+ echo "Error: Unknown package '$pkg'"
+ continue
+ ;;
+ esac
+
+ echo "=========================================="
+ echo "=== $pkg ==="
+ echo "=========================================="
+
+ # Checkout if needed
+ if [[ ! -d "$PROJECT/$pkg" ]]; then
+ osc co "$PROJECT/$pkg" 2>&1 | tail -1
+ fi
+
+ cd "$PROJECT/$pkg"
+
+ ALL_RESULTS=$(osc results 2>&1)
+
+ # Check each repository and architecture
+ FAILED_BUILDS=()
+ for repo in "${REPOS[@]}"; do
+ for arch in "${ARCHES[@]}"; do
+ STATUS=$(echo "$ALL_RESULTS" | grep "$repo.*$arch" | awk '{print $NF}' | head -1)
+
+ if [[ -n "$STATUS" ]]; then
+ # Color code status
+ case "$STATUS" in
+ succeeded)
+ COLOR="\033[0;32m" # Green
+ SYMBOL="✅"
+ ;;
+ failed)
+ COLOR="\033[0;31m" # Red
+ SYMBOL="❌"
+ FAILED_BUILDS+=("$repo $arch")
+ ;;
+ unresolvable)
+ COLOR="\033[0;33m" # Yellow
+ SYMBOL="⚠️"
+ ;;
+ *)
+ COLOR="\033[0;37m" # White
+ SYMBOL="⏳"
+ ;;
+ esac
+ echo -e " $SYMBOL $repo $arch: ${COLOR}$STATUS\033[0m"
+ fi
+ done
+ done
+
+ # Pull logs for failed builds
+ if [[ ${#FAILED_BUILDS[@]} -gt 0 ]]; then
+ echo ""
+ echo " 📋 Fetching logs for failed builds..."
+ for build in "${FAILED_BUILDS[@]}"; do
+ read -r repo arch <<< "$build"
+ echo ""
+ echo " ────────────────────────────────────────────"
+ echo " Build log: $repo $arch"
+ echo " ────────────────────────────────────────────"
+ osc remotebuildlog "$PROJECT" "$pkg" "$repo" "$arch" 2>&1 | tail -100
+ done
+ fi
+
+ echo ""
+ cd - > /dev/null
+done
+
+echo "=========================================="
+echo "Status check complete!"
+
diff --git a/distro/scripts/obs-upload.sh b/distro/scripts/obs-upload.sh
new file mode 100644
index 00000000..0ed13f3d
--- /dev/null
+++ b/distro/scripts/obs-upload.sh
@@ -0,0 +1,733 @@
+#!/bin/bash
+# Unified OBS upload script for dms packages
+# Handles Debian and OpenSUSE builds for both x86_64 and aarch64
+# Usage: ./distro/scripts/obs-upload.sh [distro] [commit-message]
+#
+# Examples:
+# ./distro/scripts/obs-upload.sh dms "Update to v0.6.2"
+# ./distro/scripts/obs-upload.sh debian dms
+# ./distro/scripts/obs-upload.sh opensuse dms-git
+
+set -e
+
+UPLOAD_DEBIAN=true
+UPLOAD_OPENSUSE=true
+PACKAGE=""
+MESSAGE=""
+
+for arg in "$@"; do
+ case "$arg" in
+ debian)
+ UPLOAD_DEBIAN=true
+ UPLOAD_OPENSUSE=false
+ ;;
+ opensuse)
+ UPLOAD_DEBIAN=false
+ UPLOAD_OPENSUSE=true
+ ;;
+ *)
+ if [[ -z "$PACKAGE" ]]; then
+ PACKAGE="$arg"
+ elif [[ -z "$MESSAGE" ]]; then
+ MESSAGE="$arg"
+ fi
+ ;;
+ esac
+done
+
+OBS_BASE_PROJECT="home:AvengeMedia"
+OBS_BASE="$HOME/.cache/osc-checkouts"
+
+# Available packages
+AVAILABLE_PACKAGES=(dms dms-git)
+
+if [[ -z "$PACKAGE" ]]; then
+ echo "Available packages:"
+ echo ""
+ echo " 1. dms - Stable DMS"
+ echo " 2. dms-git - Nightly DMS"
+ echo " a. all"
+ echo ""
+ read -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
+
+ if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
+ PACKAGE="all"
+ elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
+ PACKAGE="${AVAILABLE_PACKAGES[$((selection-1))]}"
+ else
+ echo "Error: Invalid selection"
+ exit 1
+ fi
+
+fi
+
+if [[ -z "$MESSAGE" ]]; then
+ MESSAGE="Update packaging"
+fi
+
+# Get repo root (2 levels up from distro/scripts/)
+REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+# Ensure we're in repo root
+if [[ ! -d "distro/debian" ]]; then
+ echo "Error: Run this script from the repository root"
+ exit 1
+fi
+
+# Handle "all" option
+if [[ "$PACKAGE" == "all" ]]; then
+ echo "==> Uploading all packages"
+ DISTRO_ARG=""
+ if [[ "$UPLOAD_DEBIAN" == true && "$UPLOAD_OPENSUSE" == false ]]; then
+ DISTRO_ARG="debian"
+ elif [[ "$UPLOAD_DEBIAN" == false && "$UPLOAD_OPENSUSE" == true ]]; then
+ DISTRO_ARG="opensuse"
+ fi
+ echo ""
+ FAILED=()
+ for pkg in "${AVAILABLE_PACKAGES[@]}"; do
+ if [[ -d "distro/debian/$pkg" ]]; then
+ echo "=========================================="
+ echo "Uploading $pkg..."
+ echo "=========================================="
+ if [[ -n "$DISTRO_ARG" ]]; then
+ if bash "$0" "$DISTRO_ARG" "$pkg" "$MESSAGE"; then
+ echo "✅ $pkg uploaded successfully"
+ else
+ echo "❌ $pkg failed to upload"
+ FAILED+=("$pkg")
+ fi
+ else
+ if bash "$0" "$pkg" "$MESSAGE"; then
+ echo "✅ $pkg uploaded successfully"
+ else
+ echo "❌ $pkg failed to upload"
+ FAILED+=("$pkg")
+ fi
+ fi
+ echo ""
+ else
+ echo "⚠️ Skipping $pkg (not found in distro/debian/)"
+ fi
+ done
+
+ if [[ ${#FAILED[@]} -eq 0 ]]; then
+ echo "✅ All packages uploaded successfully!"
+ exit 0
+ else
+ echo "❌ Some packages failed: ${FAILED[*]}"
+ exit 1
+ fi
+fi
+
+# Check if package exists
+if [[ ! -d "distro/debian/$PACKAGE" ]]; then
+ echo "Error: Package '$PACKAGE' not found in distro/debian/"
+ exit 1
+fi
+
+case "$PACKAGE" in
+ dms)
+ PROJECT="dms"
+ ;;
+ dms-git)
+ PROJECT="dms-git"
+ ;;
+ *)
+ echo "Error: Unknown package '$PACKAGE'"
+ exit 1
+ ;;
+esac
+
+OBS_PROJECT="${OBS_BASE_PROJECT}:${PROJECT}"
+
+echo "==> Target: $OBS_PROJECT / $PACKAGE"
+echo "==> Message: $MESSAGE"
+if [[ "$UPLOAD_DEBIAN" == true && "$UPLOAD_OPENSUSE" == true ]]; then
+ echo "==> Distributions: Debian + OpenSUSE"
+elif [[ "$UPLOAD_DEBIAN" == true ]]; then
+ echo "==> Distribution: Debian only"
+elif [[ "$UPLOAD_OPENSUSE" == true ]]; then
+ echo "==> Distribution: OpenSUSE only"
+fi
+
+# Create .obs directory if it doesn't exist
+mkdir -p "$OBS_BASE"
+
+# Check out package if not already present
+if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
+ echo "Checking out $OBS_PROJECT/$PACKAGE..."
+ cd "$OBS_BASE"
+ osc co "$OBS_PROJECT/$PACKAGE"
+ cd "$REPO_ROOT"
+fi
+
+WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
+
+echo "==> Preparing $PACKAGE for OBS upload"
+
+# Clean working directory (keep osc metadata)
+find "$WORK_DIR" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.spec" -o -name "_service" -o -name "*.dsc" \) -delete 2>/dev/null || true
+
+if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
+ echo " - Copying _service (for binary downloads)"
+ cp "distro/debian/$PACKAGE/_service" "$WORK_DIR/"
+fi
+
+# Copy OpenSUSE spec if it exists and handle auto-increment
+if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
+ echo " - Copying $PACKAGE.spec for OpenSUSE"
+ cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
+
+ # Auto-increment Release if same Version is being rebuilt
+ if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
+ NEW_VERSION=$(grep "^Version:" "$WORK_DIR/$PACKAGE.spec" | awk '{print $2}' | head -1)
+ NEW_RELEASE=$(grep "^Release:" "$WORK_DIR/$PACKAGE.spec" | sed 's/^Release:[[:space:]]*//' | sed 's/%{?dist}//' | head -1)
+
+ OLD_VERSION=$(grep "^Version:" "$WORK_DIR/.osc/$PACKAGE.spec" | awk '{print $2}' | head -1)
+ OLD_RELEASE=$(grep "^Release:" "$WORK_DIR/.osc/$PACKAGE.spec" | sed 's/^Release:[[:space:]]*//' | sed 's/%{?dist}//' | head -1)
+
+ if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then
+ # Same version - increment release number
+ if [[ "$OLD_RELEASE" =~ ^([0-9]+) ]]; then
+ BASE_RELEASE="${BASH_REMATCH[1]}"
+ NEXT_RELEASE=$((BASE_RELEASE + 1))
+ echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)"
+ sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${NEXT_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
+ fi
+ else
+ echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)"
+ fi
+ else
+ echo " - First upload to OBS (no previous spec found)"
+ fi
+elif [[ "$UPLOAD_OPENSUSE" == true ]]; then
+ echo " - Warning: OpenSUSE spec file not found, skipping OpenSUSE upload"
+fi
+
+# Handle OpenSUSE-only uploads (create tarball without Debian processing)
+if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
+ echo " - OpenSUSE-only upload: creating source tarball"
+
+ TEMP_DIR=$(mktemp -d)
+ trap "rm -rf $TEMP_DIR" EXIT
+
+ # Check _service file to determine how to get source
+ if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
+ # Check for tar_scm (git source)
+ if grep -q "tar_scm" "distro/debian/$PACKAGE/_service"; then
+ GIT_URL=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "url" | sed 's/.*\(.*\)<\/param>.*/\1/')
+ GIT_REVISION=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "revision" | sed 's/.*\(.*\)<\/param>.*/\1/')
+
+ if [[ -n "$GIT_URL" ]]; then
+ echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
+ SOURCE_DIR="$TEMP_DIR/dms-git-source"
+ if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null || \
+ git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
+ cd "$SOURCE_DIR"
+ if [[ -n "$GIT_REVISION" ]]; then
+ git checkout "$GIT_REVISION" 2>/dev/null || true
+ fi
+ SOURCE_DIR=$(pwd)
+ cd "$REPO_ROOT"
+ fi
+ fi
+ fi
+ fi
+
+ if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
+ # Extract Source0 from spec file
+ SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
+
+ if [[ -n "$SOURCE0" ]]; then
+ OBS_TARBALL_DIR=$(mktemp -d -t obs-tarball-XXXXXX)
+ cd "$OBS_TARBALL_DIR"
+
+ case "$PACKAGE" in
+ dms)
+ DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
+ EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
+ ;;
+ dms-git)
+ EXPECTED_DIR="dms-git-source"
+ ;;
+ *)
+ EXPECTED_DIR=$(basename "$SOURCE_DIR")
+ ;;
+ esac
+
+ echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
+ cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
+ tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
+ rm -rf "$EXPECTED_DIR"
+ echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
+
+ cd "$REPO_ROOT"
+ rm -rf "$OBS_TARBALL_DIR"
+ fi
+ else
+ echo " - Warning: Could not obtain source for OpenSUSE tarball"
+ fi
+fi
+
+# Generate .dsc file and handle source format (for Debian only)
+if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; then
+ # Get version from changelog
+ CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" distro/debian/$PACKAGE/debian/changelog 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "0.1.11")
+
+ # Determine source format
+ SOURCE_FORMAT=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
+
+ # Handle native format (3.0 native)
+ if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
+ echo " - Native format detected: creating combined tarball"
+
+ VERSION="$CHANGELOG_VERSION"
+
+ # Create temp directory for building combined tarball
+ TEMP_DIR=$(mktemp -d)
+ trap "rm -rf $TEMP_DIR" EXIT
+
+ # Determine tarball name for native format (use version without revision)
+ COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
+
+ SOURCE_DIR=""
+
+ # Check _service file to determine how to get source
+ if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
+ # Check for tar_scm first (git source) - this takes priority for git packages
+ if grep -q "tar_scm" "distro/debian/$PACKAGE/_service"; then
+ # For dms-git, use tar_scm to get git source
+ GIT_URL=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "url" | sed 's/.*\(.*\)<\/param>.*/\1/')
+ GIT_REVISION=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "revision" | sed 's/.*\(.*\)<\/param>.*/\1/')
+
+ if [[ -n "$GIT_URL" ]]; then
+ echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
+ SOURCE_DIR="$TEMP_DIR/dms-git-source"
+ if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null || \
+ git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
+ cd "$SOURCE_DIR"
+ if [[ -n "$GIT_REVISION" ]]; then
+ git checkout "$GIT_REVISION" 2>/dev/null || true
+ fi
+ SOURCE_DIR=$(pwd)
+ cd "$REPO_ROOT"
+ else
+ echo "Error: Failed to clone git repository"
+ exit 1
+ fi
+ fi
+ elif grep -q "download_url" "distro/debian/$PACKAGE/_service" && [[ "$PACKAGE" != "dms-git" ]]; then
+ # Extract download_url for source (skip binary downloads)
+ # Look for download_url with "source" in path or .tar.gz/.tar.xz archives
+ # Skip binaries (distropkg, standalone .gz files, etc.)
+
+ # Extract all paths from download_url services
+ ALL_PATHS=$(grep -A 5 '' "distro/debian/$PACKAGE/_service" | \
+ grep '' | \
+ sed 's/.*\(.*\)<\/param>.*/\1/')
+
+ # Find source path (has "source" or ends with .tar.gz/.tar.xz, but not distropkg)
+ SOURCE_PATH=""
+ for path in $ALL_PATHS; do
+ if echo "$path" | grep -qE "(source|archive|\.tar\.(gz|xz|bz2))" && \
+ ! echo "$path" | grep -qE "(distropkg|binary)"; then
+ SOURCE_PATH="$path"
+ break
+ fi
+ done
+
+ # If no source found, try first path that ends with .tar.gz/.tar.xz
+ if [[ -z "$SOURCE_PATH" ]]; then
+ for path in $ALL_PATHS; do
+ if echo "$path" | grep -qE "\.tar\.(gz|xz|bz2)$"; then
+ SOURCE_PATH="$path"
+ break
+ fi
+ done
+ fi
+
+ if [[ -n "$SOURCE_PATH" ]]; then
+ # Extract the service block containing this path
+ SOURCE_BLOCK=$(awk -v target="$SOURCE_PATH" '
+ // { in_block=1; block="" }
+ in_block { block=block"\n"$0 }
+ /<\/service>/ {
+ if (in_block && block ~ target) {
+ print block
+ exit
+ }
+ in_block=0
+ }
+ ' "distro/debian/$PACKAGE/_service")
+
+ URL_PROTOCOL=$(echo "$SOURCE_BLOCK" | grep "protocol" | sed 's/.*\(.*\)<\/param>.*/\1/' | head -1)
+ URL_HOST=$(echo "$SOURCE_BLOCK" | grep "host" | sed 's/.*\(.*\)<\/param>.*/\1/' | head -1)
+ URL_PATH="$SOURCE_PATH"
+ fi
+
+ if [[ -n "$URL_PROTOCOL" && -n "$URL_HOST" && -n "$URL_PATH" ]]; then
+ SOURCE_URL="${URL_PROTOCOL}://${URL_HOST}${URL_PATH}"
+ echo " Downloading source from: $SOURCE_URL"
+
+ if wget -q -O "$TEMP_DIR/source-archive" "$SOURCE_URL"; then
+ cd "$TEMP_DIR"
+ if [[ "$SOURCE_URL" == *.tar.xz ]]; then
+ tar -xJf source-archive
+ elif [[ "$SOURCE_URL" == *.tar.gz ]] || [[ "$SOURCE_URL" == *.tgz ]]; then
+ tar -xzf source-archive
+ fi
+ # GitHub archives extract to DankMaterialShell-VERSION/ or similar
+ SOURCE_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
+ if [[ -z "$SOURCE_DIR" ]]; then
+ # Try to find any extracted directory
+ SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
+ fi
+ if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
+ echo "Error: Failed to extract source archive or find source directory"
+ echo "Contents of $TEMP_DIR:"
+ ls -la "$TEMP_DIR"
+ cd "$REPO_ROOT"
+ exit 1
+ fi
+ # Convert to absolute path
+ SOURCE_DIR=$(cd "$SOURCE_DIR" && pwd)
+ cd "$REPO_ROOT"
+ else
+ echo "Error: Failed to download source from $SOURCE_URL"
+ exit 1
+ fi
+ fi
+ fi
+ fi
+
+ if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
+ echo "Error: Could not determine or obtain source for $PACKAGE"
+ echo "SOURCE_DIR: $SOURCE_DIR"
+ if [[ -d "$TEMP_DIR" ]]; then
+ echo "Contents of temp directory:"
+ ls -la "$TEMP_DIR"
+ fi
+ exit 1
+ fi
+
+ echo " Found source directory: $SOURCE_DIR"
+
+ # Create OpenSUSE-compatible source tarballs BEFORE adding debian/ directory
+ # (OpenSUSE doesn't need debian/ directory)
+ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
+ # If SOURCE_DIR is not set (OpenSUSE-only upload), detect source now
+ if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
+ echo " - Detecting source for OpenSUSE-only upload"
+ if [[ -z "$TEMP_DIR" ]]; then
+ TEMP_DIR=$(mktemp -d)
+ trap "rm -rf $TEMP_DIR" EXIT
+ fi
+
+ # Check _service file to determine how to get source
+ if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
+ # Check for tar_scm first (git source) - this takes priority for git packages
+ if grep -q "tar_scm" "distro/debian/$PACKAGE/_service"; then
+ # For dms-git, use tar_scm to get git source
+ GIT_URL=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "url" | sed 's/.*\(.*\)<\/param>.*/\1/')
+ GIT_REVISION=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "revision" | sed 's/.*\(.*\)<\/param>.*/\1/')
+
+ if [[ -n "$GIT_URL" ]]; then
+ echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
+ SOURCE_DIR="$TEMP_DIR/dms-git-source"
+ if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null || \
+ git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
+ cd "$SOURCE_DIR"
+ if [[ -n "$GIT_REVISION" ]]; then
+ git checkout "$GIT_REVISION" 2>/dev/null || true
+ fi
+ SOURCE_DIR=$(pwd)
+ cd "$REPO_ROOT"
+ else
+ echo "Error: Failed to clone git repository"
+ exit 1
+ fi
+ fi
+ elif grep -q "download_url" "distro/debian/$PACKAGE/_service" && [[ "$PACKAGE" != "dms-git" ]]; then
+ # Extract download_url for source (skip binary downloads)
+ ALL_PATHS=$(grep -A 5 '' "distro/debian/$PACKAGE/_service" | \
+ grep '' | \
+ sed 's/.*\(.*\)<\/param>.*/\1/')
+
+ # Find source path (has "source" or ends with .tar.gz/.tar.xz, but not distropkg)
+ SOURCE_PATH=""
+ for path in $ALL_PATHS; do
+ if echo "$path" | grep -qE "(source|archive|\.tar\.(gz|xz|bz2))" && \
+ ! echo "$path" | grep -qE "(distropkg|binary)"; then
+ SOURCE_PATH="$path"
+ break
+ fi
+ done
+
+ # If no source found, try first path that ends with .tar.gz/.tar.xz
+ if [[ -z "$SOURCE_PATH" ]]; then
+ for path in $ALL_PATHS; do
+ if echo "$path" | grep -qE "\.tar\.(gz|xz|bz2)$"; then
+ SOURCE_PATH="$path"
+ break
+ fi
+ done
+ fi
+
+ if [[ -n "$SOURCE_PATH" ]]; then
+ # Extract the service block containing this path
+ SOURCE_BLOCK=$(awk -v target="$SOURCE_PATH" '
+ // { in_block=1; block="" }
+ in_block { block=block"\n"$0 }
+ /<\/service>/ {
+ if (in_block && block ~ target) {
+ print block
+ exit
+ }
+ in_block=0
+ }
+ ' "distro/debian/$PACKAGE/_service")
+
+ URL_PROTOCOL=$(echo "$SOURCE_BLOCK" | grep "protocol" | sed 's/.*\(.*\)<\/param>.*/\1/' | head -1)
+ URL_HOST=$(echo "$SOURCE_BLOCK" | grep "host" | sed 's/.*\(.*\)<\/param>.*/\1/' | head -1)
+ URL_PATH="$SOURCE_PATH"
+ fi
+
+ if [[ -n "$URL_PROTOCOL" && -n "$URL_HOST" && -n "$URL_PATH" ]]; then
+ SOURCE_URL="${URL_PROTOCOL}://${URL_HOST}${URL_PATH}"
+ echo " Downloading source from: $SOURCE_URL"
+
+ if wget -q -O "$TEMP_DIR/source-archive" "$SOURCE_URL"; then
+ cd "$TEMP_DIR"
+ if [[ "$SOURCE_URL" == *.tar.xz ]]; then
+ tar -xJf source-archive
+ elif [[ "$SOURCE_URL" == *.tar.gz ]] || [[ "$SOURCE_URL" == *.tgz ]]; then
+ tar -xzf source-archive
+ fi
+ # GitHub archives extract to DankMaterialShell-VERSION/ or similar
+ SOURCE_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
+ if [[ -z "$SOURCE_DIR" ]]; then
+ # Try to find any extracted directory
+ SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
+ fi
+ if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
+ echo "Error: Failed to extract source archive or find source directory"
+ echo "Contents of $TEMP_DIR:"
+ ls -la "$TEMP_DIR"
+ cd "$REPO_ROOT"
+ exit 1
+ fi
+ # Convert to absolute path
+ SOURCE_DIR=$(cd "$SOURCE_DIR" && pwd)
+ cd "$REPO_ROOT"
+ else
+ echo "Error: Failed to download source from $SOURCE_URL"
+ exit 1
+ fi
+ fi
+ fi
+ fi
+
+ if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
+ echo "Error: Could not determine or obtain source for $PACKAGE (OpenSUSE-only upload)"
+ echo "SOURCE_DIR: $SOURCE_DIR"
+ if [[ -d "$TEMP_DIR" ]]; then
+ echo "Contents of temp directory:"
+ ls -la "$TEMP_DIR"
+ fi
+ exit 1
+ fi
+
+ echo " Found source directory: $SOURCE_DIR"
+ fi
+ echo " - Creating OpenSUSE-compatible source tarballs"
+
+ # Extract Source0 from spec file
+ SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1); if [[ -z "$SOURCE0" && "$PACKAGE" == "dms-git" ]]; then SOURCE0="dms-git-source.tar.gz"; fi
+
+ if [[ -n "$SOURCE0" ]]; then
+ # Create a separate temporary directory for OpenSUSE tarball creation to avoid conflicts
+ OBS_TARBALL_DIR=$(mktemp -d -t obs-tarball-XXXXXX)
+ cd "$OBS_TARBALL_DIR"
+
+ case "$PACKAGE" in
+ dms)
+ # dms spec expects DankMaterialShell-%{version} directory (from %setup -q -n DankMaterialShell-%{version})
+ # Extract version from spec file
+ DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
+ EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
+ echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
+ cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
+ if [[ "$SOURCE0" == *.tar.xz ]]; then
+ tar -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
+ elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
+ tar -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
+ else
+ tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
+ fi
+ rm -rf "$EXPECTED_DIR"
+ echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
+ ;;
+ dms-git)
+ # dms-git spec expects dms-git-source directory (from %setup -q -n dms-git-source)
+ EXPECTED_DIR="dms-git-source"
+ echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
+ cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
+ if [[ "$SOURCE0" == *.tar.xz ]]; then
+ tar -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
+ elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
+ tar -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
+ else
+ tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
+ fi
+ rm -rf "$EXPECTED_DIR"
+ echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
+ ;;
+ *)
+ # Generic handling
+ DIR_NAME=$(basename "$SOURCE_DIR")
+ echo " Creating $SOURCE0 (directory: $DIR_NAME)"
+ cp -r "$SOURCE_DIR" "$DIR_NAME"
+ if [[ "$SOURCE0" == *.tar.xz ]]; then
+ tar -cJf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
+ elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
+ tar -cjf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
+ else
+ tar -czf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
+ fi
+ rm -rf "$DIR_NAME"
+ echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
+ ;;
+ esac
+ # Clean up the tarball work directory
+ cd "$REPO_ROOT"
+ rm -rf "$OBS_TARBALL_DIR"
+ echo " - OpenSUSE source tarballs created"
+ fi
+
+ # Copy spec file
+ cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
+ fi
+
+ # Copy debian/ directory into source (for Debian builds only)
+ if [[ "$UPLOAD_DEBIAN" == true ]]; then
+ echo " Copying debian/ directory into source"
+ cp -r "distro/debian/$PACKAGE/debian" "$SOURCE_DIR/"
+
+ # Create combined tarball
+ echo " Creating combined tarball: $COMBINED_TARBALL"
+ cd "$(dirname "$SOURCE_DIR")"
+ TARBALL_BASE=$(basename "$SOURCE_DIR")
+ tar -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
+ cd "$REPO_ROOT"
+
+ # Generate .dsc file for native format
+ TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
+ TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
+
+ # Extract Build-Depends from control file
+ BUILD_DEPS="debhelper-compat (= 13)"
+ if [[ -f "distro/debian/$PACKAGE/debian/control" ]]; then
+ CONTROL_DEPS=$(sed -n '/^Build-Depends:/,/^[A-Z]/p' "distro/debian/$PACKAGE/debian/control" | \
+ sed '/^Build-Depends:/s/^Build-Depends: *//' | \
+ sed '/^[A-Z]/d' | \
+ tr '\n' ' ' | \
+ sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[[:space:]]\+/ /g')
+ if [[ -n "$CONTROL_DEPS" && "$CONTROL_DEPS" != "" ]]; then
+ BUILD_DEPS="$CONTROL_DEPS"
+ fi
+ fi
+
+ cat > "$WORK_DIR/$PACKAGE.dsc" << EOF
+Format: 3.0 (native)
+Source: $PACKAGE
+Binary: $PACKAGE
+Architecture: any
+Version: $VERSION
+Maintainer: Avenge Media
+Build-Depends: $BUILD_DEPS
+Files:
+ $TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL
+EOF
+
+ echo " - Generated $PACKAGE.dsc for native format"
+ fi
+ else
+ # Quilt format (legacy) - for Debian only
+ if [[ "$UPLOAD_DEBIAN" == true ]]; then
+ # For quilt format, version can have revision
+ if [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
+ VERSION="$CHANGELOG_VERSION"
+ else
+ VERSION="${CHANGELOG_VERSION}-1"
+ fi
+
+ echo " - Quilt format detected: creating debian.tar.gz"
+ tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
+
+ echo " - Generating $PACKAGE.dsc for quilt format"
+ cat > "$WORK_DIR/$PACKAGE.dsc" << EOF
+Format: 3.0 (quilt)
+Source: $PACKAGE
+Binary: $PACKAGE
+Architecture: any
+Version: $VERSION
+Maintainer: Avenge Media
+Build-Depends: debhelper-compat (= 13), wget, gzip
+DEBTRANSFORM-TAR: debian.tar.gz
+Files:
+ 00000000000000000000000000000000 1 debian.tar.gz
+EOF
+ fi
+ fi
+fi
+
+# Change to working directory and commit
+cd "$WORK_DIR"
+
+echo "==> Staging changes"
+# List files to be uploaded
+echo "Files to upload:"
+# Only list files relevant to the selected upload type
+if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$UPLOAD_OPENSUSE" == true ]]; then
+ ls -lh *.tar.gz *.tar.xz *.tar *.spec *.dsc _service 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
+elif [[ "$UPLOAD_DEBIAN" == true ]]; then
+ ls -lh *.tar.gz *.dsc _service 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
+elif [[ "$UPLOAD_OPENSUSE" == true ]]; then
+ ls -lh *.tar.gz *.tar.xz *.tar *.spec _service 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
+fi
+echo ""
+
+osc addremove
+
+echo "==> Committing to OBS"
+osc commit -m "$MESSAGE"
+
+echo "==> Checking build status"
+osc results
+
+echo ""
+echo "Upload complete! Monitor builds with:"
+echo " cd $WORK_DIR && osc results"
+echo " cd $WORK_DIR && osc buildlog "
+echo ""
+
+# Don't cleanup - keep checkout for status checking
+echo ""
+echo "Upload complete! Build status:"
+cd "$WORK_DIR"
+osc results 2>&1 | head -10
+cd "$REPO_ROOT"
+
+echo ""
+echo "To check detailed status:"
+echo " cd $WORK_DIR && osc results"
+echo " cd $WORK_DIR && osc remotebuildlog $OBS_PROJECT $PACKAGE Debian_13 x86_64"
+echo ""
+echo "NOTE: Checkout kept at $WORK_DIR for status checking"
+echo ""
+echo "✅ Upload complete!"
+echo ""
+echo "Check build status with:"
+echo " ./distro/scripts/obs-status.sh $PACKAGE"
diff --git a/distro/scripts/test-packaging.sh b/distro/scripts/test-packaging.sh
new file mode 100755
index 00000000..05ac7568
--- /dev/null
+++ b/distro/scripts/test-packaging.sh
@@ -0,0 +1,169 @@
+#!/bin/bash
+# Manual testing script for DMS packaging
+# Tests OBS (Debian/openSUSE) and PPA (Ubuntu) workflows
+# Usage: ./distro/test-packaging.sh [obs|ppa|all]
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+DISTRO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+REPO_ROOT="$(cd "$DISTRO_DIR/.." && pwd)"
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+info() { echo -e "${BLUE}[INFO]${NC} $1"; }
+success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
+error() { echo -e "${RED}[ERROR]${NC} $1"; }
+warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+
+TEST_MODE="${1:-all}"
+
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "DMS Packaging Test Suite"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo ""
+
+# Test 1: OBS Upload (Debian + openSUSE)
+if [[ "$TEST_MODE" == "obs" ]] || [[ "$TEST_MODE" == "all" ]]; then
+ echo "═══════════════════════════════════════════════════════════════════"
+ echo "TEST 1: OBS Upload (Debian + openSUSE)"
+ echo "═══════════════════════════════════════════════════════════════════"
+ echo ""
+
+ OBS_SCRIPT="$SCRIPT_DIR/obs-upload.sh"
+
+ if [[ ! -f "$OBS_SCRIPT" ]]; then
+ error "OBS script not found: $OBS_SCRIPT"
+ exit 1
+ fi
+
+ info "OBS script location: $OBS_SCRIPT"
+ info "Available packages: dms, dms-git"
+ echo ""
+
+ warn "This will upload to OBS (home:AvengeMedia)"
+ read -p "Continue with OBS test? [y/N] " -n 1 -r
+ echo
+
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ info "Select package to test:"
+ echo " 1. dms (stable)"
+ echo " 2. dms-git (nightly)"
+ echo " 3. all (both packages)"
+ read -p "Choice [1]: " -n 1 -r PKG_CHOICE
+ echo
+ echo ""
+
+ PKG_CHOICE="${PKG_CHOICE:-1}"
+
+ cd "$REPO_ROOT"
+
+ case "$PKG_CHOICE" in
+ 1)
+ info "Testing OBS upload for 'dms' package..."
+ bash "$OBS_SCRIPT" dms "Test packaging update"
+ ;;
+ 2)
+ info "Testing OBS upload for 'dms-git' package..."
+ bash "$OBS_SCRIPT" dms-git "Test packaging update"
+ ;;
+ 3)
+ info "Testing OBS upload for all packages..."
+ bash "$OBS_SCRIPT" all "Test packaging update"
+ ;;
+ *)
+ error "Invalid choice"
+ exit 1
+ ;;
+ esac
+
+ echo ""
+ success "OBS test completed"
+ echo ""
+ info "Check build status: https://build.opensuse.org/project/monitor/home:AvengeMedia"
+ else
+ warn "OBS test skipped"
+ fi
+
+ echo ""
+fi
+
+# Test 2: PPA Upload (Ubuntu)
+if [[ "$TEST_MODE" == "ppa" ]] || [[ "$TEST_MODE" == "all" ]]; then
+ echo "═══════════════════════════════════════════════════════════════════"
+ echo "TEST 2: PPA Upload (Ubuntu)"
+ echo "═══════════════════════════════════════════════════════════════════"
+ echo ""
+
+ PPA_SCRIPT="$DISTRO_DIR/ubuntu/ppa/create-and-upload.sh"
+
+ if [[ ! -f "$PPA_SCRIPT" ]]; then
+ error "PPA script not found: $PPA_SCRIPT"
+ exit 1
+ fi
+
+ info "PPA script location: $PPA_SCRIPT"
+ info "Available PPAs: dms, dms-git"
+ info "Ubuntu series: questing (25.10)"
+ echo ""
+
+ warn "This will upload to Launchpad PPA (ppa:avengemedia/dms)"
+ read -p "Continue with PPA test? [y/N] " -n 1 -r
+ echo
+
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ info "Select package to test:"
+ echo " 1. dms (stable)"
+ echo " 2. dms-git (nightly)"
+ read -p "Choice [1]: " -n 1 -r PKG_CHOICE
+ echo
+ echo ""
+
+ PKG_CHOICE="${PKG_CHOICE:-1}"
+
+ case "$PKG_CHOICE" in
+ 1)
+ info "Testing PPA upload for 'dms' package..."
+ DMS_PKG="$DISTRO_DIR/ubuntu/dms"
+ PPA_NAME="dms"
+ ;;
+ 2)
+ info "Testing PPA upload for 'dms-git' package..."
+ DMS_PKG="$DISTRO_DIR/ubuntu/dms-git"
+ PPA_NAME="dms-git"
+ ;;
+ *)
+ error "Invalid choice"
+ exit 1
+ ;;
+ esac
+
+ echo ""
+
+ if [[ ! -d "$DMS_PKG" ]]; then
+ error "DMS package directory not found: $DMS_PKG"
+ exit 1
+ fi
+
+ bash "$PPA_SCRIPT" "$DMS_PKG" "$PPA_NAME" questing
+
+ echo ""
+ success "PPA test completed"
+ echo ""
+ info "Check build status: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages"
+ else
+ warn "PPA test skipped"
+ fi
+
+ echo ""
+fi
+
+# Summary
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "Testing Summary"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
diff --git a/distro/ubuntu/danklinux/danksearch/debian/changelog b/distro/ubuntu/danklinux/danksearch/debian/changelog
new file mode 100644
index 00000000..10746dea
--- /dev/null
+++ b/distro/ubuntu/danklinux/danksearch/debian/changelog
@@ -0,0 +1,5 @@
+danksearch (0.0.7ppa3) questing; urgency=medium
+
+ * Rebuild for packaging fixes (ppa3)
+
+ -- Avenge Media Fri, 21 Nov 2025 14:19:58 -0500
diff --git a/distro/ubuntu/danklinux/danksearch/debian/control b/distro/ubuntu/danklinux/danksearch/debian/control
new file mode 100644
index 00000000..03975eac
--- /dev/null
+++ b/distro/ubuntu/danklinux/danksearch/debian/control
@@ -0,0 +1,24 @@
+Source: danksearch
+Section: utils
+Priority: optional
+Maintainer: Avenge Media
+Build-Depends: debhelper-compat (= 13)
+Standards-Version: 4.6.2
+Homepage: https://github.com/AvengeMedia/danksearch
+Vcs-Browser: https://github.com/AvengeMedia/danksearch
+Vcs-Git: https://github.com/AvengeMedia/danksearch.git
+
+Package: danksearch
+Architecture: amd64 arm64
+Depends: ${misc:Depends}
+Description: Fast file search utility for DMS
+ DankSearch is a fast file search utility designed for DankMaterialShell.
+ It provides efficient file and content search capabilities with minimal
+ dependencies. This package contains the pre-built binary from the official
+ GitHub release.
+ .
+ Features include:
+ - Fast file searching
+ - Lightweight and efficient
+ - Designed for DMS integration
+ - Minimal resource usage
diff --git a/distro/ubuntu/danklinux/danksearch/debian/copyright b/distro/ubuntu/danklinux/danksearch/debian/copyright
new file mode 100644
index 00000000..7c832574
--- /dev/null
+++ b/distro/ubuntu/danklinux/danksearch/debian/copyright
@@ -0,0 +1,24 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: danksearch
+Upstream-Contact: Avenge Media LLC
+Source: https://github.com/AvengeMedia/danksearch
+
+Files: *
+Copyright: 2025 Avenge Media LLC
+License: GPL-3.0-only
+
+License: GPL-3.0-only
+ This package is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License version 3 as
+ published by the Free Software Foundation.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
diff --git a/distro/ubuntu/danklinux/danksearch/debian/files b/distro/ubuntu/danklinux/danksearch/debian/files
new file mode 100644
index 00000000..ac634a6d
--- /dev/null
+++ b/distro/ubuntu/danklinux/danksearch/debian/files
@@ -0,0 +1 @@
+danksearch_0.0.7ppa3_source.buildinfo utils optional
diff --git a/distro/ubuntu/danklinux/danksearch/debian/rules b/distro/ubuntu/danklinux/danksearch/debian/rules
new file mode 100755
index 00000000..a9c821e3
--- /dev/null
+++ b/distro/ubuntu/danklinux/danksearch/debian/rules
@@ -0,0 +1,33 @@
+#!/usr/bin/make -f
+
+export DH_VERBOSE = 1
+
+# Detect architecture for selecting correct binary
+DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
+
+# Map Debian arch to binary filename
+ifeq ($(DEB_HOST_ARCH),amd64)
+ BINARY_FILE := dsearch-amd64
+else ifeq ($(DEB_HOST_ARCH),arm64)
+ BINARY_FILE := dsearch-arm64
+else
+ $(error Unsupported architecture: $(DEB_HOST_ARCH))
+endif
+
+%:
+ dh $@
+
+override_dh_auto_build:
+ # Binary is already included in source package (native format)
+ # Downloaded by build-source.sh before upload
+ # Just verify it exists and is executable
+ test -f $(BINARY_FILE) || (echo "ERROR: $(BINARY_FILE) not found!" && exit 1)
+ chmod +x $(BINARY_FILE)
+
+override_dh_auto_install:
+ # Install binary as danksearch
+ install -Dm755 $(BINARY_FILE) debian/danksearch/usr/bin/danksearch
+
+override_dh_auto_clean:
+ # Don't delete binaries - they're part of the source package (native format)
+ dh_auto_clean
diff --git a/distro/ubuntu/danklinux/danksearch/debian/source/format b/distro/ubuntu/danklinux/danksearch/debian/source/format
new file mode 100644
index 00000000..89ae9db8
--- /dev/null
+++ b/distro/ubuntu/danklinux/danksearch/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/distro/ubuntu/danklinux/danksearch/dsearch-amd64 b/distro/ubuntu/danklinux/danksearch/dsearch-amd64
new file mode 100755
index 00000000..e66bcbf7
Binary files /dev/null and b/distro/ubuntu/danklinux/danksearch/dsearch-amd64 differ
diff --git a/distro/ubuntu/danklinux/danksearch/dsearch-arm64 b/distro/ubuntu/danklinux/danksearch/dsearch-arm64
new file mode 100755
index 00000000..fd6d1d66
Binary files /dev/null and b/distro/ubuntu/danklinux/danksearch/dsearch-arm64 differ
diff --git a/distro/ubuntu/danklinux/dgop/debian/changelog b/distro/ubuntu/danklinux/dgop/debian/changelog
new file mode 100644
index 00000000..43632cd9
--- /dev/null
+++ b/distro/ubuntu/danklinux/dgop/debian/changelog
@@ -0,0 +1,9 @@
+dgop (0.1.11ppa2) questing; urgency=medium
+
+ * Rebuild for Questing (25.10) - Ubuntu 25.10+ only
+ * Stateless CPU/GPU monitoring tool
+ * Support for NVIDIA and AMD GPUs
+ * JSON output for integration
+ * Pre-built binary package for amd64 and arm64
+
+ -- Avenge Media Sun, 16 Nov 2025 22:50:00 -0500
diff --git a/distro/ubuntu/danklinux/dgop/debian/control b/distro/ubuntu/danklinux/dgop/debian/control
new file mode 100644
index 00000000..f1dd4614
--- /dev/null
+++ b/distro/ubuntu/danklinux/dgop/debian/control
@@ -0,0 +1,27 @@
+Source: dgop
+Section: utils
+Priority: optional
+Maintainer: Avenge Media
+Build-Depends: debhelper-compat (= 13),
+ wget,
+ gzip
+Standards-Version: 4.6.2
+Homepage: https://github.com/AvengeMedia/dgop
+Vcs-Browser: https://github.com/AvengeMedia/dgop
+Vcs-Git: https://github.com/AvengeMedia/dgop.git
+
+Package: dgop
+Architecture: amd64 arm64
+Depends: ${misc:Depends}
+Description: Stateless CPU/GPU monitor for DankMaterialShell
+ DGOP is a stateless system monitoring tool that provides CPU, GPU,
+ memory, and network statistics. Designed for integration with
+ DankMaterialShell but can be used standalone.
+ .
+ Features:
+ - CPU usage monitoring
+ - GPU usage and temperature (NVIDIA, AMD)
+ - Memory and swap statistics
+ - Network traffic monitoring
+ - Zero-state design (no background processes)
+ - JSON output for easy integration
diff --git a/distro/ubuntu/danklinux/dgop/debian/copyright b/distro/ubuntu/danklinux/dgop/debian/copyright
new file mode 100644
index 00000000..f1f3b2dd
--- /dev/null
+++ b/distro/ubuntu/danklinux/dgop/debian/copyright
@@ -0,0 +1,27 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: dgop
+Upstream-Contact: Avenge Media LLC
+Source: https://github.com/AvengeMedia/dgop
+
+Files: *
+Copyright: 2025 Avenge Media LLC
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/distro/ubuntu/danklinux/dgop/debian/files b/distro/ubuntu/danklinux/dgop/debian/files
new file mode 100644
index 00000000..d4bb33d1
--- /dev/null
+++ b/distro/ubuntu/danklinux/dgop/debian/files
@@ -0,0 +1 @@
+dgop_0.1.11ppa2_source.buildinfo utils optional
diff --git a/distro/ubuntu/danklinux/dgop/debian/rules b/distro/ubuntu/danklinux/dgop/debian/rules
new file mode 100755
index 00000000..0af8b692
--- /dev/null
+++ b/distro/ubuntu/danklinux/dgop/debian/rules
@@ -0,0 +1,38 @@
+#!/usr/bin/make -f
+
+export DH_VERBOSE = 1
+
+# Extract version from debian/changelog
+DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
+# Get upstream version (strip -1ppa1 suffix)
+UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
+
+# Detect architecture for downloading correct binary
+DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
+
+# Map Debian arch to GitHub release arch names
+ifeq ($(DEB_HOST_ARCH),amd64)
+ GITHUB_ARCH := amd64
+else ifeq ($(DEB_HOST_ARCH),arm64)
+ GITHUB_ARCH := arm64
+else
+ $(error Unsupported architecture: $(DEB_HOST_ARCH))
+endif
+
+%:
+ dh $@
+
+override_dh_auto_build:
+ # Binary is already included in source package (native format)
+ # Just verify it exists and is executable
+ test -f dgop || (echo "ERROR: dgop binary not found!" && exit 1)
+ chmod +x dgop
+
+override_dh_auto_install:
+ # Install binary
+ install -Dm755 dgop debian/dgop/usr/bin/dgop
+
+override_dh_auto_clean:
+ # Don't delete dgop binary - it's part of the source package (native format)
+ rm -f dgop.gz
+ dh_auto_clean
diff --git a/distro/ubuntu/danklinux/dgop/debian/source/format b/distro/ubuntu/danklinux/dgop/debian/source/format
new file mode 100644
index 00000000..89ae9db8
--- /dev/null
+++ b/distro/ubuntu/danklinux/dgop/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/distro/ubuntu/dms-git/debian/changelog b/distro/ubuntu/dms-git/debian/changelog
new file mode 100644
index 00000000..6ed3a3ed
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/changelog
@@ -0,0 +1,5 @@
+dms-git (0.6.2+git2094.6cc6e7c8ppa1) questing; urgency=medium
+
+ * Git snapshot (commit 2094: 6cc6e7c8)
+
+ -- Avenge Media Sun, 23 Nov 2025 00:43:28 -0500
diff --git a/distro/ubuntu/dms-git/debian/control b/distro/ubuntu/dms-git/debian/control
new file mode 100644
index 00000000..e974f8c5
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/control
@@ -0,0 +1,50 @@
+Source: dms-git
+Section: x11
+Priority: optional
+Maintainer: Avenge Media
+Build-Depends: debhelper-compat (= 13)
+Standards-Version: 4.6.2
+Homepage: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
+
+Package: dms-git
+Architecture: amd64
+Depends: ${misc:Depends},
+ quickshell-git | quickshell,
+ accountsservice,
+ cava,
+ cliphist,
+ danksearch,
+ dgop,
+ matugen,
+ qml6-module-qtcore,
+ qml6-module-qtmultimedia,
+ qml6-module-qtqml,
+ qml6-module-qtquick,
+ qml6-module-qtquick-controls,
+ qml6-module-qtquick-dialogs,
+ qml6-module-qtquick-effects,
+ qml6-module-qtquick-layouts,
+ qml6-module-qtquick-templates,
+ qml6-module-qtquick-window,
+ qt6ct,
+ wl-clipboard
+Provides: dms
+Conflicts: dms
+Replaces: dms
+Description: DankMaterialShell - Modern Wayland Desktop Shell (git nightly)
+ DMS (DankMaterialShell) is a feature-rich desktop shell built on
+ Quickshell, providing a modern and customizable user interface for
+ Wayland compositors like niri, hyprland, and sway.
+ .
+ This is the nightly/git version built from the latest master branch.
+ .
+ Features include:
+ - Material Design inspired UI
+ - Customizable themes and appearance
+ - Built-in application launcher
+ - System tray and notifications
+ - Network and Bluetooth management
+ - Audio controls
+ - Systemd integration
diff --git a/distro/ubuntu/dms-git/debian/copyright b/distro/ubuntu/dms-git/debian/copyright
new file mode 100644
index 00000000..1e7bce73
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/copyright
@@ -0,0 +1,27 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: dms
+Upstream-Contact: Avenge Media LLC
+Source: https://github.com/AvengeMedia/DankMaterialShell
+
+Files: *
+Copyright: 2025 Avenge Media LLC
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/distro/ubuntu/dms-git/debian/files b/distro/ubuntu/dms-git/debian/files
new file mode 100644
index 00000000..10d74fb1
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/files
@@ -0,0 +1 @@
+dms-git_0.6.2+git2094.6cc6e7c8ppa1_source.buildinfo x11 optional
diff --git a/distro/ubuntu/dms-git/debian/rules b/distro/ubuntu/dms-git/debian/rules
new file mode 100755
index 00000000..9cd337c6
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/rules
@@ -0,0 +1,45 @@
+#!/usr/bin/make -f
+
+export DH_VERBOSE = 1
+
+# Get git commit date for version
+GIT_DATE := $(shell date +%Y%m%d)
+GIT_COMMIT := HEAD
+
+%:
+ dh $@
+
+override_dh_auto_build:
+ # Git source is already included in source package (cloned by build-source.sh)
+ # Launchpad build environment has no internet access
+ test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1)
+ test -f dms-distropkg-amd64.gz || (echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1)
+
+ # Extract pre-built binary from latest release
+ # Note: For git versions, we use the latest release binary
+ # The QML files come from git master
+ gunzip -c dms-distropkg-amd64.gz > dms
+ chmod +x dms
+
+override_dh_auto_install:
+ # Install binary
+ install -Dm755 dms debian/dms-git/usr/bin/dms
+
+ # Install QML files from git clone
+ mkdir -p debian/dms-git/usr/share/quickshell/dms
+ cp -r dms-git-repo/* debian/dms-git/usr/share/quickshell/dms/
+
+ # Remove unnecessary directories
+ rm -rf debian/dms-git/usr/share/quickshell/dms/core
+ rm -rf debian/dms-git/usr/share/quickshell/dms/distro
+
+ # Install systemd user service
+ install -Dm644 dms-git-repo/quickshell/assets/systemd/dms.service \
+ debian/dms-git/usr/lib/systemd/user/dms.service
+
+override_dh_auto_clean:
+ # Don't delete dms-git-repo directory - it's part of the source package (native format)
+ # Clean up build artifacts (but keep dms-distropkg-amd64.gz for Launchpad)
+ rm -f dms
+ # Don't remove dms-distropkg-amd64.gz - it needs to be included in the source package for Launchpad builds
+ dh_auto_clean
diff --git a/distro/ubuntu/dms-git/debian/source/format b/distro/ubuntu/dms-git/debian/source/format
new file mode 100644
index 00000000..89ae9db8
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/distro/ubuntu/dms-git/debian/source/include-binaries b/distro/ubuntu/dms-git/debian/source/include-binaries
new file mode 100644
index 00000000..77d7423a
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/source/include-binaries
@@ -0,0 +1 @@
+dms-distropkg-amd64.gz
diff --git a/distro/ubuntu/dms-git/debian/source/options b/distro/ubuntu/dms-git/debian/source/options
new file mode 100644
index 00000000..dc3f6a5a
--- /dev/null
+++ b/distro/ubuntu/dms-git/debian/source/options
@@ -0,0 +1,4 @@
+# Include files that are normally excluded by .gitignore
+# These are needed for the build process on Launchpad (which has no internet access)
+tar-ignore = !dms-distropkg-amd64.gz
+tar-ignore = !dms-git-repo
diff --git a/distro/ubuntu/dms/debian/changelog b/distro/ubuntu/dms/debian/changelog
new file mode 100644
index 00000000..634af4eb
--- /dev/null
+++ b/distro/ubuntu/dms/debian/changelog
@@ -0,0 +1,5 @@
+dms (0.6.2ppa3) questing; urgency=medium
+
+ * Rebuild for packaging fixes (ppa3)
+
+ -- Avenge Media Sun, 23 Nov 2025 00:40:41 -0500
diff --git a/distro/ubuntu/dms/debian/control b/distro/ubuntu/dms/debian/control
new file mode 100644
index 00000000..91e63601
--- /dev/null
+++ b/distro/ubuntu/dms/debian/control
@@ -0,0 +1,47 @@
+Source: dms
+Section: x11
+Priority: optional
+Maintainer: Avenge Media
+Build-Depends: debhelper-compat (= 13)
+Standards-Version: 4.6.2
+Homepage: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
+Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
+
+Package: dms
+Architecture: amd64
+Depends: ${misc:Depends},
+ quickshell-git | quickshell,
+ accountsservice,
+ cava,
+ cliphist,
+ danksearch,
+ dgop,
+ matugen,
+ qml6-module-qtcore,
+ qml6-module-qtmultimedia,
+ qml6-module-qtqml,
+ qml6-module-qtquick,
+ qml6-module-qtquick-controls,
+ qml6-module-qtquick-dialogs,
+ qml6-module-qtquick-effects,
+ qml6-module-qtquick-layouts,
+ qml6-module-qtquick-templates,
+ qml6-module-qtquick-window,
+ qt6ct,
+ wl-clipboard
+Conflicts: dms-git
+Replaces: dms-git
+Description: DankMaterialShell - Modern Wayland Desktop Shell
+ DMS (DankMaterialShell) is a feature-rich desktop shell built on
+ Quickshell, providing a modern and customizable user interface for
+ Wayland compositors like niri, hyprland, and sway.
+ .
+ Features include:
+ - Material Design inspired UI
+ - Customizable themes and appearance
+ - Built-in application launcher
+ - System tray and notifications
+ - Network and Bluetooth management
+ - Audio controls
+ - Systemd integration
diff --git a/distro/ubuntu/dms/debian/copyright b/distro/ubuntu/dms/debian/copyright
new file mode 100644
index 00000000..1e7bce73
--- /dev/null
+++ b/distro/ubuntu/dms/debian/copyright
@@ -0,0 +1,27 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: dms
+Upstream-Contact: Avenge Media LLC
+Source: https://github.com/AvengeMedia/DankMaterialShell
+
+Files: *
+Copyright: 2025 Avenge Media LLC
+License: MIT
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
diff --git a/distro/ubuntu/dms/debian/files b/distro/ubuntu/dms/debian/files
new file mode 100644
index 00000000..d527f9a1
--- /dev/null
+++ b/distro/ubuntu/dms/debian/files
@@ -0,0 +1 @@
+dms_0.6.2ppa3_source.buildinfo x11 optional
diff --git a/distro/ubuntu/dms/debian/rules b/distro/ubuntu/dms/debian/rules
new file mode 100755
index 00000000..73c3ce04
--- /dev/null
+++ b/distro/ubuntu/dms/debian/rules
@@ -0,0 +1,63 @@
+#!/usr/bin/make -f
+
+export DH_VERBOSE = 1
+
+# Extract version from debian/changelog
+DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
+# Get upstream version (strip -1ppa1 suffix)
+UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
+# Strip ppa suffix and handle git versions
+# Examples: 0.5.2ppa9 -> 0.5.2, 0.5.2+git20251116 -> 0.5.2
+BASE_VERSION := $(shell echo $(UPSTREAM_VERSION) | sed 's/ppa[0-9]*$$//' | sed 's/+git.*//')
+
+%:
+ dh $@
+
+override_dh_auto_build:
+ # All files are included in source package (downloaded by build-source.sh)
+ # Launchpad build environment has no internet access
+ test -f dms-distropkg-amd64.gz || (echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1)
+ test -f dms-source.tar.gz || (echo "ERROR: dms-source.tar.gz not found!" && exit 1)
+
+ # Extract pre-built binary
+ gunzip -c dms-distropkg-amd64.gz > dms
+ chmod +x dms
+
+ # Extract source tarball for QML files
+ tar -xzf dms-source.tar.gz
+ # Find the extracted directory (it might have various names)
+ # and create a symlink to expected name for consistent install
+ SOURCE_DIR=$$(find . -maxdepth 1 -type d -name "DankMaterialShell*" | head -n1); \
+ if [ -n "$$SOURCE_DIR" ]; then \
+ ln -sf $$SOURCE_DIR DankMaterialShell-$(BASE_VERSION); \
+ fi
+
+override_dh_auto_install:
+ # Install binary
+ install -Dm755 dms debian/dms/usr/bin/dms
+
+ # Install QML files from source tarball
+ mkdir -p debian/dms/usr/share/quickshell/dms
+ cp -r DankMaterialShell-$(BASE_VERSION)/* debian/dms/usr/share/quickshell/dms/
+
+ # Remove unnecessary directories
+ rm -rf debian/dms/usr/share/quickshell/dms/core
+ rm -rf debian/dms/usr/share/quickshell/dms/distro
+
+ # Install systemd user service
+ install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/assets/systemd/dms.service \
+ debian/dms/usr/lib/systemd/user/dms.service
+
+ # Generate and install shell completions (if applicable)
+ # Uncomment if dms supports completion generation
+ # ./dms completion bash > dms.bash
+ # ./dms completion zsh > dms.zsh
+ # install -Dm644 dms.bash debian/dms/usr/share/bash-completion/completions/dms
+ # install -Dm644 dms.zsh debian/dms/usr/share/zsh/vendor-completions/_dms
+
+override_dh_auto_clean:
+ rm -f dms
+ rm -rf DankMaterialShell-*
+ # Don't remove dms-distropkg-amd64.gz and dms-source.tar.gz
+ # They need to be included in the source package for Launchpad builds
+ dh_auto_clean
diff --git a/distro/ubuntu/dms/debian/source/format b/distro/ubuntu/dms/debian/source/format
new file mode 100644
index 00000000..89ae9db8
--- /dev/null
+++ b/distro/ubuntu/dms/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/distro/ubuntu/dms/debian/source/include-binaries b/distro/ubuntu/dms/debian/source/include-binaries
new file mode 100644
index 00000000..bac25ab7
--- /dev/null
+++ b/distro/ubuntu/dms/debian/source/include-binaries
@@ -0,0 +1,2 @@
+dms-distropkg-amd64.gz
+dms-source.tar.gz
diff --git a/distro/ubuntu/dms/debian/source/options b/distro/ubuntu/dms/debian/source/options
new file mode 100644
index 00000000..ca1d9b05
--- /dev/null
+++ b/distro/ubuntu/dms/debian/source/options
@@ -0,0 +1,4 @@
+# Include files that are normally excluded by .gitignore
+# These are needed for the build process on Launchpad (which has no internet access)
+tar-ignore = !dms-distropkg-amd64.gz
+tar-ignore = !dms-source.tar.gz
diff --git a/distro/ubuntu/dput.cf.template b/distro/ubuntu/dput.cf.template
new file mode 100644
index 00000000..275cc625
--- /dev/null
+++ b/distro/ubuntu/dput.cf.template
@@ -0,0 +1,44 @@
+# dput configuration for AvengeMedia DMS PPAs
+# Copy this to ~/.dput.cf (or merge with existing ~/.dput.cf)
+#
+# Usage:
+# dput ppa:avengemedia/dms ../package_version_source.changes
+# dput ppa:avengemedia/dms-git ../package_version_source.changes
+
+# Stable DMS PPA - for release versions
+[ppa:avengemedia/dms]
+fqdn = ppa.launchpad.net
+method = ftp
+incoming = ~avengemedia/ubuntu/dms/
+login = anonymous
+allow_unsigned_uploads = 0
+
+# Nightly/Git DMS PPA - for development builds
+[ppa:avengemedia/dms-git]
+fqdn = ppa.launchpad.net
+method = ftp
+incoming = ~avengemedia/ubuntu/dms-git/
+login = anonymous
+allow_unsigned_uploads = 0
+
+# Alternative: Use HTTPS instead of FTP (more reliable through firewalls)
+# Uncomment these if FTP doesn't work:
+#
+# [ppa:avengemedia/dms-https]
+# fqdn = ppa.launchpad.net
+# method = https
+# incoming = ~avengemedia/ubuntu/dms/
+# login = anonymous
+# allow_unsigned_uploads = 0
+#
+# [ppa:avengemedia/dms-git-https]
+# fqdn = ppa.launchpad.net
+# method = https
+# incoming = ~avengemedia/ubuntu/dms-git/
+# login = anonymous
+# allow_unsigned_uploads = 0
+
+# Notes:
+# - allow_unsigned_uploads = 0 enforces GPG signing (required by Launchpad)
+# - anonymous login is standard for PPA uploads
+# - The incoming path must match your Launchpad username and PPA name
diff --git a/distro/ubuntu/ppa/create-and-upload.sh b/distro/ubuntu/ppa/create-and-upload.sh
new file mode 100755
index 00000000..1169d9d2
--- /dev/null
+++ b/distro/ubuntu/ppa/create-and-upload.sh
@@ -0,0 +1,246 @@
+#!/bin/bash
+# Build and upload PPA package with automatic cleanup
+# Usage: ./create-and-upload.sh [ubuntu-series] [--keep-builds]
+#
+# Example:
+# ./create-and-upload.sh ../dms dms questing
+# ./create-and-upload.sh ../danklinux/dgop danklinux questing --keep-builds
+
+set -e
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+info() { echo -e "${BLUE}[INFO]${NC} $1"; }
+success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
+warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+error() { echo -e "${RED}[ERROR]${NC} $1"; }
+
+# Parse arguments
+KEEP_BUILDS=false
+ARGS=()
+for arg in "$@"; do
+ if [ "$arg" = "--keep-builds" ]; then
+ KEEP_BUILDS=true
+ else
+ ARGS+=("$arg")
+ fi
+done
+
+if [ ${#ARGS[@]} -lt 2 ]; then
+ error "Usage: $0 [ubuntu-series] [--keep-builds]"
+ echo
+ echo "Arguments:"
+ echo " package-dir : Path to package directory (e.g., ../dms, ../danklinux/dgop)"
+ echo " ppa-name : PPA name (danklinux, dms, dms-git)"
+ echo " ubuntu-series : Ubuntu series (optional, default: questing)"
+ echo " Supported: questing (25.10) and newer only"
+ echo " Note: Requires Qt 6.6+ (quickshell requirement)"
+ echo " --keep-builds : Keep build artifacts after upload (optional)"
+ echo
+ echo "Examples:"
+ echo " $0 ../dms dms questing"
+ echo " $0 ../danklinux/dgop danklinux questing --keep-builds"
+ echo " $0 ../dms-git dms-git # Defaults to questing"
+ exit 1
+fi
+
+PACKAGE_DIR="${ARGS[0]}"
+PPA_NAME="${ARGS[1]}"
+UBUNTU_SERIES="${ARGS[2]:-questing}"
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+BUILD_SCRIPT="$SCRIPT_DIR/create-source.sh"
+UPLOAD_SCRIPT="$SCRIPT_DIR/upload-ppa.sh"
+
+# Validate scripts exist
+if [ ! -f "$BUILD_SCRIPT" ]; then
+ error "Build script not found: $BUILD_SCRIPT"
+ exit 1
+fi
+
+# Get absolute path
+PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
+PACKAGE_NAME=$(basename "$PACKAGE_DIR")
+PARENT_DIR=$(dirname "$PACKAGE_DIR")
+
+info "Building and uploading: $PACKAGE_NAME"
+info "Package directory: $PACKAGE_DIR"
+info "PPA: ppa:avengemedia/$PPA_NAME"
+info "Ubuntu series: $UBUNTU_SERIES"
+echo
+
+# Step 1: Build source package
+info "Step 1: Building source package..."
+if ! "$BUILD_SCRIPT" "$PACKAGE_DIR" "$UBUNTU_SERIES"; then
+ error "Build failed!"
+ exit 1
+fi
+
+# Find the changes file
+CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
+
+if [ -z "$CHANGES_FILE" ]; then
+ error "Changes file not found in $PARENT_DIR"
+ exit 1
+fi
+
+info "Found changes file: $CHANGES_FILE"
+echo
+
+# Step 2: Upload to PPA
+info "Step 2: Uploading to PPA..."
+
+# Check if using lftp (for all PPAs) or dput
+if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
+ warn "Using lftp for upload"
+
+ # Extract version from changes file
+ VERSION=$(grep "^Version:" "$CHANGES_FILE" | awk '{print $2}')
+ SOURCE_NAME=$(grep "^Source:" "$CHANGES_FILE" | awk '{print $2}')
+
+ # Find all files to upload
+ BUILD_DIR=$(dirname "$CHANGES_FILE")
+ CHANGES_BASENAME=$(basename "$CHANGES_FILE")
+ DSC_FILE="${CHANGES_BASENAME/_source.changes/.dsc}"
+ TARBALL="${CHANGES_BASENAME/_source.changes/.tar.xz}"
+ BUILDINFO="${CHANGES_BASENAME/_source.changes/_source.buildinfo}"
+
+ # Check all files exist
+ MISSING_FILES=()
+ [ ! -f "$BUILD_DIR/$DSC_FILE" ] && MISSING_FILES+=("$DSC_FILE")
+ [ ! -f "$BUILD_DIR/$TARBALL" ] && MISSING_FILES+=("$TARBALL")
+ [ ! -f "$BUILD_DIR/$BUILDINFO" ] && MISSING_FILES+=("$BUILDINFO")
+
+ if [ ${#MISSING_FILES[@]} -gt 0 ]; then
+ error "Missing required files:"
+ for file in "${MISSING_FILES[@]}"; do
+ error " - $file"
+ done
+ exit 1
+ fi
+
+ info "Uploading files:"
+ info " - $CHANGES_BASENAME"
+ info " - $DSC_FILE"
+ info " - $TARBALL"
+ info " - $BUILDINFO"
+ echo
+
+ # lftp build dir change
+ LFTP_SCRIPT=$(mktemp)
+ cat > "$LFTP_SCRIPT" < [ubuntu-series]
+#
+# Example:
+# ./create-source.sh ../dms questing
+# ./create-source.sh ../dms-git questing
+
+set -e
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+info() { echo -e "${BLUE}[INFO]${NC} $1"; }
+success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
+warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+error() { echo -e "${RED}[ERROR]${NC} $1"; }
+
+if [ $# -lt 1 ]; then
+ error "Usage: $0 [ubuntu-series]"
+ echo
+ echo "Arguments:"
+ echo " package-dir : Path to package directory (e.g., ../dms)"
+ echo " ubuntu-series : Ubuntu series (optional, default: noble)"
+ echo " Options: noble, jammy, oracular, mantic"
+ echo
+ echo "Examples:"
+ echo " $0 ../dms questing"
+ echo " $0 ../dms-git questing"
+ exit 1
+fi
+
+PACKAGE_DIR="$1"
+UBUNTU_SERIES="${2:-noble}"
+
+# Validate package directory
+if [ ! -d "$PACKAGE_DIR" ]; then
+ error "Package directory not found: $PACKAGE_DIR"
+ exit 1
+fi
+
+if [ ! -d "$PACKAGE_DIR/debian" ]; then
+ error "No debian/ directory found in $PACKAGE_DIR"
+ exit 1
+fi
+
+# Get absolute path
+PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
+PACKAGE_NAME=$(basename "$PACKAGE_DIR")
+
+info "Building source package for: $PACKAGE_NAME"
+info "Package directory: $PACKAGE_DIR"
+info "Target Ubuntu series: $UBUNTU_SERIES"
+
+# Check for required files
+REQUIRED_FILES=(
+ "debian/control"
+ "debian/rules"
+ "debian/changelog"
+ "debian/copyright"
+ "debian/source/format"
+)
+
+for file in "${REQUIRED_FILES[@]}"; do
+ if [ ! -f "$PACKAGE_DIR/$file" ]; then
+ error "Required file missing: $file"
+ exit 1
+ fi
+done
+
+# Verify GPG key is set up
+info "Checking GPG key setup..."
+if ! gpg --list-secret-keys &> /dev/null; then
+ error "No GPG secret keys found. Please set up GPG first!"
+ error "See GPG_SETUP.md for instructions"
+ exit 1
+fi
+
+success "GPG key found"
+
+# Check if debuild is installed
+if ! command -v debuild &> /dev/null; then
+ error "debuild not found. Install devscripts:"
+ error " sudo dnf install devscripts"
+ exit 1
+fi
+
+# Extract package info from changelog
+cd "$PACKAGE_DIR"
+CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
+SOURCE_NAME=$(dpkg-parsechangelog -S Source)
+
+info "Source package: $SOURCE_NAME"
+info "Version: $CHANGELOG_VERSION"
+
+# Check if version targets correct Ubuntu series
+CHANGELOG_SERIES=$(dpkg-parsechangelog -S Distribution)
+if [ "$CHANGELOG_SERIES" != "$UBUNTU_SERIES" ] && [ "$CHANGELOG_SERIES" != "UNRELEASED" ]; then
+ warn "Changelog targets '$CHANGELOG_SERIES' but building for '$UBUNTU_SERIES'"
+ warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES"
+fi
+
+# Detect package type and update version automatically
+cd "$PACKAGE_DIR"
+
+# Function to get latest tag from GitHub
+get_latest_tag() {
+ local repo="$1"
+ # Try GitHub API first (faster)
+ if command -v curl &> /dev/null; then
+ LATEST_TAG=$(curl -s "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null | grep '"tag_name":' | sed 's/.*"tag_name": "\(.*\)".*/\1/' | head -1)
+ if [ -n "$LATEST_TAG" ]; then
+ echo "$LATEST_TAG" | sed 's/^v//'
+ return
+ fi
+ fi
+ # Fallback: clone and get latest tag
+ TEMP_REPO=$(mktemp -d)
+ if git clone --depth=1 --quiet "https://github.com/$repo.git" "$TEMP_REPO" 2>/dev/null; then
+ LATEST_TAG=$(cd "$TEMP_REPO" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "")
+ rm -rf "$TEMP_REPO"
+ echo "$LATEST_TAG"
+ fi
+}
+
+# Detect if package is git-based
+IS_GIT_PACKAGE=false
+GIT_REPO=""
+SOURCE_DIR=""
+
+# Check package name for -git suffix
+if [[ "$PACKAGE_NAME" == *"-git" ]]; then
+ IS_GIT_PACKAGE=true
+fi
+
+# Check rules file for git clone patterns and extract repo
+if grep -q "git clone" debian/rules 2>/dev/null; then
+ IS_GIT_PACKAGE=true
+ # Extract GitHub repo URL from rules
+ GIT_URL=$(grep -o "git clone.*https://github.com/[^/]*/[^/]*\.git" debian/rules 2>/dev/null | head -1 | sed 's/.*github\.com\///' | sed 's/\.git.*//' || echo "")
+ if [ -n "$GIT_URL" ]; then
+ GIT_REPO="$GIT_URL"
+ fi
+fi
+
+# Special handling for known packages
+case "$PACKAGE_NAME" in
+ dms-git)
+ IS_GIT_PACKAGE=true
+ GIT_REPO="AvengeMedia/DankMaterialShell"
+ SOURCE_DIR="dms-git-repo"
+ ;;
+ dms)
+ GIT_REPO="AvengeMedia/DankMaterialShell"
+ info "Downloading pre-built binaries and source for dms..."
+ # Get version from changelog (remove ppa suffix for both quilt and native formats)
+ # Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2
+ VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
+
+ # Download amd64 binary (will be included in source package)
+ if [ ! -f "dms-distropkg-amd64.gz" ]; then
+ info "Downloading dms binary for amd64..."
+ if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-distropkg-amd64.gz"; then
+ success "amd64 binary downloaded"
+ else
+ error "Failed to download dms-distropkg-amd64.gz"
+ exit 1
+ fi
+ fi
+
+ # Download source tarball for QML files
+ if [ ! -f "dms-source.tar.gz" ]; then
+ info "Downloading dms source for QML files..."
+ if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
+ success "source tarball downloaded"
+ else
+ error "Failed to download dms-source.tar.gz"
+ exit 1
+ fi
+ fi
+ ;;
+ danksearch)
+ # danksearch uses pre-built binary from releases, like dgop
+ GIT_REPO="AvengeMedia/danksearch"
+ ;;
+ dgop)
+ # dgop uses pre-built binary from releases
+ GIT_REPO="AvengeMedia/dgop"
+ ;;
+esac
+
+# Handle git packages
+if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
+ info "Detected git package: $PACKAGE_NAME"
+
+ # Determine source directory name
+ if [ -z "$SOURCE_DIR" ]; then
+ # Default: use package name without -git suffix + -source or -repo
+ BASE_NAME=$(echo "$PACKAGE_NAME" | sed 's/-git$//')
+ if [ -d "${BASE_NAME}-source" ] 2>/dev/null; then
+ SOURCE_DIR="${BASE_NAME}-source"
+ elif [ -d "${BASE_NAME}-repo" ] 2>/dev/null; then
+ SOURCE_DIR="${BASE_NAME}-repo"
+ elif [ -d "$BASE_NAME" ] 2>/dev/null; then
+ SOURCE_DIR="$BASE_NAME"
+ else
+ SOURCE_DIR="${BASE_NAME}-source"
+ fi
+ fi
+
+ # Always clone fresh source to get latest commit info
+ info "Cloning $GIT_REPO from GitHub (getting latest commit info)..."
+ TEMP_CLONE=$(mktemp -d)
+ if git clone "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
+ # Get git commit info from fresh clone
+ GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short HEAD)
+ GIT_COMMIT_COUNT=$(cd "$TEMP_CLONE" && git rev-list --count HEAD)
+
+ # Get upstream version from latest git tag (e.g., 0.2.1)
+ # Sort all tags by version and get the latest one (not just the one reachable from HEAD)
+ UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l "v*" | sed 's/^v//' | sort -V | tail -1)
+ if [ -z "$UPSTREAM_VERSION" ]; then
+ # Fallback: try without v prefix
+ UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -1)
+ fi
+ if [ -z "$UPSTREAM_VERSION" ]; then
+ # Last resort: use git describe
+ UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.1")
+ fi
+
+ # Verify we got valid commit info
+ if [ -z "$GIT_COMMIT_COUNT" ] || [ "$GIT_COMMIT_COUNT" = "0" ]; then
+ error "Failed to get commit count from $GIT_REPO"
+ rm -rf "$TEMP_CLONE"
+ exit 1
+ fi
+
+ if [ -z "$GIT_COMMIT_HASH" ]; then
+ error "Failed to get commit hash from $GIT_REPO"
+ rm -rf "$TEMP_CLONE"
+ exit 1
+ fi
+
+ success "Got commit info: $GIT_COMMIT_COUNT ($GIT_COMMIT_HASH), upstream: $UPSTREAM_VERSION"
+
+ # Update changelog with git commit info
+ info "Updating changelog with git commit info..."
+ # Format: 0.2.1+git705.fdbb86appa1
+ # Check if we're rebuilding the same commit (increment PPA number if so)
+ BASE_VERSION="${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}"
+ CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
+ PPA_NUM=1
+
+ # If current version matches the base version, increment PPA number
+ # Escape special regex characters in BASE_VERSION for pattern matching
+ ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/+/\\+/g')
+ if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
+ PPA_NUM=$((BASH_REMATCH[1] + 1))
+ info "Detected rebuild of same commit (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
+ else
+ info "New commit or first build, using PPA number $PPA_NUM"
+ fi
+
+ NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
+
+ # Use sed to update changelog (non-interactive, faster)
+ # Get current changelog content - find the next package header line (starts with package name)
+ # Skip the first entry entirely by finding the second occurrence of the package name at start of line
+ OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
+ if [ -n "$OLD_ENTRY_START" ]; then
+ # Found second entry, use everything from there
+ CHANGELOG_CONTENT=$(tail -n +$OLD_ENTRY_START debian/changelog)
+ else
+ # No second entry found, changelog will only have new entry
+ CHANGELOG_CONTENT=""
+ fi
+
+ # Create new changelog entry with proper format
+ CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
+
+ * Git snapshot (commit ${GIT_COMMIT_COUNT}: ${GIT_COMMIT_HASH})
+
+ -- Avenge Media $(date -R)"
+
+ # Write new changelog (new entry, blank line, then old entries)
+ echo "$CHANGELOG_ENTRY" > debian/changelog
+ if [ -n "$CHANGELOG_CONTENT" ]; then
+ echo "" >> debian/changelog
+ echo "$CHANGELOG_CONTENT" >> debian/changelog
+ fi
+ success "Version updated to $NEW_VERSION"
+
+ # Now clone to source directory (without .git for inclusion in package)
+ rm -rf "$SOURCE_DIR"
+ cp -r "$TEMP_CLONE" "$SOURCE_DIR"
+ rm -rf "$SOURCE_DIR/.git"
+ rm -rf "$TEMP_CLONE"
+
+ # Vendor Rust dependencies for packages that need it
+ if false; then
+ # No current packages need Rust vendoring
+ if [ -f "$SOURCE_DIR/Cargo.toml" ]; then
+ info "Vendoring Rust dependencies (Launchpad has no internet access)..."
+ cd "$SOURCE_DIR"
+
+ # Clean up any existing vendor directory and .orig files
+ # (prevents cargo from including .orig files in checksums)
+ rm -rf vendor .cargo
+ find . -type f -name "*.orig" -exec rm -f {} + || true
+
+ # Download all dependencies (crates.io + git repos) to vendor/
+ # cargo vendor outputs the config to stderr, capture it
+ mkdir -p .cargo
+ cargo vendor 2>&1 | awk '
+ /^\[source\.crates-io\]/ { printing=1 }
+ printing { print }
+ /^directory = "vendor"$/ { exit }
+ ' > .cargo/config.toml
+
+ # Verify vendor directory was created
+ if [ ! -d "vendor" ]; then
+ error "Failed to vendor dependencies"
+ exit 1
+ fi
+
+ # Verify config was created
+ if [ ! -s .cargo/config.toml ]; then
+ error "Failed to create cargo config"
+ exit 1
+ fi
+
+ # CRITICAL: Remove ALL .orig files from vendor directory
+ # These break cargo checksums when dh_clean tries to use them
+ info "Cleaning .orig files from vendor directory..."
+ find vendor -type f -name "*.orig" -exec rm -fv {} + || true
+ find vendor -type f -name "*.rej" -exec rm -fv {} + || true
+
+ # Verify no .orig files remain
+ ORIG_COUNT=$(find vendor -type f -name "*.orig" | wc -l)
+ if [ "$ORIG_COUNT" -gt 0 ]; then
+ warn "Found $ORIG_COUNT .orig files still in vendor directory"
+ fi
+
+ success "Rust dependencies vendored (including git dependencies)"
+ cd "$PACKAGE_DIR"
+ fi
+ fi
+
+ # Download pre-built binary for dms-git
+ # dms-git uses latest release binary with git master QML files
+ if [ "$PACKAGE_NAME" = "dms-git" ]; then
+ info "Downloading latest release binary for dms-git..."
+ if [ ! -f "dms-distropkg-amd64.gz" ]; then
+ if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-amd64.gz"; then
+ success "Latest release binary downloaded"
+ else
+ error "Failed to download dms-distropkg-amd64.gz"
+ exit 1
+ fi
+ else
+ info "Release binary already downloaded"
+ fi
+ fi
+
+ success "Source prepared for packaging"
+ else
+ error "Failed to clone $GIT_REPO"
+ rm -rf "$TEMP_CLONE"
+ exit 1
+ fi
+# Handle stable packages - get latest tag
+elif [ -n "$GIT_REPO" ]; then
+ info "Detected stable package: $PACKAGE_NAME"
+ info "Fetching latest tag from $GIT_REPO..."
+
+ LATEST_TAG=$(get_latest_tag "$GIT_REPO")
+ if [ -n "$LATEST_TAG" ]; then
+ # Check source format - native packages can't use dashes
+ SOURCE_FORMAT=$(cat debian/source/format 2>/dev/null | head -1 || echo "3.0 (quilt)")
+
+ # Get current version to check if we need to increment PPA number
+ CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
+ PPA_NUM=1
+
+ if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
+ # Native format: 0.2.1ppa1 (no dash, no revision)
+ BASE_VERSION="${LATEST_TAG}"
+ # Check if we're rebuilding the same version (increment PPA number if so)
+ if [[ "$CURRENT_VERSION" =~ ^${LATEST_TAG}ppa([0-9]+)$ ]]; then
+ PPA_NUM=$((BASH_REMATCH[1] + 1))
+ info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
+ else
+ info "New version or first build, using PPA number $PPA_NUM"
+ fi
+ NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
+ else
+ # Quilt format: 0.2.1-1ppa1 (with revision)
+ BASE_VERSION="${LATEST_TAG}-1"
+ # Check if we're rebuilding the same version (increment PPA number if so)
+ ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/-/\\-/g')
+ if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
+ PPA_NUM=$((BASH_REMATCH[1] + 1))
+ info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
+ else
+ info "New version or first build, using PPA number $PPA_NUM"
+ fi
+ NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
+ fi
+
+ # Check if version needs updating (either new version or PPA number changed)
+ if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
+ if [ "$PPA_NUM" -gt 1 ]; then
+ info "Updating changelog for rebuild (PPA number incremented to $PPA_NUM)"
+ else
+ info "Updating changelog to latest tag: $LATEST_TAG"
+ fi
+ # Use sed to update changelog (non-interactive)
+ # Get current changelog content - find the next package header line
+ OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
+ if [ -n "$OLD_ENTRY_START" ]; then
+ CHANGELOG_CONTENT=$(tail -n +$OLD_ENTRY_START debian/changelog)
+ else
+ CHANGELOG_CONTENT=""
+ fi
+
+ # Create appropriate changelog message
+ if [ "$PPA_NUM" -gt 1 ]; then
+ CHANGELOG_MSG="Rebuild for packaging fixes (ppa${PPA_NUM})"
+ else
+ CHANGELOG_MSG="Upstream release ${LATEST_TAG}"
+ fi
+
+ CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
+
+ * ${CHANGELOG_MSG}
+
+ -- Avenge Media $(date -R)"
+ echo "$CHANGELOG_ENTRY" > debian/changelog
+ if [ -n "$CHANGELOG_CONTENT" ]; then
+ echo "" >> debian/changelog
+ echo "$CHANGELOG_CONTENT" >> debian/changelog
+ fi
+ success "Version updated to $NEW_VERSION"
+ else
+ info "Version already at latest tag: $LATEST_TAG"
+ fi
+ else
+ warn "Could not determine latest tag for $GIT_REPO, using existing version"
+ fi
+fi
+
+# Handle packages that need pre-built binaries downloaded
+cd "$PACKAGE_DIR"
+case "$PACKAGE_NAME" in
+ danksearch)
+ info "Downloading pre-built binaries for danksearch..."
+ # Get version from changelog (remove ppa suffix for both quilt and native formats)
+ # Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2
+ VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
+
+ # Download both amd64 and arm64 binaries (will be included in source package)
+ # Launchpad can't download during build, so we include both architectures
+ if [ ! -f "dsearch-amd64" ]; then
+ info "Downloading dsearch binary for amd64..."
+ if wget -O dsearch-amd64.gz "https://github.com/AvengeMedia/danksearch/releases/download/v${VERSION}/dsearch-linux-amd64.gz"; then
+ gunzip dsearch-amd64.gz
+ chmod +x dsearch-amd64
+ success "amd64 binary downloaded"
+ else
+ error "Failed to download dsearch-amd64.gz"
+ exit 1
+ fi
+ fi
+
+ if [ ! -f "dsearch-arm64" ]; then
+ info "Downloading dsearch binary for arm64..."
+ if wget -O dsearch-arm64.gz "https://github.com/AvengeMedia/danksearch/releases/download/v${VERSION}/dsearch-linux-arm64.gz"; then
+ gunzip dsearch-arm64.gz
+ chmod +x dsearch-arm64
+ success "arm64 binary downloaded"
+ else
+ error "Failed to download dsearch-arm64.gz"
+ exit 1
+ fi
+ fi
+ ;;
+ dgop)
+ # dgop binary should already be committed in the repo
+ if [ ! -f "dgop" ]; then
+ warn "dgop binary not found - should be committed to repo"
+ fi
+ ;;
+esac
+
+cd - > /dev/null
+
+# Build source package
+info "Building source package..."
+echo
+
+# Use -S for source only, -sa to include original source
+# -d skips dependency checking (we're building on Fedora, not Ubuntu)
+# Pipe yes to automatically answer prompts (e.g., "continue anyway?")
+if yes | DEBIAN_FRONTEND=noninteractive debuild -S -sa -d; then
+ echo
+ success "Source package built successfully!"
+
+ # List generated files
+ info "Generated files in $(dirname "$PACKAGE_DIR"):"
+ ls -lh "$(dirname "$PACKAGE_DIR")"/${SOURCE_NAME}_${CHANGELOG_VERSION}* 2>/dev/null || true
+
+ # Show what to do next
+ echo
+ info "Next steps:"
+ echo " 1. Review the source package:"
+ echo " cd $(dirname "$PACKAGE_DIR")"
+ 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
+ echo " 3. Or upload to PPA (nightly):"
+ echo " dput ppa:avengemedia/dms-git ${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes"
+ echo
+ echo " 4. Or use the upload script:"
+ echo " ./upload-ppa.sh $(dirname "$PACKAGE_DIR")/${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes dms"
+
+else
+ error "Source package build failed!"
+ exit 1
+fi
diff --git a/distro/ubuntu/ppa/upload-ppa.sh b/distro/ubuntu/ppa/upload-ppa.sh
new file mode 100755
index 00000000..e7c99a16
--- /dev/null
+++ b/distro/ubuntu/ppa/upload-ppa.sh
@@ -0,0 +1,179 @@
+#!/bin/bash
+# Generic PPA uploader for DMS packages
+# Usage: ./upload-ppa.sh
+#
+# Example:
+# ./upload-ppa.sh ../dms_0.5.2ppa1_source.changes dms
+# ./upload-ppa.sh ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git
+
+set -e
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+info() { echo -e "${BLUE}[INFO]${NC} $1"; }
+success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
+warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
+error() { echo -e "${RED}[ERROR]${NC} $1"; }
+
+if [ $# -lt 2 ]; then
+ error "Usage: $0 "
+ echo
+ echo "Arguments:"
+ echo " changes-file : Path to .changes file (e.g., ../dms_0.5.2ppa1_source.changes)"
+ echo " ppa-name : PPA to upload to (dms or dms-git)"
+ echo
+ echo "Examples:"
+ echo " $0 ../dms_0.5.2ppa1_source.changes dms"
+ echo " $0 ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git"
+ exit 1
+fi
+
+CHANGES_FILE="$1"
+PPA_NAME="$2"
+
+# Validate changes file
+if [ ! -f "$CHANGES_FILE" ]; then
+ error "Changes file not found: $CHANGES_FILE"
+ exit 1
+fi
+
+if [[ ! "$CHANGES_FILE" =~ \.changes$ ]]; then
+ error "File must be a .changes file"
+ exit 1
+fi
+
+# Validate PPA name
+if [ "$PPA_NAME" != "dms" ] && [ "$PPA_NAME" != "dms-git" ] && [ "$PPA_NAME" != "danklinux" ]; then
+ error "PPA name must be 'dms', 'dms-git', or 'danklinux'"
+ exit 1
+fi
+
+# Get absolute path
+CHANGES_FILE=$(realpath "$CHANGES_FILE")
+
+info "Uploading to PPA: ppa:avengemedia/$PPA_NAME"
+info "Changes file: $CHANGES_FILE"
+
+# Check if dput or lftp is installed
+UPLOAD_METHOD=""
+if command -v dput &> /dev/null; then
+ UPLOAD_METHOD="dput"
+elif command -v lftp &> /dev/null; then
+ UPLOAD_METHOD="lftp"
+ warn "dput not found, using lftp as fallback"
+else
+ error "Neither dput nor lftp found. Install one with:"
+ error " sudo dnf install dput-ng # Preferred but broken on Fedora"
+ error " sudo dnf install lftp # Alternative upload method"
+ exit 1
+fi
+
+# Check if ~/.dput.cf exists
+if [ ! -f "$HOME/.dput.cf" ]; then
+ error "~/.dput.cf not found!"
+ echo
+ info "Create it from template:"
+ echo " cp $(dirname "$0")/../dput.cf.template ~/.dput.cf"
+ echo
+ info "Or create it manually with:"
+ cat <<'EOF'
+[ppa:avengemedia/dms]
+fqdn = ppa.launchpad.net
+method = ftp
+incoming = ~avengemedia/ubuntu/dms/
+login = anonymous
+allow_unsigned_uploads = 0
+
+[ppa:avengemedia/dms-git]
+fqdn = ppa.launchpad.net
+method = ftp
+incoming = ~avengemedia/ubuntu/dms-git/
+login = anonymous
+allow_unsigned_uploads = 0
+EOF
+ exit 1
+fi
+
+# Check if PPA is configured in dput.cf
+if ! grep -q "^\[ppa:avengemedia/$PPA_NAME\]" "$HOME/.dput.cf"; then
+ error "PPA 'ppa:avengemedia/$PPA_NAME' not found in ~/.dput.cf"
+ echo
+ info "Add this to ~/.dput.cf:"
+ cat </dev/null; then
+ success "GPG signature valid"
+else
+ error "GPG signature verification failed!"
+ error "The .changes file must be signed with your GPG key"
+ exit 1
+fi
+
+# Ask for confirmation
+echo
+warn "About to upload to: ppa:avengemedia/$PPA_NAME"
+read -p "Continue? (y/N) " -n 1 -r
+echo
+if [[ ! $REPLY =~ ^[Yy]$ ]]; then
+ info "Upload cancelled"
+ exit 0
+fi
+
+# Upload to PPA
+info "Uploading to Launchpad..."
+echo
+
+if dput "ppa:avengemedia/$PPA_NAME" "$CHANGES_FILE"; then
+ echo
+ success "Upload successful!"
+ echo
+ info "Monitor build progress at:"
+ echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
+ echo
+ info "Builds typically take 5-30 minutes depending on:"
+ echo " - Build queue length"
+ echo " - Package complexity"
+ echo " - Number of target Ubuntu series"
+ echo
+ info "Once built, users can install with:"
+ echo " sudo add-apt-repository ppa:avengemedia/$PPA_NAME"
+ echo " sudo apt update"
+ echo " sudo apt install $PACKAGE_NAME"
+
+else
+ error "Upload failed!"
+ echo
+ info "Common issues:"
+ echo " - GPG key not verified on Launchpad (check https://launchpad.net/~/+editpgpkeys)"
+ echo " - Version already uploaded (must increment version number)"
+ echo " - Network/firewall blocking FTP (try HTTPS method in dput.cf)"
+ echo " - Email in changelog doesn't match GPG key email"
+ exit 1
+fi