├── .github └── workflows │ ├── buildTooling.yml │ └── static.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── appbundle-runtime ├── appbundle-runtime.go ├── cli.go ├── embed.go ├── embed_dwarfs.go ├── embed_squashfs.go └── noEmbed.go ├── assets ├── AppRun.gccToolchain ├── AppRun.generic ├── AppRun.goToolchain ├── AppRun.multiBinary ├── AppRun.override ├── AppRun.rootfs-based ├── AppRun.rootfs-based.stable ├── AppRun.sharun ├── AppRun.sharun.ovfsProto ├── LAUNCH-multicall.rootfs.entrypoint ├── pin.svg └── screenshot.png ├── cbuild.sh ├── cmd ├── dynexec │ ├── C │ │ └── dynexec.c │ ├── README.md │ ├── dynexec.go │ └── lib4bin │ │ └── lib4bin.go ├── misc │ ├── BS2AppBundle │ ├── appstream-helper │ │ ├── appstream-helper.go │ │ ├── go.mod │ │ └── go.sum │ ├── getlibs │ ├── rootfs2sharun │ └── thumbgen ├── pelfCreator │ └── pelfCreator.go ├── pelfd-gui.deprecated │ ├── pelfd.go │ └── util.go ├── pelfd │ ├── appbundle_support.go │ ├── appimage_support.go │ ├── pelfd.go │ └── util.go └── pfusermount │ ├── cbuild.sh │ ├── fusermount.go │ └── fusermount3.go ├── docs ├── _index.md ├── format.md ├── runtime.md └── tooling.md ├── go.mod ├── go.sum ├── pelf.go ├── pelf_linker └── www ├── archetypes └── default.md ├── config.toml ├── content ├── _index.md └── docs │ ├── _index.md │ ├── format.md │ ├── runtime.md │ └── tooling.md ├── gen.sh └── static └── assets └── pin.svg /.github/workflows/buildTooling.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release PELF tooling as a single-file executable 2 | concurrency: 3 | group: build-${{ github.ref }} 4 | cancel-in-progress: true 5 | on: 6 | schedule: 7 | - cron: "0 14 * * 0" 8 | workflow_dispatch: 9 | jobs: 10 | build: 11 | name: "${{ matrix.name }} (${{ matrix.arch }})" 12 | runs-on: ${{ matrix.runs-on }} 13 | strategy: 14 | matrix: 15 | include: 16 | - runs-on: ubuntu-latest 17 | name: "cbuild.sh (amd64)" 18 | arch: x86_64 19 | - runs-on: ubuntu-24.04-arm 20 | name: "cbuild.sh (arm64)" 21 | arch: aarch64 22 | container: 23 | image: "alpine:edge" 24 | volumes: 25 | - /:/host # Jailbreak! 26 | steps: 27 | - name: Patch native Alpine NodeJS into Runner environment 28 | if: matrix.arch == 'aarch64' 29 | run: | 30 | apk add nodejs gcompat openssl 31 | sed -i "s:ID=alpine:ID=NotpineForGHA:" /etc/os-release 32 | cd /host/home/runner/runners/*/externals/ 33 | rm -rf node20/* 34 | mkdir node20/bin 35 | ln -sfT /usr/bin/node node20/bin/node 36 | 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | - name: Set up GOBIN and install lib4bin 42 | run: | 43 | set -x 44 | apk add zstd git bash file binutils patchelf findutils grep sed strace go fuse3 fuse curl yq-go b3sum 45 | export GOBIN="$GITHUB_WORKSPACE/.local/bin" CGO_ENABLED=0 GO_LDFLAGS='-buildmode=static-pie' GOFLAGS='-ldflags=-static-pie -ldflags=-s -ldflags=-w' 46 | export DBIN_INSTALL_DIR="$GOBIN" DBIN_NOCONFIG=1 PATH="$GOBIN:$PATH" 47 | mkdir -p "$GOBIN" 48 | wget -qO- "https://raw.githubusercontent.com/xplshn/dbin/master/stubdl" | sh -s -- --install "$DBIN_INSTALL_DIR/dbin" -v 49 | "$DBIN_INSTALL_DIR/dbin" --silent add yq upx 50 | echo "PATH=$PATH" >> $GITHUB_ENV 51 | echo "DBIN_INSTALL_DIR=$GOBIN" >> $GITHUB_ENV 52 | echo "WITH_SHARUN=1" >> $GITHUB_ENV 53 | echo "GEN_LIB_PATH=1" >> $GITHUB_ENV 54 | echo "ANY_EXECUTABLE=1" >> $GITHUB_ENV 55 | mkdir "$GITHUB_WORKSPACE/dist" 56 | ROOTFS_URL="$(curl -qsL https://dl-cdn.alpinelinux.org/alpine/edge/releases/${{ matrix.arch }}/latest-releases.yaml | yq '.[0].file')" 57 | echo "https://dl-cdn.alpinelinux.org/alpine/edge/releases/${{ matrix.arch }}/${ROOTFS_URL}" >"$GITHUB_WORKSPACE/dist/alpineLinuxEdge.${{ matrix.arch }}.rootfsURL" 58 | ROOTFS_URL="https://dl-cdn.alpinelinux.org/alpine/edge/releases/${{ matrix.arch }}/${ROOTFS_URL}" 59 | export ROOTFS_URL 60 | echo "ROOTFS_URL=$ROOTFS_URL" >> "$GITHUB_ENV" 61 | apk add coreutils 62 | - name: Build AppBundle tooling 63 | run: | 64 | cd "$GITHUB_WORKSPACE" 65 | export CGO_ENABLED=0 GOFLAGS="-ldflags=-static-pie -ldflags=-s -ldflags=-w" GO_LDFLAGS="-buildmode=static-pie -s -w" 66 | export _RELEASE="1" 67 | ./cbuild.sh && ./cbuild.sh pelfCreator_extensions 68 | B3SUM_CHECKSUM="$(b3sum ./pelf | awk '{print $1}')" 69 | mv ./pelf "$GITHUB_WORKSPACE/dist/pelf_${{ matrix.arch }}" 70 | mv ./cmd/pelfCreator/pelfCreator "$GITHUB_WORKSPACE/dist/pelfCreator_${{ matrix.arch }}" 71 | mv ./cmd/pelfCreator/pelfCreatorExtension_archLinux.tar.zst "$GITHUB_WORKSPACE/dist/pelfCreatorExtension_archLinux_${{ matrix.arch }}".tar.zst 72 | mv ./cmd/misc/appstream-helper/appstream-helper "$GITHUB_WORKSPACE/dist/appstream-helper_${{ matrix.arch }}" 73 | echo "RELEASE_TAG=$(date +%d%m%Y)-$B3SUM_CHECKSUM" >> $GITHUB_ENV 74 | - name: Upload artifact 75 | uses: actions/upload-artifact@v4.6.1 76 | with: 77 | name: AppBundle-${{ matrix.arch }} 78 | path: ${{ github.workspace }}/dist/* 79 | - name: Set build output 80 | id: build_output 81 | run: | 82 | echo "release_tag=$(date +%d%m%Y)-$(b3sum ./pelf | awk '{print $1}')" >> $GITHUB_OUTPUT 83 | 84 | release: 85 | name: Create Release 86 | needs: build 87 | runs-on: ubuntu-latest 88 | steps: 89 | - name: Download all artifacts 90 | uses: actions/download-artifact@v4 91 | with: 92 | path: artifacts 93 | merge-multiple: true 94 | 95 | - name: List files 96 | run: find artifacts -type f | sort 97 | 98 | - name: Create Release 99 | uses: softprops/action-gh-release@v2.2.1 100 | with: 101 | name: "Build ${{ needs.build.outputs.release_tag || github.run_number }}" 102 | tag_name: "${{ needs.build.outputs.release_tag || github.run_number }}" 103 | prerelease: false 104 | draft: false 105 | generate_release_notes: false 106 | make_latest: true 107 | files: | 108 | artifacts/* 109 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Deploy static content to Pages 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | workflow_dispatch: 7 | 8 | 9 | permissions: 10 | contents: write 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | deploy: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: ubuntu-latest 24 | 25 | container: 26 | image: alpine:latest 27 | 28 | steps: 29 | - name: Install Hugo, Git, and the worst version of Tar 30 | run: | 31 | apk update 32 | apk add hugo git tar 33 | 34 | - name: Checkout GitHub repo 35 | uses: actions/checkout@v4 36 | with: 37 | ref: 'master' 38 | submodules: 'true' 39 | fetch-depth: 0 40 | 41 | - name: Configure Git safe.directory 42 | run: | 43 | git config --global --add safe.directory "${GITHUB_WORKSPACE}" 44 | 45 | - name: Generate the site 46 | run: | 47 | cd "${GITHUB_WORKSPACE}/www" 48 | tree 49 | ./gen.sh 50 | cd "${GITHUB_WORKSPACE}" 51 | 52 | # Prepare git to commit changes 53 | git config user.name '[CI]' 54 | git config user.email 'action@github.com' 55 | 56 | ## Commit changes dynamically 57 | #git add "${GITHUB_WORKSPACE}/www" 58 | #git commit -m "[www]" || echo "Nothing to commit" 59 | #git push origin HEAD || echo "Push failed; check repository settings" 60 | 61 | - name: Setup Pages 62 | uses: actions/configure-pages@v5 63 | 64 | - name: Upload artifact 65 | uses: actions/upload-pages-artifact@v3 66 | with: 67 | path: "./www/pub" 68 | 69 | - name: Deploy to GitHub Pages 70 | id: deployment 71 | uses: actions/deploy-pages@v4 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.AppBundle 2 | *.blob 3 | .* 4 | work/ 5 | exp/ 6 | appbundle-runtime/appbundle-runtime 7 | appbundle-runtime/binaryDependencies 8 | cmd/pelfCreator/pelfCreator 9 | cmd/misc/appstream-helper/appstream-helper 10 | binaryDependencies 11 | binaryDependencies.tar.zst 12 | runtime 13 | pelf 14 | fusermount 15 | fusermount3 16 | TODO 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "www/themes/werx"] 2 | path = www/themes/werx 3 | url = https://github.com/xplshn/werx 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, xplshn 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### PELF - The AppBundle format and the AppBundle Creation Tool 2 | ###### PELF used to stand for Pack an Elf, but we slowly evolved into a much simpler yet more featureful alternative to .AppImages 3 | ###### PELF now refers to the tool used to create .AppBundles 4 | 5 | --- 6 | 7 | > .AppBundles are an executable *packaging format* designed to pack applications, toolchains, window managers, and multiple programs into a *single portable file*. 8 | 9 | AppBundles can serve as a drop-in replacement for AppImages. Both AppBundles and AppImages utilize the AppDir specification, making it easy to unpack an AppImage and re-package it as an AppBundle, gaining many features, such as faster start-up times, better compression and file de-duplication, and faster build-time. A completely customizable and flexible format. 10 | 11 | #### Advantages 12 | - **Support for multiple filesystem formats**: Support for multiple mountable filesystem formats, we currently support `squashfs` and `dwarfs`. With ongoing efforts to add a third alternative that isn't copylefted/propietary 13 | - **Simplicity**: PELF is a minimalistic Go program that makes creating portable POSIX executables a trivial task. 14 | - **Flexibility of AppBundles**: AppBundles do not force compliance with the AppDir standard. For example, you can bundle window managers and basic GUI utilities into a single file (as done with `Sway.AppBundle`). You can even package toolchains as single-file executables. 15 | - **Endless Possibilities**: With a custom AppRun script, you can create versatile `.AppBundles`. For instance, packaging a Rick Roll video with a video player that works on both glibc and musl systems is straightforward. You can even generate AppBundles that overlay on top of each other. 16 | - **Complete tooling**: The `pelfd` daemon (and its GUI version) are available for use as system integrators, they're in charge of adding the AppBundles that you put under ~/Applications in your "start menu". This is one of the many programs that are part of the tooling, another great tool is pelfCreator, which lets you create programs via simple one-liners (by default it uses an Alpine rootfs + bwrap, but you can get smaller binaries via using -x to only keep the binaries you want), a one-liner to pack Chromium into a single-file executable looks like this: `pelfCreator --maintainer "xplshn" --name "org.chromium.Chromium" --pkg-add "chromium" --entrypoint "chromium.desktop"` 17 | - **Predictable mount directories**: Our mount directories contain the AppBundle's ID, making it clear to which AppBundle the mount directory belongs 18 | - **Reliable unmount**: The AppBundle starts a background task to unmount the filesystem, and it retries 5 times, then it forces the unmount if all 5 tries failed 19 | - **Leverages many handy env variables**: Thus making .AppBundles very flexible and scriptable 20 | - **AppImage compatibility**: The --appimage-* flags are supported by our runtime, making us an actual drop-in replacement 21 | 22 | ### Usage 23 | ``` 24 | ./pelf --add-appdir "nano-14_02_2025.AppDir" --appbundle-id "nano-14_02_2025-xplshn" --output-to "nano-14_02_2025.dwfs.AppBundle" 25 | ``` 26 | OR 27 | ``` 28 | ./pelf --add-appdir "nano-14_02_2025.AppDir" --appbundle-id "nano-14_02_2025-xplshn" --output-to "nano-14_02_2025.sqfs.AppBundle" 29 | ``` 30 | 31 | ### Build ./pelf 32 | 1. Get yourself an up-to-date `go` toolchain and install `dbin` into your system or put it anywhere in your `$PATH` 33 | 2. execute `./cbuild.sh` 34 | 3. Put the resulting `./pelf` binary in your `$PATH` 35 | 4. Spread the joy of AppBundles! :) 36 | 37 | ### Usage of the Resulting `.AppBundle` 38 | > By using the `--pbundle_link` option, you can access files contained within the `./bin` or `./usr/bin` directories of an `.AppBundle`, inheriting environment variables like `PATH`. This allows multiple AppBundles to stack on top of each other, sharing libraries and binaries across "parent" bundles. 39 | 40 | #### Explanation 41 | You specify an `AppDir` to be packed and an ID for the app. This ID will be used when mounting the `.AppBundle` and should include the packing date, the project or program name, and the maintainer's information. While you can choose an arbitrary name, it’s not recommended. 42 | 43 | Additionally, we embed the tools used for mounting and unmounting the `.AppBundle`, such as `dwarfs` when using `pelf`. 44 | 45 |

46 | Screenshot showcasing a bunch of AppBundles with their icons correctly set in a thunar file manager window 47 |

48 | 49 | #### Known working distros/OSes: 50 | - Ubuntu (10.04 onwards) & derivatives, Ubuntu Touch 51 | - Alpine Linux 2.+ onwards 52 | - Void Linux Musl/Glibc 53 | - Debian/Devuan, and derivatives 54 | - Fedora 55 | - *SUSE 56 | - Maemo leste 57 | - AliceLinux 58 | - FreeBSD's Linuxlator 59 | - FreeBSD native 60 | - Chimera Linux 61 | - LFS (Linux from Scratch) 62 | - Most if not all Musl linux distributions 63 | - etc (please contribute to this list if you're a user of AppBundles) 64 | 65 | #### Resources: 66 | - [AppBundle format documentation & specifications](https://xplshn.github.io/pelf/docs) 67 | - The [AppBundleHUB](https://github.com/xplshn/AppBundleHUB) a repo which builds a ton of portable AppBundles in an automated fashion, using GH actions. (we have a [webStore](https://xplshn.github.io/AppBundleHUB) too, tho that is WIP) 68 | - [dbin](https://github.com/xplshn/dbin) a self-contained, portable, statically linked, package manager, +4000 binaries (portable, self-contained/static) are available in its repos at the time of writting. Among these, are the AppBundles from the AppBundleHUB and from pkgforge 69 | -------------------------------------------------------------------------------- /appbundle-runtime/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func handleRuntimeFlags(fh *fileHandler, args *[]string, cfg *RuntimeConfig) error { 12 | switch (*args)[0] { 13 | case "--pbundle_help": 14 | fmt.Printf("This bundle was generated automatically by PELF %s, the machine on which it was created has the following \"uname -mrsp(v)\":\n %s\n\n", cfg.pelfVersion, cfg.pelfHost) 15 | fmt.Printf(" Internal variables:\n") 16 | fmt.Printf(" cfg.exeName: %s%s%s\n", blueColor, cfg.exeName, resetColor) 17 | fmt.Printf(" cfg.rExeName: %s%s%s\n", blueColor, cfg.rExeName, resetColor) 18 | fmt.Printf(" cfg.mountDir: %s%s%s\n", blueColor, cfg.mountDir, resetColor) 19 | fmt.Printf(" cfg.workDir: %s%s%s\n", blueColor, cfg.workDir, resetColor) 20 | fmt.Printf(" cfg.appBundleFS: %s%s%s\n", blueColor, cfg.appBundleFS, resetColor) 21 | fmt.Printf(" cfg.archiveOffset: %s%d%s\n", blueColor, cfg.archiveOffset, resetColor) 22 | fmt.Printf(` 23 | Flags: 24 | --pbundle_help: Needs no introduction 25 | --pbundle_list: List the contens of the AppBundle (including the static files that aren't part of the AppDir) 26 | --pbundle_link : Executes a given command, while leveraging the env variables of the AppBundle, including $PATH 27 | You can use this flag to execute commands within the AppBundle 28 | example: --pbundle_link sh -c "ls \$SELF_TEMPDIR" ; It'd output the contents of this AppBundle's AppDir 29 | --pbundle_pngIcon: Sends to stdout the base64 encoded .DirIcon, exits with error number 1 if the .DirIcon does not exist 30 | --pbundle_svgIcon: Sends to stdout the base64 encoded .DirIcon.svg, exits with error number 1 if the .DirIcon does not exist 31 | --pbundle_appstream: Same as --pbundle_pngIcon but it uses the first .xml file it encounters on the top level of the AppDir 32 | --pbundle_desktop: Same as --pbundle_pngIcon but it uses the first .desktop file it encounters on the top level of the AppDir 33 | --pbundle_portableHome: Creates a directory in the same place as the AppBundle, which will be used as $HOME during subsequent runs 34 | --pbundle_portableConfig: Creates a directory in the same place as the AppBundle, which will be used as $XDG_CONFIG_HOME during subsequent runs 35 | --pbundle_cleanup: Unmounts, removes, and tides up the AppBundle's workdir and mount pool. Does not affect other running AppBundles 36 | Only affects other instances of this same AppBundle. 37 | --pbundle_mount: Mounts the AppBundle's filesystem to the specified directory or the default mount directory. 38 | `) 39 | 40 | if cfg.appBundleFS != "dwarfs" { 41 | fmt.Printf(" --pbundle_extract <[]globs>: Extracts the AppBundle's filesystem to ./%s\n", cfg.rExeName + "_" + cfg.appBundleFS) 42 | fmt.Println(` If globs are provided, it will extract the matching files`) 43 | } else { 44 | fmt.Printf(" --pbundle_extract: Extracts the AppBundle's filesystem to ./%s\n", cfg.rExeName + "_" + cfg.appBundleFS) 45 | } 46 | 47 | fmt.Printf(` 48 | Compatibilty flags: 49 | --appimage-extract: Same as --pbundle_extract but hardcodes the output directory to ./squashfs-root 50 | --appimage-extract-and-run: Same as --pbundle_extract_and_run but for AppImage compatibility 51 | --appimage-mount: Same as --pbundle_mount but for AppImage compatibility 52 | --appimage-offset: Same as --pbundle_offset but for AppImage compatibility 53 | 54 | NOTE: EXE_NAME is the AppBundleID -> rEXE_NAME is the same, but sanitized to be used as a variable name 55 | NOTE: The -v option in uname may have not been saved, to allow for reproducibility (since uname -v will output the current date) 56 | NOTE: This runtime is written in Go, it is not the default runtime used by pelf 57 | `) 58 | return fmt.Errorf("!no_return") 59 | 60 | case "--pbundle_list": 61 | mountOrExtract(cfg, fh) 62 | err := filepath.Walk(cfg.workDir, func(path string, info os.FileInfo, err error) error { 63 | if err != nil { 64 | return err 65 | } 66 | fmt.Println(path) 67 | return nil 68 | }) 69 | if err != nil { 70 | return fmt.Errorf("%v", err) 71 | } 72 | return fmt.Errorf("!no_return") 73 | 74 | case "--pbundle_portableHome": 75 | if err := os.MkdirAll("." + cfg.selfPath + ".home", 0755); err != nil { 76 | return err 77 | } 78 | return fmt.Errorf("!no_return") 79 | 80 | case "--pbundle_portableConfig": 81 | if err := os.MkdirAll("." + cfg.selfPath + ".config", 0755); err != nil { 82 | return err 83 | } 84 | return fmt.Errorf("!no_return") 85 | 86 | case "--pbundle_link": 87 | if len(*args) < 2 { 88 | return fmt.Errorf("missing binary argument for --pbundle_link") 89 | } 90 | cfg.entrypoint = (*args)[1] 91 | *args = (*args)[2:] 92 | mountOrExtract(cfg, fh) 93 | _ = executeFile(*args, cfg) 94 | return fmt.Errorf("!no_return") 95 | 96 | case "--pbundle_pngIcon": 97 | mountOrExtract(cfg, fh) 98 | iconPath := cfg.mountDir + "/.DirIcon" 99 | if _, err := os.Stat(iconPath); err == nil { 100 | return encodeFileToBase64(iconPath) 101 | } 102 | logError("PNG icon not found", nil, cfg) 103 | 104 | case "--pbundle_svgIcon": 105 | mountOrExtract(cfg, fh) 106 | iconPath := cfg.mountDir + "/.DirIcon.svg" 107 | if _, err := os.Stat(iconPath); err == nil { 108 | return encodeFileToBase64(iconPath) 109 | } 110 | logError("SVG icon not found", nil, cfg) 111 | 112 | case "--pbundle_desktop": 113 | mountOrExtract(cfg, fh) 114 | return findAndEncodeFiles(cfg.mountDir, "*.desktop", cfg) 115 | 116 | case "--pbundle_appstream": 117 | mountOrExtract(cfg, fh) 118 | return findAndEncodeFiles(cfg.mountDir, "*.xml", cfg) 119 | 120 | case "--pbundle_extract": 121 | query := "" 122 | if len(*args) > 1 { 123 | query = strings.Join((*args)[1:], " ") 124 | } 125 | cfg.mountDir = cfg.rExeName + "_" + cfg.appBundleFS 126 | fs, err := checkDeps(cfg, fh) 127 | if err != nil { 128 | return err 129 | } 130 | if err := extractImage(cfg, fh, fs, query); err != nil { 131 | return err 132 | } 133 | fmt.Println("./" + cfg.mountDir) 134 | return fmt.Errorf("!no_return") 135 | 136 | case "--appimage-extract": 137 | query := "" 138 | if len(*args) > 1 { 139 | query = strings.Join((*args)[1:], " ") 140 | } 141 | cfg.mountDir = "squashfs-root" 142 | fs, err := checkDeps(cfg, fh) 143 | if err != nil { 144 | return err 145 | } 146 | if err := extractImage(cfg, fh, fs, query); err != nil { 147 | return err 148 | } 149 | fmt.Println("./" + cfg.mountDir) 150 | return fmt.Errorf("!no_return") 151 | 152 | case "--pbundle_extract_and_run", "--appimage-extract-and-run": 153 | cfg.mountOrExtract = 1 154 | fs, err := checkDeps(cfg, fh) 155 | if err != nil { 156 | return err 157 | } 158 | if err := extractImage(cfg, fh, fs, ""); err != nil { 159 | return err 160 | } 161 | *args = (*args)[1:] 162 | _ = executeFile(*args, cfg) 163 | return fmt.Errorf("!no_return") 164 | 165 | case "--pbundle_mount", "--appimage-mount": 166 | cfg.mountOrExtract = 0 167 | cfg.noCleanup = false 168 | 169 | if len(*args) == 2 && (*args)[1] != "" { 170 | if info, err := os.Stat((*args)[1]); err == nil && info.IsDir() { 171 | cfg.mountDir = (*args)[1] 172 | } else { 173 | return fmt.Errorf("error: invalid argument. The specified mount point is not a valid directory.") 174 | } 175 | } 176 | 177 | fs, err := checkDeps(cfg, fh) 178 | if err != nil { 179 | return err 180 | } 181 | if err := mountImage(cfg, fh, fs); err != nil { 182 | return err 183 | } 184 | fmt.Println(cfg.mountDir) 185 | // Is there a better way to idle? 186 | for { 187 | time.Sleep(time.Hour) 188 | } 189 | return fmt.Errorf("!no_return") 190 | 191 | case "--pbundle_offset", "--appimage-offset": 192 | fmt.Println(cfg.archiveOffset) 193 | return fmt.Errorf("!no_return") 194 | 195 | case "--pbundle_cleanup": 196 | fmt.Println("A cleanup job has been requested...") 197 | cfg.noCleanup = false 198 | cleanup(cfg) 199 | return fmt.Errorf("!no_return") 200 | 201 | default: 202 | mountOrExtract(cfg, fh) 203 | _ = executeFile(*args, cfg) 204 | } 205 | 206 | return nil 207 | } 208 | -------------------------------------------------------------------------------- /appbundle-runtime/embed.go: -------------------------------------------------------------------------------- 1 | //go:build !noEmbed 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | 12 | "github.com/liamg/memit" 13 | ) 14 | 15 | type memitCmd struct { 16 | *exec.Cmd 17 | file *os.File 18 | } 19 | 20 | func (c *memitCmd) SetStdout(w io.Writer) { 21 | c.Cmd.Stdout = w 22 | } 23 | func (c *memitCmd) SetStderr(w io.Writer) { 24 | c.Cmd.Stderr = w 25 | } 26 | func (c *memitCmd) SetStdin(r io.Reader) { 27 | c.Cmd.Stdin = r 28 | } 29 | func (c *memitCmd) CombinedOutput() ([]byte, error) { 30 | return c.Cmd.CombinedOutput() 31 | } 32 | func (c *memitCmd) Run() error { 33 | defer c.file.Close() 34 | return c.Cmd.Run() 35 | } 36 | 37 | func newMemitCmd(cfg *RuntimeConfig, binary []byte, name string, args ...string) (*memitCmd, error) { 38 | if getEnv(globalEnv, "NO_MEMFDEXEC") == "1" { 39 | tempDir := filepath.Join(cfg.workDir, ".static") 40 | if err := os.MkdirAll(tempDir, 0755); err != nil { 41 | return nil, fmt.Errorf("failed to create temporary directory: %v", err) 42 | } 43 | tempFile := filepath.Join(tempDir, name) 44 | if err := os.WriteFile(tempFile, binary, 0755); err != nil { 45 | return nil, fmt.Errorf("failed to write temporary file: %v", err) 46 | } 47 | cmd := exec.Command(tempFile, args...) 48 | cmd.Env = globalEnv 49 | return &memitCmd{Cmd: cmd}, nil 50 | } 51 | cmd, file, err := memit.Command(bytes.NewReader(binary), args...) 52 | if err != nil { 53 | return nil, err 54 | } 55 | cmd.Args[0] = name 56 | cmd.Env = globalEnv 57 | return &memitCmd{Cmd: cmd, file: file}, nil 58 | } 59 | 60 | func checkDeps(cfg *RuntimeConfig, fh *fileHandler) (*Filesystem, error) { 61 | fs, ok := getFilesystem(cfg.appBundleFS) 62 | if !ok { 63 | return nil, fmt.Errorf("unsupported filesystem: %s", cfg.appBundleFS) 64 | } 65 | return fs, nil 66 | } 67 | 68 | -------------------------------------------------------------------------------- /appbundle-runtime/embed_dwarfs.go: -------------------------------------------------------------------------------- 1 | //go:build !noEmbed && !squashfs 2 | 3 | package main 4 | 5 | import ( 6 | _ "embed" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | //go:embed binaryDependencies/dwarfs 12 | var dwarfsBinary []byte 13 | 14 | var Filesystems = []*Filesystem{ 15 | &Filesystem{ 16 | Type: "dwarfs", 17 | Commands: []string{"dwarfs", "dwarfsextract"}, 18 | MountCmd: func(cfg *RuntimeConfig) CommandRunner { 19 | cacheSize := getDwarfsCacheSize() 20 | args := []string{ 21 | "-o", "ro,nodev", 22 | "-o", "cache_files,no_cache_image,clone_fd", 23 | "-o", "block_allocator=" + getEnvWithDefault(globalEnv, "DWARFS_BLOCK_ALLOCATOR", DWARFS_BLOCK_ALLOCATOR), 24 | "-o", getEnvWithDefault(globalEnv, "DWARFS_TIDY_STRATEGY", DWARFS_TIDY_STRATEGY), 25 | "-o", "debuglevel=" + T(getEnv(globalEnv, "ENABLE_FUSE_DEBUG") != "", "debug", "error"), 26 | "-o", "readahead=" + getEnvWithDefault(globalEnv, "DWARFS_READAHEAD", DWARFS_READAHEAD), 27 | "-o", "blocksize=" + getEnvWithDefault(globalEnv, "DWARFS_BLOCKSIZE", DWARFS_BLOCKSIZE), 28 | "-o", "cachesize=" + cacheSize, 29 | "-o", "workers=" + getDwarfsWorkers(&cacheSize), 30 | "-o", fmt.Sprintf("offset=%d", cfg.archiveOffset), 31 | cfg.selfPath, 32 | cfg.mountDir, 33 | } 34 | if e := getEnv(globalEnv, "DWARFS_ANALYSIS_FILE"); e != "" { 35 | args = append(args, "-o", "analysis_file="+e) 36 | } 37 | if e := getEnv(globalEnv, "DWARFS_PRELOAD_ALL"); e != "" { 38 | args = append(args, "-o", "preload_all") 39 | } else { 40 | args = append(args, "-o", "preload_category=hotness") 41 | } 42 | memitCmd, err := newMemitCmd(cfg, dwarfsBinary, "dwarfs", args...) 43 | if err != nil { 44 | logError("Failed to create memit command", err, cfg) 45 | } 46 | return memitCmd 47 | }, 48 | ExtractCmd: func(cfg *RuntimeConfig, query string) CommandRunner { 49 | args := []string{ 50 | "--input", cfg.selfPath, 51 | "--image-offset", fmt.Sprintf("%d", cfg.archiveOffset), 52 | "--output", cfg.mountDir, 53 | } 54 | if query != "" { 55 | for _, pattern := range strings.Split(query, " ") { 56 | args = append(args, "--pattern", pattern) 57 | } 58 | } 59 | memitCmd, err := newMemitCmd(cfg, dwarfsBinary, "dwarfsextract", args...) 60 | if err != nil { 61 | logError("Failed to create memit command", err, cfg) 62 | } 63 | return memitCmd 64 | }, 65 | }, 66 | } 67 | 68 | -------------------------------------------------------------------------------- /appbundle-runtime/embed_squashfs.go: -------------------------------------------------------------------------------- 1 | //go:build !noEmbed && squashfs 2 | 3 | package main 4 | 5 | import ( 6 | _ "embed" 7 | "fmt" 8 | "strings" 9 | ) 10 | 11 | //go:embed binaryDependencies/squashfuse 12 | var squashfuseBinary []byte 13 | 14 | //go:embed binaryDependencies/unsquashfs 15 | var unsquashfsBinary []byte 16 | 17 | var Filesystems = []*Filesystem{ 18 | &Filesystem{ 19 | Type: "squashfs", 20 | Commands: []string{"squashfuse", "unsquashfs"}, 21 | MountCmd: func(cfg *RuntimeConfig) CommandRunner { 22 | args := []string{ 23 | "-o", "ro,nodev", 24 | "-o", "uid=0,gid=0", 25 | "-o", fmt.Sprintf("offset=%d", cfg.archiveOffset), 26 | cfg.selfPath, 27 | cfg.mountDir, 28 | } 29 | if getEnv(globalEnv, "ENABLE_FUSE_DEBUG") != "" { 30 | logWarning("squashfuse's debug mode implies foreground. The AppRun won't be called.") 31 | args = append(args, "-o", "debug") 32 | } 33 | memitCmd, err := newMemitCmd(cfg, squashfuseBinary, "squashfuse", args...) 34 | if err != nil { 35 | logError("Failed to create memit command", err, cfg) 36 | } 37 | return memitCmd 38 | }, 39 | ExtractCmd: func(cfg *RuntimeConfig, query string) CommandRunner { 40 | args := []string{"-d", cfg.mountDir, "-o", fmt.Sprintf("%d", cfg.archiveOffset), cfg.selfPath} 41 | if query != "" { 42 | for _, file := range strings.Split(query, " ") { 43 | args = append(args, "-e", file) 44 | } 45 | } 46 | memitCmd, err := newMemitCmd(cfg, unsquashfsBinary, "unsquashfs", args...) 47 | if err != nil { 48 | logError("Failed to create memit command", err, cfg) 49 | } 50 | return memitCmd 51 | }, 52 | }, 53 | } 54 | 55 | -------------------------------------------------------------------------------- /appbundle-runtime/noEmbed.go: -------------------------------------------------------------------------------- 1 | //go:build noEmbed 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "debug/elf" 10 | "strings" 11 | "runtime" 12 | "io" 13 | "archive/tar" 14 | "bytes" 15 | "path/filepath" 16 | 17 | "github.com/klauspost/compress/zstd" 18 | 19 | ) 20 | 21 | const runtimeEdition = "noEmbed" 22 | 23 | type osExecCmd struct { 24 | *exec.Cmd 25 | } 26 | 27 | func (c *osExecCmd) SetStdout(w io.Writer) { c.Cmd.Stdout = w } 28 | func (c *osExecCmd) SetStderr(w io.Writer) { c.Cmd.Stderr = w } 29 | func (c *osExecCmd) SetStdin(r io.Reader) { c.Cmd.Stdin = r } 30 | func (c *osExecCmd) CombinedOutput() ([]byte, error) { return c.Cmd.CombinedOutput() } 31 | 32 | var Filesystems = []*Filesystem{ 33 | { 34 | Type: "squashfs", 35 | Commands: []string{"squashfuse", "unsquashfs"}, 36 | MountCmd: func(cfg *RuntimeConfig) CommandRunner { 37 | executable, err := lookPath("squashfuse", globalPath) 38 | if err != nil { 39 | println(globalPath) 40 | logError("squashfuse not available", err, cfg) 41 | } 42 | args := []string{ 43 | "-o", "ro,nodev", 44 | "-o", "uid=0,gid=0", 45 | "-o", fmt.Sprintf("offset=%d", cfg.archiveOffset), 46 | cfg.selfPath, 47 | cfg.mountDir, 48 | } 49 | if getEnv(globalEnv, "ENABLE_FUSE_DEBUG") != "" { 50 | logWarning("squashfuse's debug mode implies foreground. The AppRun won't be called.") 51 | args = append(args, "-o", "debug") 52 | } 53 | cmd := exec.Command(executable, args...) 54 | cmd.Env = globalEnv 55 | return &osExecCmd{cmd} 56 | }, 57 | ExtractCmd: func(cfg *RuntimeConfig, query string) CommandRunner { 58 | executable, err := lookPath("unsquashfs", globalPath) 59 | if err != nil { 60 | logError("unsquashfs not available", err, cfg) 61 | } 62 | args := []string{"-d", cfg.mountDir, "-o", fmt.Sprintf("%d", cfg.archiveOffset), cfg.selfPath} 63 | if query != "" { 64 | for _, file := range strings.Split(query, " ") { 65 | args = append(args, "-e", file) 66 | } 67 | } 68 | cmd := exec.Command(executable, args...) 69 | cmd.Env = globalEnv 70 | return &osExecCmd{cmd} 71 | }, 72 | }, 73 | { 74 | Type: "dwarfs", 75 | Commands: []string{"dwarfs", "dwarfsextract"}, 76 | MountCmd: func(cfg *RuntimeConfig) CommandRunner { 77 | executable, err := lookPath("dwarfs", globalPath) 78 | if err != nil { 79 | logError("dwarfs not available", err, cfg) 80 | } 81 | cacheSize := getDwarfsCacheSize() 82 | args := []string{ 83 | "-o", "ro,nodev", 84 | "-o", "cache_files,no_cache_image,clone_fd", 85 | "-o", "block_allocator=" + getEnvWithDefault(globalEnv, "DWARFS_BLOCK_ALLOCATOR", DWARFS_BLOCK_ALLOCATOR), 86 | "-o", getEnvWithDefault(globalEnv, "DWARFS_TIDY_STRATEGY", DWARFS_TIDY_STRATEGY), 87 | "-o", "debuglevel=" + T(getEnv(globalEnv, "ENABLE_FUSE_DEBUG") != "", "debug", "error"), 88 | "-o", "readahead=" + getEnvWithDefault(globalEnv, "DWARFS_READAHEAD", DWARFS_READAHEAD), 89 | "-o", "blocksize=" + getEnvWithDefault(globalEnv, "DWARFS_BLOCKSIZE", DWARFS_BLOCKSIZE), 90 | "-o", "cachesize=" + cacheSize, 91 | "-o", "workers=" + getDwarfsWorkers(&cacheSize), 92 | "-o", fmt.Sprintf("offset=%d", cfg.archiveOffset), 93 | cfg.selfPath, 94 | cfg.mountDir, 95 | } 96 | if e := getEnv(globalEnv, "DWARFS_ANALYSIS_FILE"); e != "" { 97 | args = append(args, "-o", "analysis_file="+e) 98 | } 99 | if e := getEnv(globalEnv, "DWARFS_PRELOAD_ALL"); e != "" { 100 | args = append(args, "-o", "preload_all") 101 | } else { 102 | args = append(args, "-o", "preload_category=hotness") 103 | } 104 | cmd := exec.Command(executable, args...) 105 | cmd.Env = globalEnv 106 | return &osExecCmd{cmd} 107 | }, 108 | ExtractCmd: func(cfg *RuntimeConfig, query string) CommandRunner { 109 | executable, err := lookPath("dwarfsextract", globalPath) 110 | if err != nil { 111 | logError("dwarfsextract not available", err, cfg) 112 | } 113 | args := []string{ 114 | "--input", cfg.selfPath, 115 | "--image-offset", fmt.Sprintf("%d", cfg.archiveOffset), 116 | "--output", cfg.mountDir, 117 | } 118 | if query != "" { 119 | for _, pattern := range strings.Split(query, " ") { 120 | args = append(args, "--pattern", pattern) 121 | } 122 | } 123 | cmd := exec.Command(executable, args...) 124 | cmd.Env = globalEnv 125 | return &osExecCmd{cmd} 126 | }, 127 | }, 128 | } 129 | 130 | func (f *fileHandler) extractStaticTools(cfg *RuntimeConfig) error { 131 | elfFile, err := elf.NewFile(f.file) 132 | if err != nil { 133 | return fmt.Errorf("parse ELF: %w", err) 134 | } 135 | 136 | staticToolsSection := elfFile.Section(".pbundle_static_tools") 137 | if staticToolsSection == nil { 138 | return fmt.Errorf("static_tools section not found") 139 | } 140 | 141 | staticToolsData, err := staticToolsSection.Data() 142 | if err != nil { 143 | return fmt.Errorf("failed to read static_tools section: %w", err) 144 | } 145 | 146 | decoder, err := zstd.NewReader(bytes.NewReader(staticToolsData)) 147 | if err != nil { 148 | return fmt.Errorf("zstd init: %w", err) 149 | } 150 | defer decoder.Close() 151 | 152 | sizeCache := make(map[string]int64) 153 | err = filepath.Walk(cfg.staticToolsDir, func(path string, info os.FileInfo, err error) error { 154 | if err != nil { 155 | return err 156 | } 157 | if !info.IsDir() { 158 | relPath, err := filepath.Rel(cfg.staticToolsDir, path) 159 | if err != nil { 160 | return err 161 | } 162 | sizeCache[relPath] = info.Size() 163 | } 164 | return nil 165 | }) 166 | if err != nil { 167 | return fmt.Errorf("failed to cache file sizes: %w", err) 168 | } 169 | 170 | tr := tar.NewReader(decoder) 171 | for { 172 | hdr, err := tr.Next() 173 | if err == io.EOF { 174 | break 175 | } 176 | if err != nil { 177 | return fmt.Errorf("tar read: %w", err) 178 | } 179 | 180 | fpath := filepath.Join(cfg.staticToolsDir, hdr.Name) 181 | relPath, err := filepath.Rel(cfg.staticToolsDir, fpath) 182 | if err != nil { 183 | return fmt.Errorf("failed to get relative path: %w", err) 184 | } 185 | 186 | if _, exists := sizeCache[relPath]; exists { 187 | continue 188 | } 189 | 190 | switch hdr.Typeflag { 191 | case tar.TypeDir: 192 | if err := os.MkdirAll(fpath, 0755); err != nil { 193 | return fmt.Errorf("mkdir %s: %w", fpath, err) 194 | } 195 | case tar.TypeReg: 196 | if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { 197 | return fmt.Errorf("mkdir parent: %w", err) 198 | } 199 | f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode)) 200 | if err != nil { 201 | return fmt.Errorf("create: %w", err) 202 | } 203 | _, err = io.Copy(f, tr) 204 | f.Close() 205 | if err != nil { 206 | return fmt.Errorf("write: %w", err) 207 | } 208 | case tar.TypeSymlink: 209 | if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { 210 | return fmt.Errorf("mkdir parent: %w", err) 211 | } 212 | if err := os.Symlink(hdr.Linkname, fpath); err != nil { 213 | return fmt.Errorf("symlink: %w", err) 214 | } 215 | case tar.TypeLink: 216 | if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { 217 | return fmt.Errorf("mkdir parent: %w", err) 218 | } 219 | if err := os.Link(hdr.Linkname, fpath); err != nil { 220 | return fmt.Errorf("hardlink: %w", err) 221 | } 222 | } 223 | } 224 | 225 | return nil 226 | } 227 | 228 | func checkDeps(cfg *RuntimeConfig, fh *fileHandler) (*Filesystem, error) { 229 | fs, ok := getFilesystem(cfg.appBundleFS) 230 | if !ok { 231 | return nil, fmt.Errorf("unsupported filesystem: %s", cfg.appBundleFS) 232 | } 233 | 234 | updatePath("PATH", cfg.staticToolsDir) 235 | var missingCmd bool 236 | for _, cmd := range fs.Commands { 237 | if _, err := lookPath(cmd, globalPath); err != nil { 238 | missingCmd = true 239 | break 240 | } 241 | } 242 | 243 | if missingCmd { 244 | if err := os.MkdirAll(cfg.staticToolsDir, 0755); err != nil { 245 | return nil, fmt.Errorf("failed to create static tools directory: %v", err) 246 | } 247 | 248 | if err := fh.extractStaticTools(cfg); err != nil { 249 | return nil, fmt.Errorf("failed to extract static tools: %v", err) 250 | } 251 | } 252 | 253 | return fs, nil 254 | } 255 | 256 | -------------------------------------------------------------------------------- /assets/AppRun.gccToolchain: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -n "$SELF" ]; then # Check if ARGV0 is set, which should contain the original command name 4 | SELF=$(basename "$SELF") 5 | else 6 | SELF=$(basename "$0") # Fallback to $0 if ARGV0 is not set, but this shouldn't happen with proper symlink setup 7 | fi 8 | 9 | SELF_TEMPDIR="$(dirname "$0")" 10 | 11 | # Set PATH to include local directories 12 | export PATH="$SELF_TEMPDIR/usr/bin:$SELF_TEMPDIR/bin:$PATH" 13 | 14 | # Set C_INCLUDE_PATH if it exists, or just initialize it 15 | if [ -n "$C_INCLUDE_PATH" ]; then 16 | export C_INCLUDE_PATH="$SELF_TEMPDIR/usr/include:$SELF_TEMPDIR/include:$C_INCLUDE_PATH" 17 | else 18 | export C_INCLUDE_PATH="$SELF_TEMPDIR/usr/include:$SELF_TEMPDIR/include" 19 | fi 20 | 21 | # Set CPLUS_INCLUDE_PATH if it exists, or just initialize it 22 | if [ -n "$CPLUS_INCLUDE_PATH" ]; then 23 | export CPLUS_INCLUDE_PATH="$SELF_TEMPDIR/usr/include:$SELF_TEMPDIR/include:$CPLUS_INCLUDE_PATH" 24 | else 25 | export CPLUS_INCLUDE_PATH="$SELF_TEMPDIR/usr/include:$SELF_TEMPDIR/include" 26 | fi 27 | 28 | # Set LIBRARY_PATH if it exists, or just initialize it 29 | if [ -n "$LIBRARY_PATH" ]; then 30 | export LIBRARY_PATH="$SELF_TEMPDIR/usr/lib:$SELF_TEMPDIR/lib:$LIBRARY_PATH" 31 | else 32 | export LIBRARY_PATH="$SELF_TEMPDIR/usr/lib:$SELF_TEMPDIR/lib" 33 | fi 34 | 35 | # Set PKG_CONFIG_PATH if it exists, or just initialize it 36 | if [ -n "$PKG_CONFIG_PATH" ]; then 37 | export PKG_CONFIG_PATH="$SELF_TEMPDIR/usr/lib/pkgconfig:$SELF_TEMPDIR/lib/pkgconfig:$PKG_CONFIG_PATH" 38 | else 39 | export PKG_CONFIG_PATH="$SELF_TEMPDIR/usr/lib/pkgconfig:$SELF_TEMPDIR/lib/pkgconfig" 40 | fi 41 | 42 | # Set CFLAGS if it exists, or just initialize it 43 | if [ -n "$CFLAGS" ]; then 44 | export CFLAGS="-I$SELF_TEMPDIR/usr/include -I$SELF_TEMPDIR/include $CFLAGS" 45 | else 46 | export CFLAGS="-I$SELF_TEMPDIR/usr/include -I$SELF_TEMPDIR/include" 47 | fi 48 | 49 | # Set LDFLAGS if it exists, or just initialize it 50 | if [ -n "$LDFLAGS" ]; then 51 | export LDFLAGS="-L$SELF_TEMPDIR/usr/lib -L$SELF_TEMPDIR/lib $LDFLAGS" 52 | else 53 | export LDFLAGS="-L$SELF_TEMPDIR/usr/lib -L$SELF_TEMPDIR/lib" 54 | fi 55 | 56 | # Check if the binary exists in the specified directories and execute it 57 | if [ -f "$SELF_TEMPDIR/bin/$SELF" ]; then 58 | exec "$SELF_TEMPDIR/bin/$SELF" "$@" 59 | elif [ -f "$SELF_TEMPDIR/usr/bin/$SELF" ]; then 60 | exec "$SELF_TEMPDIR/usr/bin/$SELF" "$@" 61 | fi 62 | 63 | if [ "$#" -lt 1 ]; then 64 | echo "No arguments were passed or the command does not match any binaries in bin/ or usr/bin/" 65 | else 66 | exec "$@" 67 | fi 68 | -------------------------------------------------------------------------------- /assets/AppRun.generic: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Taken from https://github.com/Samueru-sama/deploy-linux.sh/blob/d608f71a55bb4368b4fab3eb203b96dff6a71c43/deploy-linux.sh#L42C2-L95C4 3 | # Autogenerated AppRun 4 | # Simplified version of the AppRun that go-appimage makes 5 | 6 | # shellcheck disable=SC2086 7 | [ -n "$DEBUG" ] && set -$DEBUG 8 | 9 | HERE="$(dirname "$(readlink -f "${0}")")" 10 | BIN="$ARGV0" 11 | unset ARGVO 12 | BIN_DIR="$HERE/usr/bin" 13 | LIB_DIR="$HERE/usr/lib" 14 | SHARE_DIR="$HERE/usr/share" 15 | SCHEMA_HERE="$SHARE_DIR/glib-2.0/runtime-schemas:$SHARE_DIR/glib-2.0/schemas" 16 | LD_LINUX="$(find "$HERE" -name 'ld-*.so.*' -print -quit)" 17 | PY_HERE="$(find "$LIB_DIR" -type d -name 'python*' -print -quit)" 18 | QT_HERE="$HERE/usr/plugins" 19 | GTK_HERE="$(find "$LIB_DIR" -name 'gtk-*' -type d -print -quit)" 20 | GDK_HERE="$(find "$HERE" -type d -regex '.*gdk.*loaders' -print -quit)" 21 | GDK_LOADER="$(find "$HERE" -type f -regex '.*gdk.*loaders.cache' -print -quit)" 22 | 23 | if [ ! -e "$BIN_DIR/$BIN" ]; then 24 | BIN="$(awk -F"=| " '/Exec=/{print $2; exit}' "$HERE"/*.desktop)" 25 | fi 26 | export PATH="$BIN_DIR:$PATH" 27 | export XDG_DATA_DIRS="$SHARE_DIR:$XDG_DATA_DIRS" 28 | if [ -n "$PY_HERE" ]; then 29 | export PYTHONHOME="$PY_HERE" 30 | fi 31 | if [ -d "$SHARE_DIR"/perl5 ] || [ -d "$LIB_DIR"/perl5 ]; then 32 | export PERLLIB="$SHARE_DIR/perl5:$LIB_DIR/perl5:$PERLLIB" 33 | fi 34 | if [ -d "$QT_HERE" ]; then 35 | export QT_PLUGIN_PATH="$QT_HERE" 36 | fi 37 | if [ -d "$GTK_HERE" ]; then 38 | export GTK_PATH="$GTK_HERE" \ 39 | GTK_EXE_PREFIX="$HERE/usr" \ 40 | GTK_DATA_PREFIX="$HERE/usr" 41 | fi 42 | 43 | TARGET="$BIN_DIR/$BIN" 44 | # deploy everything mode 45 | if [ -n "$LD_LINUX" ] ; then 46 | export GTK_THEME=Default \ 47 | GCONV_PATH="$LIB_DIR"/gconv \ 48 | GDK_PIXBUF_MODULEDIR="$GDK_HERE" \ 49 | GDK_PIXBUF_MODULE_FILE="$GDK_LOADER" \ 50 | FONTCONFIG_FILE="/etc/fonts/fonts.conf" \ 51 | GSETTINGS_SCHEMA_DIR="$SCHEMA_HERE:$GSETTINGS_SCHEMA_DIR" 52 | if echo "$LD_LINUX" | grep -qi musl; then 53 | exec "$LD_LINUX" "$TARGET" "$@" 54 | else 55 | exec "$LD_LINUX" --inhibit-cache "$TARGET" "$@" 56 | fi 57 | else 58 | exec "$TARGET" "$@" 59 | fi 60 | -------------------------------------------------------------------------------- /assets/AppRun.goToolchain: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #: "${GOCACHE:=$HOME/.cache/go}" 4 | #: "${GOBIN:=$HOME/.local/bin}" 5 | #: "${GOPATH:=$HOME/.cache/go}" 6 | #: "${CGO_ENABLED:=0}" 7 | #: "${GOFLAGS:=-ldflags=-static -ldflags=-s -ldflags=-w}" 8 | #: "${GO_LDFLAGS:=-buildmode=pie}" 9 | #: "${GOROOT:=$SELF_TEMPDIR}" 10 | 11 | GOROOT="$SELF_TEMPDIR" exec "${SELF_TEMPDIR}/bin/go" "$@" 12 | -------------------------------------------------------------------------------- /assets/AppRun.multiBinary: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -n "$SELF" ]; then # Check if ARGV0 is set, which should contain the original command name 4 | SELF=$(basename "$SELF") 5 | else 6 | SELF=$(basename "$0") # Fallback to $0 if ARGV0 is not set, but this shouldn't happen with proper symlink setup 7 | fi 8 | 9 | SELF_TEMPDIR="$(dirname "$0")" 10 | 11 | # Check if the binary exists in the specified directories and execute it 12 | if [ -f "$SELF_TEMPDIR/bin/$SELF" ]; then 13 | exec "$SELF_TEMPDIR/bin/$SELF" "$@" 14 | elif [ -f "$SELF_TEMPDIR/usr/bin/$SELF" ]; then 15 | exec "$SELF_TEMPDIR/usr/bin/$SELF" "$@" 16 | fi 17 | 18 | if [ "$#" -lt 1 ]; then 19 | echo "No arguments were passed or the command does not match any binaries in bin/ or usr/bin/" 20 | else 21 | exec "$@" 22 | fi 23 | -------------------------------------------------------------------------------- /assets/AppRun.override: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # For converting a .NixAppImage to the .AppBundle format, also for AppDirs that have been patched away with patchelf 4 | # Move original AppRun over to ./AppRun_original and put this script at ./AppRun 5 | 6 | SELF_TEMPDIR="$(dirname "$(readlink -f "${0}")")" 7 | exec env -u LD_LIBRARY_PATH -u LD_PRELOAD "${SELF_TEMPDIR}/AppRun_original" "$@" 8 | -------------------------------------------------------------------------------- /assets/AppRun.rootfs-based: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # DATE OF LAST REVISION: 26-05-2025 4 | 5 | # shellcheck disable=SC2086 6 | [ -n "$DEBUG" ] && set -$DEBUG 7 | 8 | # Let people use an external AppRun 9 | [ "$EXT_APPRUN" = "1" ] || { 10 | # Determine the path to the AppRun itself 11 | SELF="$(readlink -f "$0")" 12 | APPDIR="${SELF%/*}" 13 | } 14 | 15 | [ -d "$APPDIR/rootfs" ] && BWROOTFS="$APPDIR/rootfs" 16 | [ -d "$APPDIR/proto" ] && BWROOTFS="$APPDIR/proto" 17 | 18 | # Find bwrap 19 | BWRAP_BIN="${APPDIR}/usr/bin/bwrap" 20 | [ ! -f "$BWRAP_BIN" ] && BWRAP_BIN="bwrap" 21 | 22 | # Default ARGV0 23 | [ -z "$ARGV0" ] && ARGV0="${0##*/}" 24 | 25 | # Forces the use of things contained within the rootfs 26 | PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" 27 | 28 | bool() { 29 | case "$1" in 30 | false | 0) echo "0" ;; 31 | true | 1) echo "1" ;; 32 | *) echo "Invalid boolean value: $1" >&2; exit 1 ;; 33 | esac 34 | } 35 | 36 | # Function to check if a feature is enabled by file presence 37 | is_enabled() { 38 | _propName="$1" 39 | [ -f "$APPDIR/.enabled/$_propName" ] && echo "1" && return 0 40 | [ -f "$APPDIR/.disabled/$_propName" ] && echo "0" && return 0 41 | echo "$2" ; return 5 42 | } 43 | 44 | # Defaults with file-based overrides 45 | SHARE_LOOK="$(is_enabled "SHARE_LOOK" 1)" 46 | SHARE_FONTS="$(is_enabled "SHARE_FONTS" 1)" 47 | SHARE_AUDIO="$(is_enabled "SHARE_AUDIO" 1)" 48 | SHARE_XDG_RUNTIME_DIR="$(is_enabled "SHARE_XDG_RUNTIME_DIR" 1)" 49 | SHARE_OPT="$(is_enabled "SHARE_OPT" 1)" 50 | UID0_GID0="$(is_enabled "UID0_GID0" 0)" 51 | 52 | _sh_cat() { 53 | while IFS= read -r line; do 54 | echo "$line" 55 | done < "$1" 56 | } 57 | # Set default cmd 58 | SELF_ARGS="-- $(_sh_cat "$APPDIR/entrypoint")" 59 | 60 | # Parse other arguments 61 | while [ "$#" -gt 0 ]; do 62 | case "$1" in 63 | --Xbwrap) 64 | shift 65 | SELF_ARGS="$*" 66 | SHARE_XDG_RUNTIME_DIR=0 67 | SHARE_AUDIO=0 68 | SHARE_LOOK=0 69 | SHARE_FONTS=0 70 | SHARE_OPT=0 71 | break 72 | ;; 73 | --Xbwrap-XdgRuntimeDir) 74 | SHARE_XDG_RUNTIME_DIR=$(bool "$2") # Shares the entire XDG_RUNTIME_DIR 75 | shift 76 | ;; 77 | --Xbwrap-audio) 78 | SHARE_AUDIO=$(bool "$2") # Shares the audio sockets that pipewire and pulseaudio need. As well as the Alsa config at /etc 79 | shift 80 | ;; 81 | --Xbwrap-look) 82 | SHARE_LOOK=$(bool "$2") # Shares the icons & themes directories with the rootfs 83 | shift 84 | ;; 85 | --Xbwrap-opt) 86 | SHARE_OPT=$(bool "$2") # Binds /opt 87 | shift 88 | ;; 89 | --Xbwrap-hostFonts) 90 | SHARE_FONTS=$(bool "$2") # Shares the host's fonts 91 | shift 92 | ;; 93 | --Xbwrap-uid0gid0) 94 | UID0_GID0=$(bool "$2") # Enables --uid 0 and --gid 0, effectively tricking the program within the rootfs into thinking that you have superuser rights 95 | shift 96 | ;; 97 | *) 98 | SELF_ARGS="$SELF_ARGS $1" 99 | ;; 100 | esac 101 | shift 102 | done 103 | 104 | # Check for existing entrypoint execution 105 | if [ "$WITHIN_BWRAP" = 1 ] && [ -f "/entrypoint" ]; then 106 | exec "/entrypoint" 107 | fi 108 | 109 | # Function to build bwrap options 110 | build_bwrap_options() { 111 | #BWRAP_OPTIONS="\ 112 | # --dev-bind / / \ 113 | # --ro-bind-try $BWROOTFS/usr/lib /usr/lib \ 114 | # --ro-bind-try $BWROOTFS/usr/lib64 /usr/lib64 \ 115 | # --ro-bind-try $BWROOTFS/usr/lib32 /usr/lib32 \ 116 | # --ro-bind-try $BWROOTFS/lib /lib \ 117 | # --ro-bind-try $BWROOTFS/lib64 /lib64 \ 118 | # --ro-bind-try $BWROOTFS/lib32 /lib32 \ 119 | # --ro-bind-try $BWROOTFS/usr/bin /usr/bin \ 120 | # --ro-bind-try $BWROOTFS/usr/sbin /usr/sbin \ 121 | # --ro-bind-try $BWROOTFS/bin /bin \ 122 | # --ro-bind-try $BWROOTFS/sbin /sbin \ 123 | # --ro-bind-try $BWROOTFS/etc /etc \ 124 | # --setenv BWROOTFS \"$BWROOTFS\" \ 125 | # --setenv ARGV0 \"$ARGV0\" \ 126 | # --setenv ARGS \"$SELF_ARGS\" \ 127 | # --setenv WITHIN_BWRAP \"1\" \ 128 | # --cap-add CAP_NET_BIND_SERVICE \ 129 | # --cap-add CAP_SYS_ADMIN" 130 | 131 | BWRAP_OPTIONS="--bind $BWROOTFS / \ 132 | --share-net \ 133 | --dev-bind /dev /dev \ 134 | --ro-bind-try /run /run \ 135 | --ro-bind-try /sys /sys \ 136 | --ro-bind-try /media /media \ 137 | --ro-bind-try /mnt /mnt \ 138 | --ro-bind-try /etc/localtime /etc/localtime \ 139 | --ro-bind-try /etc/machine-id /etc/machine-id \ 140 | --ro-bind-try /etc/resolv.conf /etc/resolv.conf \ 141 | --ro-bind-try /lib/firmware /lib/firmware \ 142 | --ro-bind-try /etc/passwd /etc/passwd \ 143 | --ro-bind-try /etc/groups /etc/groups \ 144 | --ro-bind-try /etc/hosts /etc/hosts \ 145 | --ro-bind-try /etc/nsswitch.conf /etc/nsswitch.conf \ 146 | --ro-bind-try /etc/hostname /etc/hostname \ 147 | --ro-bind-try \"$APPDIR\" /app \ 148 | --bind-try \"${TMPDIR:-/tmp}\" \"${TMPDIR:-/tmp}\" \ 149 | --bind-try /home /home \ 150 | --setenv SELF \"$SELF\" \ 151 | --setenv APPDIR \"$APPDIR\" \ 152 | --setenv BWROOTFS \"$BWROOTFS\" \ 153 | --setenv ARGV0 \"$ARGV0\" \ 154 | --setenv ARGS \"$SELF_ARGS\" \ 155 | --setenv WITHIN_BWRAP \"1\" \ 156 | --proc /proc \ 157 | --cap-add CAP_NET_BIND_SERVICE \ 158 | --cap-add CAP_SYS_ADMIN" 159 | # Conditionally add optional directories 160 | 161 | # Arch is a BS os, and programs get installed to /opt 162 | if [ "$SHARE_OPT" = 1 ]; then 163 | [ -d /opt ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /opt /opt" 164 | fi 165 | 166 | # Themes & Icons 167 | if [ "$SHARE_LOOK" = "1" ]; then 168 | [ -d /usr/share/icons ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/icons /usr/share/icons" 169 | [ -d /usr/share/themes ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/themes /usr/share/themes" 170 | fi 171 | 172 | # Fonts 173 | if [ "$SHARE_FONTS" = 1 ]; then 174 | [ -d /usr/share/fontconfig ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/fontconfig /usr/share/fontconfig" 175 | [ -d /usr/share/fonts ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/fonts /usr/share/fonts" 176 | fi 177 | 178 | fUID="$(id -u)" 179 | [ -z "$XDG_RUNTIME_DIR" ] && [ -d "/run/user/$fUID" ] && XDG_RUNTIME_DIR="/run/user/$fUID" 180 | 181 | # Add optional XDG_RUNTIME_DIR binding if enabled 182 | [ "$SHARE_XDG_RUNTIME_DIR" = "1" ] && [ -n "$XDG_RUNTIME_DIR" ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try $XDG_RUNTIME_DIR $XDG_RUNTIME_DIR" 183 | 184 | # Add optional audio bindings if enabled 185 | if [ "$SHARE_AUDIO" = "1" ] && [ -n "$XDG_RUNTIME_DIR" ]; then 186 | for __E in "/etc/asound.conf" "$XDG_RUNTIME_DIR/pulse" "$XDG_RUNTIME_DIR/pipewire-0" "$XDG_RUNTIME_DIR/pipewire-0.lock" "$XDG_RUNTIME_DIR/pipewire-0-manager" "$XDG_RUNTIME_DIR/pipewire-0-manager.lock"; do 187 | if [ -f "$__E" ] || [ -d "$__E" ]; then 188 | if [ -f "$BWROOTFS/$__E" ] || [ -d "$BWROOTFS/$__E" ]; then 189 | BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try $__E $__E" 190 | fi 191 | fi 192 | done 193 | fi 194 | 195 | # We could be running from a GH runner -> See AppBundleHUB & pelfCreator. 196 | [ -d "/__w" ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /__w /__w" 197 | 198 | if [ "$UID0_GID0" = "1" ]; then 199 | BWRAP_OPTIONS="$BWRAP_OPTIONS --uid 0 --gid 0" 200 | fi 201 | 202 | if [ -d "/Users/$USER" ]; then 203 | BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /Users /Users" 204 | fi 205 | 206 | printf '%s\n' "$BWRAP_OPTIONS" 207 | } 208 | 209 | # Build and execute the bwrap options 210 | BWRAP_OPTIONS="$(build_bwrap_options "$@")" 211 | 212 | eval "exec $BWRAP_BIN $BWRAP_OPTIONS $SELF_ARGS" 213 | -------------------------------------------------------------------------------- /assets/AppRun.rootfs-based.stable: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck disable=SC2086 4 | [ -n "$DEBUG" ] && set -$DEBUG 5 | 6 | # Determine the path to the script itself 7 | SELF=$(readlink -f "$0") 8 | SELF_TEMPDIR=${SELF%/*} 9 | 10 | [ -d "$SELF_TEMPDIR/rootfs" ] || exit 1 11 | BWROOTFS="$SELF_TEMPDIR/rootfs" 12 | 13 | if [ "$1" = "--Xbwrap" ]; then 14 | shift 15 | SELF_ARGS="$*" 16 | else 17 | SELF_ARGS="-- env -u LD_PRELOAD -u LD_LIBRARY_PATH $(cat "$BWROOTFS/entrypoint") $*" 18 | fi 19 | 20 | if [ -f "$SELF_TEMPDIR/usr/bin/bwrap" ]; then 21 | BWRAP_BIN="$SELF_TEMPDIR/usr/bin/bwrap" 22 | else 23 | BWRAP_BIN="bwrap" 24 | fi 25 | 26 | if [ -z "$ARGV0" ]; then 27 | ARGV0="${0##*/}" 28 | fi 29 | 30 | if [ "$WITHIN_BWRAP" = 1 ] && [ -f "/entrypoint" ]; then 31 | "/entrypoint" 32 | fi 33 | $BWRAP_BIN --bind "$BWROOTFS" / \ 34 | --share-net \ 35 | --proc /proc \ 36 | --dev-bind /dev /dev \ 37 | --bind /run /run \ 38 | --bind-try /sys /sys \ 39 | --bind /tmp /tmp \ 40 | --bind-try /media /media \ 41 | --bind-try /mnt /mnt \ 42 | --bind /home /home \ 43 | --bind-try /opt /opt \ 44 | --bind-try /usr/share/fontconfig /usr/share/fontconfig \ 45 | --ro-bind-try /usr/share/fonts /usr/share/fonts \ 46 | --ro-bind-try /usr/share/themes /usr/share/themes \ 47 | --ro-bind-try /sys /sys \ 48 | --ro-bind-try /etc/resolv.conf /etc/resolv.conf \ 49 | --ro-bind-try /etc/hosts /etc/hosts \ 50 | --ro-bind-try /etc/nsswitch.conf /etc/nsswitch.conf \ 51 | --ro-bind-try /etc/passwd /etc/passwd \ 52 | --ro-bind-try /etc/group /etc/group \ 53 | --ro-bind-try /etc/machine-id /etc/machine-id \ 54 | --ro-bind-try /etc/asound.conf /etc/asound.conf \ 55 | --ro-bind-try /etc/localtime /etc/localtime \ 56 | --ro-bind-try /etc/hostname /etc/hostname \ 57 | --setenv SELF "$SELF" \ 58 | --setenv SELF_TEMPDIR "$SELF_TEMPDIR" \ 59 | --setenv BWROOTFS "$BWROOTFS" \ 60 | --setenv ARGV0 "$ARGV0" \ 61 | --setenv ARGS "!$*" \ 62 | --setenv WITHIN_BWRAP "1" \ 63 | $SELF_ARGS 64 | 65 | # --bind-try /usr/lib/locale /usr/lib/locale \ 66 | # --perms 0700 \ 67 | # --uid "0" --gid "0" \ 68 | -------------------------------------------------------------------------------- /assets/AppRun.sharun: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck disable=SC2086 4 | [ -n "$DEBUG" ] && set -$DEBUG 5 | 6 | SELF="$(readlink -f "$0")" && export SELF 7 | APPDIR="${SELF%/*}" && export APPDIR 8 | 9 | _sh_cat() { 10 | while IFS= read -r line; do 11 | echo "$line" 12 | done < "$1" 13 | } 14 | 15 | FALLBACK="$(_sh_cat "$APPDIR/entrypoint")" 16 | FALLBACK="${FALLBACK##*/}" 17 | [ -z "$ARGV0" ] && { 18 | ARGV0="${0##*/}" 19 | } 20 | 21 | CMD="$1" 22 | 23 | oPATH="$PATH" 24 | PATH="${APPDIR}/bin" 25 | 26 | # What command shall we exec? 27 | if _cmd="$(command -v "${ARGV0#./}")" >/dev/null 2>&1; then 28 | PATH="$PATH:$oPATH" 29 | elif _cmd="$(command -v "$CMD")" >/dev/null 2>&1; then 30 | shift 31 | PATH="$PATH:$oPATH" 32 | elif _cmd="$(command -v "$FALLBACK")" >/dev/null 2>&1; then 33 | PATH="$PATH:$oPATH" 34 | else 35 | echo "Error: Neither ARGV0 ('${ARGV0%.*}') nor ARGS ('$CMD') are available in \$PATH" 36 | exit 1 37 | fi 38 | 39 | exec "$_cmd" "$@" 40 | -------------------------------------------------------------------------------- /assets/AppRun.sharun.ovfsProto: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck disable=SC2086 4 | [ -n "$DEBUG" ] && set -$DEBUG 5 | 6 | # Let people use an external AppRun 7 | [ "$EXT_APPRUN" = "1" ] || { 8 | # Determine the path to the AppRun itself 9 | SELF="$(readlink -f "$0")" 10 | APPDIR="${SELF%/*}" 11 | } 12 | 13 | _sh_cat() { 14 | while IFS= read -r line; do 15 | echo "$line" 16 | done < "$1" 17 | } 18 | 19 | FALLBACK="$(_sh_cat "$APPDIR/entrypoint")" 20 | FALLBACK="${FALLBACK##*/}" 21 | [ -z "$ARGV0" ] && { 22 | ARGV0="${0##*/}" 23 | } 24 | 25 | CMD="$1" 26 | 27 | # Check for proto or rootfs directories and set PROTO accordingly 28 | if [ -d "$APPDIR/proto" ]; then 29 | PROTO="$APPDIR/proto" 30 | elif [ -d "$APPDIR/rootfs" ]; then 31 | PROTO="$APPDIR/rootfs" 32 | else 33 | PROTO="$APPDIR/proto_trimmed" 34 | fi 35 | 36 | oPATH="$PATH" 37 | PATH="${APPDIR}/bin:${PROTO}/usr/local/bin:${PROTO}/sbin:${PROTO}/bin:${PROTO}/usr/bin:${PROTO}/usr/sbin" 38 | 39 | if [ "$NOSHARUN" = 1 ]; then 40 | if [ -z "$LD_LIBRARY_PATH" ]; then 41 | LD_LIBRARY_PATH="${APPDIR}/shared/lib:${PROTO}/lib:${PROTO}/usr/lib" 42 | else 43 | LD_LIBRARY_PATH="${APPDIR}/shared/lib:${PROTO}/lib:${PROTO}/usr/lib:$LD_LIBRARY_PATH" 44 | fi 45 | fi 46 | 47 | # What command shall we exec? 48 | if _cmd="$(command -v "${ARGV0#./}")" >/dev/null 2>&1; then 49 | PATH="$PATH:$oPATH" 50 | elif _cmd="$(command -v "$CMD")" >/dev/null 2>&1; then 51 | shift 52 | PATH="$PATH:$oPATH" 53 | elif _cmd="$(command -v $FALLBACK)" >/dev/null 2>&1; then 54 | PATH="$PATH:$oPATH" 55 | else 56 | echo "Error: Neither ARGV0 ('${ARGV0%.*}') nor ARGS ('$CMD') are available in \$PATH" 57 | exit 1 58 | fi 59 | 60 | if [ ! -d "$PROTO" ] || [ "$DIRECT_EXEC" = "1" ]; then 61 | eval "$_cmd" "$*" 62 | exit $? 63 | fi 64 | 65 | # proto/rootfs mode handling -> 66 | 67 | # Find unionfs-fuse 68 | UNIONFS_BIN="${APPDIR}/usr/bin/unionfs" 69 | [ ! -f "$UNIONFS_BIN" ] && UNIONFS_BIN="unionfs" 70 | 71 | # Find bwrap 72 | BWRAP_BIN="${APPDIR}/usr/bin/bwrap" 73 | [ ! -f "$BWRAP_BIN" ] && BWRAP_BIN="bwrap" 74 | 75 | _dirname() { # DIRNAME but made entirely in POSIX SH 76 | dir=${1:-.} ; dir=${dir%%"${dir##*[!/]}"} ; [ "${dir##*/*}" ] && dir=. ; dir=${dir%/*} ; dir=${dir%%"${dir##*[!/]}"} ; printf '%s\n' "${dir:-/}" 77 | } 78 | 79 | # Set up unionfs directories using _dirname on APPDIR 80 | UNIONFS_DIR="$(_dirname "$APPDIR")/unionfs" 81 | mkdir -p "$UNIONFS_DIR" 82 | 83 | # Create temp directories for unionfs 84 | TEMP_DIR="$(mktemp -d)" 85 | MOUNT_DIR="$TEMP_DIR/mount_dir" 86 | mkdir -p "$MOUNT_DIR" 87 | 88 | # Mount the unionfs 89 | # Note: Using CoW (copy-on-write) and preserving branch for better compatibility 90 | "$UNIONFS_BIN" -o cow,preserve_branch "$PROTO=RO:/=RW" "$MOUNT_DIR" 91 | 92 | cleanup() { 93 | ( 94 | # Attempt to unmount 95 | fusermount -u "$MOUNT_DIR" 2>/dev/null 96 | 97 | # Wait and check if the mount point is unmounted 98 | for i in 1 2 3 4 5; do 99 | if mountpoint -q "$MOUNT_DIR"; then 100 | sleep "$i" 101 | else 102 | break 103 | fi 104 | done 105 | 106 | # Force unmount if still mounted 107 | if mountpoint -q "$MOUNT_DIR"; then 108 | fusermount -uz "$MOUNT_DIR" 2>/dev/null 109 | fi 110 | 111 | # Remove temporary directories 112 | rmdir "$MOUNT_DIR" 2>/dev/null || true 113 | rm -rf "$TEMP_DIR" 2>/dev/null || true 114 | rm -rf "$UNIONFS_DIR" 2>/dev/null || true 115 | ) & # Run cleanup in the background 116 | } 117 | trap cleanup INT TERM HUP QUIT EXIT 118 | 119 | bool() { 120 | case "$1" in 121 | false | 0) echo "0" ;; 122 | true | 1) echo "1" ;; 123 | *) echo "Invalid boolean value: $1" >&2; exit 1 ;; 124 | esac 125 | } 126 | 127 | # Function to check if a feature is enabled by file presence 128 | is_enabled() { 129 | _propName="$1" 130 | [ -f "$APPDIR/.enabled/$_propName" ] && echo "1" && return 0 131 | [ -f "$APPDIR/.disabled/$_propName" ] && echo "0" && return 0 132 | echo "$2" ; return 5 133 | } 134 | 135 | # Defaults with file-based overrides 136 | #SHARE_LOOK="$(is_enabled "SHARE_LOOK" 1)" 137 | #SHARE_FONTS="$(is_enabled "SHARE_FONTS" 1)" 138 | #SHARE_AUDIO="$(is_enabled "SHARE_AUDIO" 1)" 139 | SHARE_XDG_RUNTIME_DIR="$(is_enabled "SHARE_XDG_RUNTIME_DIR" 1)" 140 | SHARE_VAR="$(is_enabled "SHARE_VAR" 1)" 141 | SHARE_RUN="$(is_enabled "SHARE_RUN" 1)" 142 | UID0_GID0="$(is_enabled "UID0_GID0" 0)" 143 | 144 | # Initialize the bwrap command 145 | TMPDIR="${TMPDIR:-/tmp}" 146 | bwrap_cmd="$BWRAP_BIN --dev-bind $MOUNT_DIR / --bind-try $TMPDIR /tmp --bind-try /home /home" 147 | [ "$UID0_GID0" = "1" ] && bwrap_cmd="$bwrap_cmd --uid 0 --gid 0" 148 | [ "$SHARE_VAR" = "1" ] && bwrap_cmd="$bwrap_cmd --bind-try /var /var" 149 | [ "$SHARE_RUN" = "1" ] && bwrap_cmd="$bwrap_cmd --bind-try /run /run" 150 | 151 | # Add optional XDG_RUNTIME_DIR binding if enabled 152 | [ "$SHARE_XDG_RUNTIME_DIR" = "1" ] && [ -n "$XDG_RUNTIME_DIR" ] && bwrap_cmd="$bwrap_cmd --bind-try $XDG_RUNTIME_DIR $XDG_RUNTIME_DIR" 153 | 154 | # Themes & Icons 155 | #if [ "$SHARE_LOOK" = "1" ]; then 156 | # [ -d /usr/share/icons ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/icons /usr/share/icons" 157 | # [ -d /usr/share/themes ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/themes /usr/share/themes" 158 | #fi 159 | # 160 | ## Fonts 161 | #if [ "$SHARE_FONTS" = 1 ]; then 162 | # [ -d /usr/share/fontconfig ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --bind-try /usr/share/fontconfig /usr/share/fontconfig" 163 | # [ -d /usr/share/fonts ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --bind-try /usr/share/fonts /usr/share/fonts" 164 | #fi 165 | # 166 | ## Audio support 167 | #if [ "$SHARE_AUDIO" = "1" ] && [ -n "$XDG_RUNTIME_DIR" ]; then 168 | # bwrap_cmd="$bwrap_cmd \ 169 | # --bind-try /etc/asound.conf /etc/asound.conf \ 170 | # --bind-try $XDG_RUNTIME_DIR/pulse $XDG_RUNTIME_DIR/pulse \ 171 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0 $XDG_RUNTIME_DIR/pipewire-0 \ 172 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0.lock $XDG_RUNTIME_DIR/pipewire-0.lock \ 173 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0-manager $XDG_RUNTIME_DIR/pipewire-0-manager \ 174 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0-manager.lock $XDG_RUNTIME_DIR/pipewire-0-manager.lock" 175 | #fi 176 | 177 | # Add special flags which are needed to "unsandbox" bwrap ("not a security boundary"): 178 | bwrap_cmd="$bwrap_cmd --proc /proc" 179 | bwrap_cmd="$bwrap_cmd --dev-bind /dev /dev --ro-bind-try /sys /sys" 180 | bwrap_cmd="$bwrap_cmd --cap-add CAP_SYS_ADMIN" 181 | bwrap_cmd="$bwrap_cmd --share-net" 182 | 183 | eval "$bwrap_cmd" -- "$_cmd" $@ 184 | -------------------------------------------------------------------------------- /assets/LAUNCH-multicall.rootfs.entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ "$DEBUG" = "1" ] && set -x 4 | 5 | # Strip the first character from ARGS and ARGV0 (assuming it's '!') 6 | ARGS="$(echo "$ARGS" | cut -c2-)" 7 | ARGV0="$(echo "$ARGV0" | cut -c2-)" 8 | 9 | _cat() { 10 | files="$*" 11 | for file in $files; do 12 | while IFS= read -r line || [ -n "$line" ]; do 13 | printf '%s\n' "$line" 14 | done <"$file" 15 | done 16 | } ; FALLBACK=$(_cat /usr/local/bin/default) 17 | 18 | # Split ARGS into command and its arguments 19 | set -- $ARGS 20 | CMD="$1" 21 | shift # Remove the command from the list, leaving the arguments in $@ 22 | 23 | # Check if ARGV0 is available as a command. We remove the "./" that might prepend ARGV0 24 | if command -v "${ARGV0%.*}" >/dev/null 2>&1; then 25 | # If ARGV0 is available, execute ARGV0 with its arguments 26 | exec "${ARGV0%.*}" "$ARGS" # Because ARGS' first element was not in fact the CMD 27 | elif command -v "$CMD" >/dev/null 2>&1; then 28 | # If CMD (the first part of ARGS) is available, execute it with remaining arguments 29 | exec "$CMD" "$@" 30 | elif command -v "$FALLBACK" >/dev/null 2>&1; then 31 | exec "$FALLBACK" "$@" 32 | else 33 | echo "Error: Neither ARGV0 ('${ARGV0%.*}') nor ARGS ('$CMD') are available in \$PATH" 34 | exit 1 35 | fi 36 | -------------------------------------------------------------------------------- /assets/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xplshn/pelf/3f09769088c3b593ec044822387b2794644225af/assets/screenshot.png -------------------------------------------------------------------------------- /cmd/dynexec/C/dynexec.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define DYNEXE_NAME "dynexe" 12 | extern char **environ; 13 | 14 | char* match_linker_name(const char* shared_lib) { 15 | struct dirent *entry; 16 | DIR *dp; 17 | regex_t regex; 18 | int reti; 19 | static char linker_name[256]; 20 | 21 | // Compile regex to match "ld-*-*.so.*" 22 | reti = regcomp(®ex, "^ld-.*-.*\\.so\\..*", 0); 23 | if (reti) { 24 | fprintf(stderr, "Could not compile regex\n"); 25 | exit(EXIT_FAILURE); 26 | } 27 | 28 | dp = opendir(shared_lib); 29 | if (dp == NULL) { 30 | perror("opendir"); 31 | exit(EXIT_FAILURE); 32 | } 33 | 34 | while ((entry = readdir(dp))) { 35 | struct stat st; 36 | char path[1024]; 37 | snprintf(path, sizeof(path), "%s/%s", shared_lib, entry->d_name); 38 | if (stat(path, &st) == 0 && S_ISREG(st.st_mode)) { 39 | reti = regexec(®ex, entry->d_name, 0, NULL, 0); 40 | if (!reti) { 41 | snprintf(linker_name, sizeof(linker_name), "%s", entry->d_name); 42 | closedir(dp); 43 | regfree(®ex); 44 | return linker_name; 45 | } 46 | } 47 | } 48 | 49 | closedir(dp); 50 | regfree(®ex); 51 | return NULL; 52 | } 53 | 54 | char* realpath_alloc(const char* path) { 55 | char* resolved_path = realpath(path, NULL); 56 | if (!resolved_path) { 57 | perror("realpath"); 58 | exit(EXIT_FAILURE); 59 | } 60 | return resolved_path; 61 | } 62 | 63 | char* custom_basename(const char* path) { 64 | char* base = strrchr(path, '/'); 65 | return base ? base + 1 : (char*)path; 66 | } 67 | 68 | int is_file(const char* path) { 69 | struct stat path_stat; 70 | if (stat(path, &path_stat) != 0) { 71 | return 0; 72 | } 73 | return S_ISREG(path_stat.st_mode); 74 | } 75 | 76 | int main(int argc, char* argv[]) { 77 | char* dynexe = realpath_alloc("/proc/self/exe"); 78 | char* dynexe_dir = strdup(dynexe); 79 | dynexe_dir = dirname(dynexe_dir); 80 | char lower_dir[512]; 81 | 82 | snprintf(lower_dir, sizeof(lower_dir), "%s/../", dynexe_dir); 83 | 84 | // Check if we are in the "bin" directory 85 | if (strcmp(custom_basename(dynexe_dir), "bin") == 0 && is_file(lower_dir)) { 86 | free(dynexe_dir); 87 | dynexe_dir = realpath_alloc(lower_dir); 88 | } 89 | 90 | char* shared_bin = malloc(strlen(dynexe_dir) + 12); 91 | snprintf(shared_bin, strlen(dynexe_dir) + 12, "%s/shared/bin", dynexe_dir); 92 | 93 | char* shared_lib = malloc(strlen(dynexe_dir) + 12); 94 | snprintf(shared_lib, strlen(dynexe_dir) + 12, "%s/shared/lib", dynexe_dir); 95 | 96 | // Determine the binary to run 97 | char* bin_name = custom_basename(argv[0]); 98 | if (strcmp(bin_name, DYNEXE_NAME) == 0 && argc > 1) { 99 | bin_name = argv[1]; 100 | argv++; 101 | argc--; 102 | } 103 | 104 | char* bin = malloc(strlen(shared_bin) + strlen(bin_name) + 2); 105 | snprintf(bin, strlen(shared_bin) + strlen(bin_name) + 2, "%s/%s", shared_bin, bin_name); 106 | 107 | char* linker_name = match_linker_name(shared_lib); 108 | if (!linker_name) { 109 | fprintf(stderr, "No valid linker found in %s\n", shared_lib); 110 | exit(EXIT_FAILURE); 111 | } 112 | 113 | char* linker = malloc(strlen(shared_lib) + strlen(linker_name) + 2); 114 | snprintf(linker, strlen(shared_lib) + strlen(linker_name) + 2, "%s/%s", shared_lib, linker_name); 115 | 116 | // Prepare arguments for execve 117 | char* exec_args[argc + 4]; 118 | exec_args[0] = linker; 119 | exec_args[1] = "--library-path"; 120 | exec_args[2] = shared_lib; 121 | exec_args[3] = bin; 122 | for (int i = 1; i < argc; i++) { 123 | exec_args[3 + i] = argv[i]; 124 | } 125 | exec_args[argc + 3] = NULL; 126 | 127 | // Execute the binary using execve 128 | if (execve(linker, exec_args, environ) == -1) { 129 | fprintf(stderr, "Failed to execute %s: %s\n", linker, strerror(errno)); 130 | exit(EXIT_FAILURE); 131 | } 132 | 133 | // Clean up 134 | free(dynexe); 135 | free(dynexe_dir); 136 | free(shared_bin); 137 | free(shared_lib); 138 | free(bin); 139 | free(linker); 140 | 141 | return 0; 142 | } 143 | -------------------------------------------------------------------------------- /cmd/dynexec/README.md: -------------------------------------------------------------------------------- 1 | NOTE: My lib4bin automatically creates a directory like those generated by https://github.com/VHSgunzo/sharun/blob/main/lib4bin, it also adds ./sharun to it (it must be in PATH) 2 | NOTE: Do not use the Go or C version of dynexec, they relies on execve instead of a user space exec implementation (since there isn't any) 3 | -------------------------------------------------------------------------------- /cmd/dynexec/dynexec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "syscall" 9 | ) 10 | 11 | const dynexeName = "dynexe" 12 | 13 | // Matches any linker name that follows the pattern "ld-linux" or "ld-musl" for x86_64 or aarch64. 14 | var linkerRegexp = regexp.MustCompile(`ld-(linux|musl)-(x86_64|aarch64).so.[0-9]+`) 15 | 16 | func matchLinkerName(sharedLib string) string { 17 | // Check files in the sharedLib directory and match against the linker pattern 18 | files, err := os.ReadDir(sharedLib) 19 | if err != nil { 20 | panic(fmt.Sprintf("failed to read shared library directory: %v", err)) 21 | } 22 | 23 | for _, file := range files { 24 | if !file.IsDir() && linkerRegexp.MatchString(file.Name()) { 25 | return file.Name() 26 | } 27 | } 28 | return "" 29 | } 30 | 31 | func realpath(path string) string { 32 | absPath, err := filepath.EvalSymlinks(path) 33 | if err != nil { 34 | panic(err) 35 | } 36 | return absPath 37 | } 38 | 39 | func basename(path string) string { 40 | return filepath.Base(path) 41 | } 42 | 43 | func isFile(path string) bool { 44 | info, err := os.Stat(path) 45 | if err != nil { 46 | return false 47 | } 48 | return !info.IsDir() 49 | } 50 | 51 | func main() { 52 | // Get the executable path 53 | dynexe, err := os.Executable() 54 | if err != nil { 55 | panic(err) 56 | } 57 | dynexeDir := filepath.Dir(dynexe) 58 | lowerDir := filepath.Join(dynexeDir, "../") // TODO, what is this? 59 | 60 | // Check if the parent directory contains the dynexe binary 61 | if basename(dynexeDir) == "bin" && isFile(filepath.Join(lowerDir, dynexeName)) { 62 | dynexeDir = realpath(lowerDir) 63 | } 64 | 65 | // Collect command-line arguments 66 | execArgs := os.Args 67 | arg0 := execArgs[0] 68 | execArgs = execArgs[1:] 69 | 70 | sharedBin := filepath.Join(dynexeDir, "shared/bin") 71 | sharedLib := filepath.Join(dynexeDir, "shared/lib") 72 | 73 | // Determine the binary name to run 74 | binName := basename(arg0) 75 | if binName == dynexeName { 76 | binName = execArgs[0] 77 | execArgs = execArgs[1:] 78 | } 79 | bin := filepath.Join(sharedBin, binName) 80 | 81 | // Get the linker path by matching against shared lib files using regular expressions 82 | linkerName := matchLinkerName(sharedLib) 83 | if linkerName == "" { 84 | panic(fmt.Sprintf("no valid linker found in %s", sharedLib)) 85 | } 86 | linker := filepath.Join(sharedLib, linkerName) 87 | 88 | // Prepare arguments for execve 89 | args := []string{linker, "--library-path", sharedLib, bin} 90 | args = append(args, execArgs...) 91 | 92 | // Prepare environment variables 93 | envs := os.Environ() 94 | 95 | // Execute the binary using syscall.Exec (equivalent to userland execve) 96 | err = syscall.Exec(linker, args, envs) 97 | if err != nil { 98 | panic(fmt.Sprintf("failed to execute %s: %v", linker, err)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /cmd/dynexec/lib4bin/lib4bin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/u-root/u-root/pkg/ldd" 13 | ) 14 | 15 | // Constants for directory structure 16 | const ( 17 | defaultDstDir = "output" 18 | defaultSharedDir = "shared" 19 | defaultLibDir = "lib" 20 | defaultBinDir = "bin" 21 | ) 22 | 23 | var ( 24 | strip = flag.Bool("strip", false, "Strip debug symbols") 25 | oneDir = flag.Bool("one-dir", true, "Use one directory for output") 26 | createLinks = flag.Bool("create-links", true, "Create symlinks in the bin directory") 27 | dstDirPath = flag.String("dst-dir", defaultDstDir, "Destination directory for libraries and binaries") 28 | ) 29 | 30 | // makeExecutable makes a file executable 31 | func makeExecutable(filePath string) error { 32 | if err := os.Chmod(filePath, 0755); err != nil { 33 | return fmt.Errorf("failed to chmod +x %s: %v", filePath, err) 34 | } 35 | return nil 36 | } 37 | 38 | // tryStrip attempts to strip the binary if the flag is set 39 | func tryStrip(filePath string) error { 40 | if *strip { 41 | stripPath, err := exec.LookPath("strip") 42 | if err != nil { 43 | return fmt.Errorf("strip command not found: %v", err) 44 | } 45 | 46 | // Execute the strip command 47 | cmd := exec.Command(stripPath, filePath) 48 | if err := cmd.Run(); err != nil { 49 | return fmt.Errorf("failed to strip %s: %v", filePath, err) 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | // getLibs retrieves the list of libraries that a binary depends on 56 | func getLibs(binaryPath string) ([]string, error) { 57 | dependencies, err := ldd.FList(binaryPath) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return dependencies, nil 62 | } 63 | 64 | // copyFile copies a file from source to destination 65 | func copyFile(src, dst string) error { 66 | input, err := os.ReadFile(src) 67 | if err != nil { 68 | return err 69 | } 70 | return os.WriteFile(dst, input, 0644) 71 | } 72 | 73 | // createSymlink creates a symlink at dst pointing to src 74 | func createSymlink(src, dst string) error { 75 | return os.Symlink(src, dst) 76 | } 77 | 78 | // Check if the binary is a dynamic executable 79 | func isDynamic(binaryPath string) (bool, error) { 80 | cmd := exec.Command("ldd", binaryPath) 81 | output, _ := cmd.CombinedOutput() 82 | // If ldd returns an error, assume the binary is static 83 | //if err != nil { 84 | // log.Printf("ldd error: %v, assuming static binary for: %s", err, binaryPath) 85 | // return false, nil 86 | //} 87 | 88 | outputLower := strings.ToLower(string(output)) 89 | if strings.Contains(outputLower, "not a dynamic executable") || strings.Contains(outputLower, "not a valid dynamic program") { 90 | return false, nil 91 | } 92 | return true, nil 93 | } 94 | 95 | // processBinary processes a binary file and decides whether to place it in the bin directory or shared/bin 96 | func processBinary(binaryPath, dynExecPath string) error { 97 | fileInfo, err := os.Stat(binaryPath) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | if !fileInfo.Mode().IsRegular() { 103 | return fmt.Errorf("skipped: %s is not a regular file", binaryPath) 104 | } 105 | 106 | // Create the main destination directory 107 | if err := os.MkdirAll(*dstDirPath, 0755); err != nil { 108 | return err 109 | } 110 | 111 | // Check if the binary is dynamic 112 | dynamic, err := isDynamic(binaryPath) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | // Handle dynamic binaries 118 | if dynamic { 119 | // Create the shared directory structure for dynamic executables 120 | sharedDir := filepath.Join(*dstDirPath, defaultSharedDir) 121 | sharedBinDir := filepath.Join(sharedDir, defaultBinDir) 122 | sharedLibDir := filepath.Join(sharedDir, defaultLibDir) 123 | 124 | if err := os.MkdirAll(sharedBinDir, 0755); err != nil { 125 | return err 126 | } 127 | 128 | if err := os.MkdirAll(sharedLibDir, 0755); err != nil { 129 | return err 130 | } 131 | 132 | sharedBinaryPath := filepath.Join(sharedBinDir, fileInfo.Name()) 133 | 134 | // Copy the binary to the shared bin directory 135 | if err := copyFile(binaryPath, sharedBinaryPath); err != nil { 136 | return err 137 | } 138 | 139 | // Chmod +x 140 | if err := makeExecutable(sharedBinaryPath); err != nil { 141 | return err 142 | } 143 | 144 | // Strip the binary if the strip flag is set 145 | if err := tryStrip(sharedBinaryPath); err != nil { 146 | return err 147 | } 148 | 149 | // Get the list of libraries the binary depends on 150 | libPaths, err := getLibs(binaryPath) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | // Copy libraries to the shared lib directory 156 | for _, libPath := range libPaths { 157 | dstLibPath := filepath.Join(sharedLibDir, filepath.Base(libPath)) 158 | if err := copyFile(libPath, dstLibPath); err != nil { 159 | return err 160 | } 161 | 162 | // Strip libraries if the strip flag is set 163 | if err := tryStrip(dstLibPath); err != nil { 164 | return err 165 | } 166 | } 167 | 168 | // Create the bin directory and symlink to the shared binary 169 | binDir := filepath.Join(*dstDirPath, defaultBinDir) 170 | if err := os.MkdirAll(binDir, 0755); err != nil { 171 | return err 172 | } 173 | 174 | symlinkPath := filepath.Join(binDir, fileInfo.Name()) 175 | if *createLinks { // Ugly as fuck. TODO: Find a better way or prettify 176 | oPWD, err := os.Getwd() 177 | if err != nil { 178 | return err 179 | } 180 | os.Chdir(filepath.Dir(symlinkPath)) 181 | if err := createSymlink("../sharun", filepath.Join("../", defaultBinDir, filepath.Base(symlinkPath))); err != nil { // TODO, don't hardcode sharun 182 | os.Chdir(oPWD) 183 | return err 184 | } 185 | os.Chdir(oPWD) 186 | } 187 | 188 | //if *createLinks { 189 | // if err := createSymlink(filepath.Join("..", dynExecPath), symlinkPath); err != nil { 190 | // return err 191 | // } 192 | //} 193 | 194 | //symlinkPath := filepath.Join(binDir, fileInfo.Name()) 195 | //if *createLinks { 196 | // if err := createSymlink(filepath.Join("..", defaultSharedDir, defaultBinDir, fileInfo.Name()), symlinkPath); err != nil { 197 | // return err 198 | // } 199 | //} 200 | } else { 201 | // Handle static binaries: Copy directly to the bin directory 202 | binDir := filepath.Join(*dstDirPath, defaultBinDir) 203 | if err := os.MkdirAll(binDir, 0755); err != nil { 204 | return err 205 | } 206 | 207 | staticBinaryPath := filepath.Join(binDir, fileInfo.Name()) 208 | if err := copyFile(binaryPath, staticBinaryPath); err != nil { 209 | return err 210 | } 211 | 212 | // Chmod +x 213 | if err := makeExecutable(staticBinaryPath); err != nil { 214 | return err 215 | } 216 | 217 | // Strip the binary if the strip flag is set 218 | if err := tryStrip(staticBinaryPath); err != nil { 219 | return err 220 | } 221 | } 222 | 223 | fmt.Printf("Processed: %s\n", fileInfo.Name()) 224 | return nil 225 | } 226 | 227 | // findDynExec finds the dynexec executable in the user's $PATH 228 | func findDynExec() (string, error) { 229 | path, err := exec.LookPath("sharun") 230 | if err != nil { 231 | return "", fmt.Errorf("sharun not found in PATH: %v", err) 232 | } 233 | return path, nil 234 | } 235 | 236 | // copyDynExec copies the sharun executable to the destination directory and makes it executable 237 | func copyDynExec(dynExecPath, dstDynExecPath string) error { 238 | if err := copyFile(dynExecPath, dstDynExecPath); err != nil { 239 | log.Fatalf("Unable to copy dynexec: %v", err) 240 | } 241 | 242 | if err := makeExecutable(dstDynExecPath); err != nil { 243 | return err 244 | } 245 | 246 | fmt.Printf("Copied and made executable: %s\n", dstDynExecPath) 247 | return nil 248 | } 249 | 250 | func main() { 251 | flag.Parse() 252 | 253 | dynExecPath, err := findDynExec() 254 | if err != nil { 255 | log.Fatalf("%v", err) 256 | } 257 | dstDynExecPath := filepath.Join(*dstDirPath, "sharun") 258 | 259 | if err := copyDynExec(dynExecPath, dstDynExecPath); err != nil { 260 | log.Printf("sharun not found in PATH or failed to copy: %v\n", err) 261 | } 262 | 263 | // Process any additional binaries passed as arguments 264 | binaryList := flag.Args() 265 | if len(binaryList) == 0 { 266 | fmt.Println("Error: Specify the ELF binary executable!") 267 | os.Exit(1) 268 | } 269 | 270 | if *oneDir && *dstDirPath == "" { 271 | *dstDirPath = defaultDstDir 272 | } 273 | 274 | for _, binary := range binaryList { 275 | if err := processBinary(binary, dstDynExecPath); err != nil { 276 | log.Printf("Error processing %s: %v\n", binary, err) 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /cmd/misc/BS2AppBundle: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -n "$OVERRIDE_ENTRYPOINT" ]; then 4 | echo "OVERRIDE_ENTRYPOINT is set to: $OVERRIDE_ENTRYPOINT. It will be used as the entrypoint." 5 | fi 6 | if [ -n "$OVERRIDE_DESKTOP" ]; then 7 | echo "OVERRIDE_DESKTOP will be passed to a sed command" 8 | fi 9 | 10 | # Check if an AppImage was provided 11 | if [ -z "$1" ]; then 12 | echo "Usage: $0 " 13 | exit 1 14 | fi 15 | 16 | # Ensure full path to avoid confusion with relative paths 17 | APPIMAGE="$(realpath "$1")" 18 | 19 | # Check if the file actually exists 20 | if [ ! -f "$APPIMAGE" ]; then 21 | echo "Error: $APPIMAGE not found!" 22 | exit 1 23 | fi 24 | 25 | DIROFTHEAPP="$(dirname "$APPIMAGE")" 26 | APPNAME="$(basename "$APPIMAGE" .AppImage)" 27 | WORKDIR="$(mktemp -d)" 28 | 29 | if [ -z "$COMPRESSION_OPTS" ]; then 30 | if [ "$APPBUNDLE_FS" = "dwfs" ]; then 31 | COMPRESSION_OPTS="-l7 -C zstd:level=22 --metadata-compression null -S 21 -B 8 --order nilsimsa -W 12 -w 4" 32 | elif [ "$APPBUNDLE_FS" = "sqfs" ]; then 33 | COMPRESSION_OPTS="-comp zstd -Xcompression-level 15" 34 | fi 35 | fi 36 | 37 | # Clean up on exit 38 | cleanup() { 39 | fusermount3 -uz "$WORKDIR/rootfs-based.AppDir/rootfs" >/dev/null 2>&1 40 | fusermount -uz "$WORKDIR/rootfs-based.AppDir/rootfs" >/dev/null 2>&1 41 | rm -rf "$WORKDIR" 42 | } 43 | trap cleanup EXIT 44 | 45 | cd "$WORKDIR" || exit 1 46 | 47 | # Ensure the AppImage is executable 48 | chmod +x "$APPIMAGE" 49 | 50 | # Extract the AppImage contents 51 | "$APPIMAGE" --appimage-extract 52 | 53 | # Check if squashfs-root was created 54 | if [ ! -d "squashfs-root" ]; then 55 | echo "Failed to extract AppImage. squashfs-root not found." 56 | exit 1 57 | fi 58 | 59 | # Check if conty.sh exists in squashfs-root 60 | if [ -f squashfs-root/conty.sh ]; then 61 | echo "Found conty.sh. Extracting with dwarfsextract..." 62 | 63 | # Create the rootfs-based.AppDir 64 | mkdir -p rootfs-based.AppDir/rootfs rootfs-based.AppDir/usr/bin 65 | 66 | # Extract the conty.sh to the rootfs 67 | cp ./squashfs-root/*.desktop ./squashfs-root/.DirIcon ./rootfs-based.AppDir 68 | dwarfs -o offset="auto",ro,auto_unmount "./squashfs-root/conty.sh" "rootfs-based.AppDir/rootfs" && { 69 | echo "Removing decompressed squashfs-root to free up RAM" 70 | rm -rf ./squashfs-root 71 | } 72 | 73 | # Download AppRun for rootfs-based AppDir 74 | if ! wget -qO "rootfs-based.AppDir/AppRun" https://raw.githubusercontent.com/xplshn/pelf/refs/heads/dev/assets/AppRun.rootfs-based; then 75 | echo "Failed to download AppRun.rootfs-based" 76 | exit 1 77 | fi 78 | chmod +x "rootfs-based.AppDir/AppRun" 79 | 80 | # Download and install bwrap 81 | if ! wget -qO "rootfs-based.AppDir/usr/bin/bwrap" "https://bin.ajam.dev/$(uname -m)/bwrap-patched"; then 82 | echo "Unable to install bwrap to rootfs-based.AppDir/usr/bin/bwrap" 83 | exit 1 84 | fi 85 | chmod +x "rootfs-based.AppDir/usr/bin/bwrap" 86 | echo "Packaging as a rootfs-based AppBundle..." 87 | 88 | # Pack the new rootfs-based.AppDir as an AppBundle 89 | pelf-$APPBUNDLE_FS --add-appdir ./rootfs-based.AppDir \ 90 | --appbundle-id "$APPNAME" \ 91 | --output-to "$DIROFTHEAPP/$APPNAME.$APPBUNDLE_FS.AppBundle" \ 92 | --embed-static-tools \ 93 | --compression "$COMPRESSION_OPTS" 94 | else 95 | echo "Packaging as a standard AppBundle..." 96 | 97 | # No conty.sh, package the squashfs-root directly as an AppBundle 98 | pelf-$APPBUNDLE_FS --add-appdir ./squashfs-root \ 99 | --appbundle-id "$APPNAME" \ 100 | --output-to "$DIROFTHEAPP/$APPNAME.$APPBUNDLE_FS.AppBundle" \ 101 | --embed-static-tools \ 102 | --compression "$COMPRESSION_OPTS" 103 | fi 104 | 105 | # Find the .desktop file and extract the Exec= line 106 | DESKTOP_FILE=$(find ./rootfs-based.AppDir -type f -name "*.desktop" | head -n 1) 107 | if [ -f "$DESKTOP_FILE" ]; then 108 | if [ -n "$OVERRIDE_DESKTOP" ]; then 109 | # Apply the custom SED expression if provided 110 | sed -i "$OVERRIDE_DESKTOP" "$DESKTOP_FILE" 111 | echo "Applied OVERRIDE_DESKTOP SED expression on $DESKTOP_FILE" 112 | fi 113 | 114 | # Extract the Exec= line after possible modifications 115 | EXEC_LINE=$(awk -F"=| " '/Exec=/ {print $2; exit}' "$DESKTOP_FILE") 116 | if [ -n "$OVERRIDE_ENTRYPOINT" ]; then 117 | # Use the provided override if set 118 | echo "$OVERRIDE_ENTRYPOINT" > "$WORKDIR/entrypoint" 119 | echo "Set entrypoint to OVERRIDE_ENTRYPOINT: $OVERRIDE_ENTRYPOINT" 120 | elif [ -n "$EXEC_LINE" ]; then 121 | echo "Exec line found: $EXEC_LINE" 122 | echo "$EXEC_LINE" > "$WORKDIR/entrypoint" 123 | else 124 | echo "Exec line not found in $DESKTOP_FILE" 125 | fi 126 | else 127 | echo "No .desktop file found in rootfs-based.AppDir" 128 | fi 129 | 130 | echo "AppBundle created successfully in $DIROFTHEAPP." 131 | -------------------------------------------------------------------------------- /cmd/misc/appstream-helper/go.mod: -------------------------------------------------------------------------------- 1 | module appstream-helper 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/fxamacker/cbor/v2 v2.8.0 7 | github.com/goccy/go-json v0.10.5 8 | github.com/klauspost/compress v1.18.0 9 | github.com/shamaton/msgpack/v2 v2.2.3 10 | github.com/tdewolff/minify/v2 v2.21.3 11 | github.com/zeebo/blake3 v0.2.4 12 | ) 13 | 14 | require ( 15 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 16 | github.com/tdewolff/parse/v2 v2.7.20 // indirect 17 | github.com/x448/float16 v0.8.4 // indirect 18 | golang.org/x/sys v0.33.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /cmd/misc/appstream-helper/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 2 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 3 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 4 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 5 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 6 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 7 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 8 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 9 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 10 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 11 | github.com/shamaton/msgpack/v2 v2.2.3 h1:uDOHmxQySlvlUYfQwdjxyybAOzjlQsD1Vjy+4jmO9NM= 12 | github.com/shamaton/msgpack/v2 v2.2.3/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= 13 | github.com/tdewolff/minify/v2 v2.21.1 h1:AAf5iltw6+KlUvjRNPAPrANIXl3XEJNBBzuZom5iCAM= 14 | github.com/tdewolff/minify/v2 v2.21.1/go.mod h1:PoqFH8ugcuTUvKqVM9vOqXw4msxvuhL/DTmV5ZXhSCI= 15 | github.com/tdewolff/minify/v2 v2.21.3 h1:KmhKNGrN/dGcvb2WDdB5yA49bo37s+hcD8RiF+lioV8= 16 | github.com/tdewolff/minify/v2 v2.21.3/go.mod h1:iGxHaGiONAnsYuo8CRyf8iPUcqRJVB/RhtEcTpqS7xw= 17 | github.com/tdewolff/parse/v2 v2.7.19 h1:7Ljh26yj+gdLFEq/7q9LT4SYyKtwQX4ocNrj45UCePg= 18 | github.com/tdewolff/parse/v2 v2.7.19/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= 19 | github.com/tdewolff/parse/v2 v2.7.20 h1:Y33JmRLjyGhX5JRvYh+CO6Sk6pGMw3iO5eKGhUhx8JE= 20 | github.com/tdewolff/parse/v2 v2.7.20/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= 21 | github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 22 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 23 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 24 | github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= 25 | github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= 26 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 27 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 28 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 29 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 30 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 31 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 32 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 33 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 34 | -------------------------------------------------------------------------------- /cmd/misc/getlibs: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | add_thelibs() { 4 | # Copy the libraries from the executable to the temporary directory 5 | SOs="$(ldd "$1")" 6 | echo "$SOs" | awk ' 7 | # Store the first word of the first line 8 | NR == 1 { first_word = $1 } 9 | # For lines with =>, check if the third word is not the same as the first word of the first line 10 | /=>/ && $3 != first_word { print $3 } 11 | '| while read -r lib; do 12 | # Copy the library to the temporary directory 13 | cp -LR "$lib" "$2" || exit 1 14 | done 15 | } 16 | 17 | add_thelibs "$@" 18 | -------------------------------------------------------------------------------- /cmd/misc/rootfs2sharun: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Function to display help information 4 | show_help() { 5 | printf "Usage: %s -d output_dir -a app_dir file1 [file2 ...]\n" "$0" 6 | printf "\nOptions:\n" 7 | printf " -d output_dir Specify the new output directory\n" 8 | printf " -a app_dir Specify the rootfs-based AppDir\n" 9 | printf " -h Show this help message\n" 10 | } 11 | 12 | # Initialize variables 13 | OUTPUT_DIR="" 14 | APP_DIR="" 15 | 16 | # Parse command-line options 17 | while [ $# -gt 0 ]; do 18 | case "$1" in 19 | -h) 20 | show_help 21 | exit 0 22 | ;; 23 | -d) 24 | shift 25 | OUTPUT_DIR="$1" 26 | shift 27 | ;; 28 | -a) 29 | shift 30 | APP_DIR="$1" 31 | shift 32 | ;; 33 | -*) 34 | printf "Invalid option: %s\n" "$1" >&2 35 | show_help 36 | exit 1 37 | ;; 38 | *) 39 | break 40 | ;; 41 | esac 42 | done 43 | 44 | # Check if both OUTPUT_DIR and APP_DIR are specified 45 | if [ -z "$OUTPUT_DIR" ] || [ -z "$APP_DIR" ]; then 46 | printf "Error: Both output directory and AppDir must be specified.\n" >&2 47 | show_help 48 | exit 1 49 | fi 50 | 51 | # Check if at least one filename is specified 52 | if [ "$#" -eq 0 ]; then 53 | printf "Error: No files specified. Please provide at least one file.\n" >&2 54 | show_help 55 | exit 1 56 | fi 57 | 58 | mkdir -p "$OUTPUT_DIR" 59 | # Loop through each file passed as argument 60 | for FILE in "$@"; do 61 | if [ -e "$APP_DIR/rootfs/usr/bin/$(basename "$FILE")" ]; then 62 | lib4bin --dst-dir "$OUTPUT_DIR" "$FILE" 63 | else 64 | printf "%s does not exist in %s/rootfs/usr/bin/\n" "$(basename "$FILE")" "$APP_DIR" 65 | fi 66 | done 67 | -------------------------------------------------------------------------------- /cmd/misc/thumbgen: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Function to calculate the MD5 hash of a URI 4 | calculate_md5() { 5 | if ! printf "%s" "$1" | md5sum | cut -d ' ' -f1; then 6 | echo "There was an error calculating the MD5 hash. Quitting..." 7 | exit 1 8 | fi 9 | # Return the hash value 10 | echo "$hash" 11 | } 12 | 13 | # Function to create a thumbnail for a file 14 | create_thumbnail() { 15 | input_file="$1" 16 | thumbnail_file="$2" 17 | 18 | # Ensure input file and thumbnail file are specified 19 | if [ -z "$input_file" ]; then 20 | echo "Usage: $0 [file_to_thumbnail] <128x128thumbnail.png>" 21 | exit 1 22 | fi 23 | 24 | # Check if the thumbnail file exists 25 | if [ -n "$thumbnail_file" ] && [ ! -f "$thumbnail_file" ]; then 26 | echo "The thumbnail file does not exist." 27 | exit 1 28 | fi 29 | 30 | # Determine the canonical URI of the input file 31 | abs_path=$(readlink -f "$input_file") 32 | uri="file://$abs_path" 33 | 34 | # Calculate the MD5 hash of the URI 35 | hash=$(calculate_md5 "$uri") 36 | 37 | # Determine the target directory and filename for the thumbnail 38 | thumbnail_dir="${XDG_CACHE_HOME:-$HOME/.cache}/thumbnails/normal" 39 | mkdir -p "$thumbnail_dir" 40 | thumbnail_path="$thumbnail_dir/$hash.png" 41 | 42 | # Copy the provided thumbnail to the target path 43 | if [ -n "$thumbnail_file" ]; then 44 | cp "$thumbnail_file" "$thumbnail_path" 45 | echo "Thumbnail saved to: $thumbnail_path" 46 | exit 0 47 | fi 48 | 49 | echo "$thumbnail_path" 50 | } 51 | 52 | # Call the function with arguments 53 | create_thumbnail "$1" "$2" 54 | -------------------------------------------------------------------------------- /cmd/pelfd-gui.deprecated/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/url" 9 | "os" 10 | "path/filepath" 11 | "regexp" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "fyne.io/fyne/v2" 17 | "fyne.io/fyne/v2/app" 18 | "fyne.io/fyne/v2/container" 19 | "fyne.io/fyne/v2/dialog" 20 | "fyne.io/fyne/v2/widget" 21 | 22 | "github.com/goccy/go-json" 23 | "github.com/liamg/tml" 24 | "github.com/minio/md5-simd" 25 | "github.com/zeebo/blake3" 26 | ) 27 | 28 | var ( 29 | progressDialog *dialog.CustomDialog 30 | messageLabel *widget.Label 31 | progressBar *widget.ProgressBar 32 | dialogMutex sync.Mutex 33 | lastUpdate time.Time 34 | fyneApp = app.New() 35 | fyneWindow = fyneApp.NewWindow("pelfd is working...") 36 | ) 37 | 38 | func logMessage(level, message string) string { 39 | logColors := map[string]string{ 40 | "INF": "INF:", 41 | "WRN": "WRN:", 42 | "ERR": "ERR:", 43 | } 44 | 45 | color, exists := logColors[level] 46 | if !exists { 47 | color = "LOG:" 48 | } 49 | 50 | formattedMessage := tml.Sprintf(fmt.Sprintf("%s %s", color, message)) 51 | log.Println(formattedMessage) 52 | 53 | // Reset progress and timestamp on each log message call 54 | dialogMutex.Lock() 55 | defer dialogMutex.Unlock() 56 | 57 | // Initialize the progress bar if not already created 58 | if progressBar == nil { 59 | progressBar = widget.NewProgressBar() 60 | progressBar.SetValue(0.05) // Set initial value to 5% 61 | 62 | messageLabel = widget.NewLabel("") 63 | 64 | fyneWindow.SetContent(container.NewVBox(messageLabel, progressBar)) 65 | fyneWindow.Resize(fyne.NewSize(400, 100)) 66 | fyneWindow.Show() 67 | lastUpdate = time.Now() 68 | } 69 | 70 | // Update the message label with the current message 71 | messageLabel.SetText(removeAnsi(formattedMessage)) 72 | 73 | // Start a goroutine for continuous update 74 | go updateProgressBar() 75 | 76 | return fmt.Sprintf("%s %s", level, message) 77 | } 78 | 79 | func updateProgressBar() { 80 | for { 81 | time.Sleep(40 * time.Millisecond) // Update interval of 40ms 82 | dialogMutex.Lock() 83 | 84 | // Check if 2 seconds have passed since the last logMessage call 85 | if time.Since(lastUpdate) > 2*time.Second { 86 | if progressBar.Value < 1.0 { 87 | progressBar.SetValue(progressBar.Value + 0.05) // Increment progress by 5% 88 | } else { 89 | // Hide the window when progress reaches 100% 90 | fyneWindow.Hide() 91 | dialogMutex.Unlock() 92 | break // Stop the goroutine when progress reaches 100% 93 | } 94 | } else { 95 | // Update if there's recent activity, by 15% 96 | progressBar.SetValue(progressBar.Value + 0.15) 97 | } 98 | dialogMutex.Unlock() 99 | } 100 | } 101 | 102 | func createThumbnailForBundle(entry *BundleEntry, path string) { 103 | if entry.Png != "" { 104 | thumbnailPath, err := generateThumbnail(path, entry.Png) 105 | if err != nil { 106 | logMessage("ERR", fmt.Sprintf("Failed to create thumbnail file: %v", err)) 107 | } 108 | entry.Thumbnail = thumbnailPath 109 | logMessage("INF", fmt.Sprintf("A thumbnail for %s was created at: %s", path, thumbnailPath)) 110 | } 111 | } 112 | 113 | func updateDesktopFileIfRequired(path, baseName, appPath string, entry *BundleEntry, cfg Config) { 114 | desktopPath := filepath.Join(appPath, baseName+".desktop") 115 | if _, err := os.Stat(desktopPath); err == nil { 116 | content, err := os.ReadFile(desktopPath) 117 | if err != nil { 118 | logMessage("ERR", fmt.Sprintf("Failed to read .desktop file: %v", err)) 119 | return 120 | } 121 | if cfg.Options.CorrectDesktopFiles { 122 | updatedContent, err := updateDesktopFile(string(content), path, entry) 123 | if err != nil { 124 | logMessage("ERR", fmt.Sprintf("Failed to update .desktop file: %v", err)) 125 | return 126 | } 127 | if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) { 128 | logMessage("ERR", fmt.Sprintf("Failed to remove existing .desktop file: %v", err)) 129 | return 130 | } 131 | if err := os.WriteFile(desktopPath, []byte(updatedContent), 0644); err != nil { 132 | logMessage("ERR", fmt.Sprintf("Failed to write updated .desktop file: %v", err)) 133 | return 134 | } 135 | } 136 | } 137 | } 138 | 139 | func loadConfig(configPath string, homeDir string) Config { 140 | config := Config{ 141 | Options: Options{ 142 | DirectoriesToWalk: []string{"~/Applications"}, 143 | ProbeInterval: 5, 144 | IconDir: filepath.Join(homeDir, ".local/share/icons"), 145 | AppDir: filepath.Join(homeDir, ".local/share/applications"), 146 | CorrectDesktopFiles: true, 147 | }, 148 | Tracker: make(map[string]*BundleEntry), 149 | } 150 | 151 | file, err := os.Open(configPath) 152 | if err != nil { 153 | if os.IsNotExist(err) { 154 | logMessage("INF", fmt.Sprintf("Config file does not exist: %s, creating a new one", configPath)) 155 | saveConfig(config, configPath) 156 | return config 157 | } 158 | logMessage("ERR", fmt.Sprintf("Failed to open config file %s: %v", configPath, err)) 159 | os.Exit(1) 160 | } 161 | defer file.Close() 162 | 163 | decoder := json.NewDecoder(file) 164 | if err := decoder.Decode(&config); err != nil { 165 | logMessage("ERR", fmt.Sprintf("Failed to decode config file: %v", err)) 166 | os.Exit(1) 167 | } 168 | 169 | return config 170 | } 171 | 172 | func saveConfig(config Config, path string) { 173 | file, err := os.Create(path) 174 | if err != nil { 175 | logMessage("ERR", fmt.Sprintf("Failed to save config file: %v", err)) 176 | } 177 | defer file.Close() 178 | 179 | encoder := json.NewEncoder(file) 180 | encoder.SetIndent("", " ") 181 | if err := encoder.Encode(config); err != nil { 182 | logMessage("ERR", fmt.Sprintf("Failed to encode config file: %v", err)) 183 | os.Exit(1) 184 | } 185 | } 186 | 187 | // fileExists checks if a file exists. 188 | func fileExists(filePath string) bool { 189 | _, err := os.Stat(filePath) 190 | if err == nil { 191 | return true 192 | } 193 | if os.IsNotExist(err) { 194 | return false 195 | } 196 | // If there's any other error, we consider that the file doesn't exist for simplicity 197 | return false 198 | } 199 | 200 | func remExtension(filePath string) string { 201 | return strings.Split(filePath, ".")[0] 202 | } 203 | 204 | func expand(filePath, homeDir string) string { 205 | // Expand the tilde (~) to the user's home directory 206 | if strings.HasPrefix(filePath, "~") { 207 | filePath = filepath.Join(homeDir, filePath[1:]) // Replace ~ with the home directory 208 | } 209 | return filePath 210 | } 211 | 212 | // HashURI computes the MD5 hash of the canonical URI. 213 | func HashURI(uri string) string { 214 | hash := md5.Sum([]byte(uri)) 215 | return hex.EncodeToString(hash[:]) 216 | } 217 | 218 | // CanonicalURI generates the canonical URI for a given file path. 219 | func CanonicalURI(filePath string) (string, error) { 220 | absPath, err := filepath.Abs(filePath) 221 | if err != nil { 222 | return "", err 223 | } 224 | uri := url.URL{Scheme: "file", Path: absPath} 225 | return uri.String(), nil 226 | } 227 | 228 | func isExecutable(path string) bool { 229 | info, err := os.Stat(path) 230 | if err != nil { 231 | logMessage("ERR", fmt.Sprintf("Failed to stat file %s: %v", path, err)) 232 | return false 233 | } 234 | mode := info.Mode() 235 | return mode&0111 != 0 236 | } 237 | 238 | // isDirectory checks if the given path is a directory. 239 | func isDirectory(path string) bool { 240 | info, err := os.Stat(path) 241 | if os.IsNotExist(err) { 242 | return false // Path does not exist 243 | } 244 | return err == nil && info.IsDir() // Check for error and if it's a directory 245 | } 246 | 247 | // computeB3SUM computes the Blake3 hash of the file at the given path. 248 | func computeB3SUM(path string) string { 249 | file, err := os.Open(path) 250 | if err != nil { 251 | logMessage("ERR", fmt.Sprintf("Failed to open file %s: %v", path, err)) 252 | return "" 253 | } 254 | defer file.Close() 255 | 256 | hasher := blake3.New() 257 | if _, err := io.Copy(hasher, file); err != nil { 258 | logMessage("ERR", fmt.Sprintf("Failed to compute Blake3 hash of %s: %v", path, err)) 259 | os.Exit(1) 260 | } 261 | 262 | return hex.EncodeToString(hasher.Sum(nil)) 263 | } 264 | 265 | // ThumbnailPath returns the path where the thumbnail should be saved. 266 | func getThumbnailPath(fileMD5 string, thumbnailType string) (string, error) { 267 | // Determine the base directory for thumbnails 268 | baseDir, err := os.UserCacheDir() 269 | if err != nil { 270 | return "", err 271 | } 272 | thumbnailDir := filepath.Join(baseDir, "thumbnails") 273 | 274 | // Determine the size directory based on thumbnail type 275 | sizeDir := "" 276 | switch thumbnailType { 277 | case "normal": 278 | sizeDir = "normal" 279 | case "large": 280 | sizeDir = "large" 281 | default: 282 | return "", fmt.Errorf("invalid thumbnail type: %s", thumbnailType) 283 | } 284 | 285 | // Create the full directory path 286 | fullDir := filepath.Join(thumbnailDir, sizeDir) 287 | err = os.MkdirAll(fullDir, os.ModePerm) 288 | if err != nil { 289 | return "", err 290 | } 291 | 292 | // Create the final path for the thumbnail 293 | thumbnailPath := filepath.Join(fullDir, fileMD5+".png") 294 | 295 | return thumbnailPath, nil 296 | } 297 | 298 | // copyFile copies a file from src to dst. 299 | func copyFile(src, dst string) error { 300 | // Open the source file 301 | srcFile, err := os.Open(src) 302 | if err != nil { 303 | return err 304 | } 305 | defer srcFile.Close() 306 | 307 | // Create the destination file 308 | dstFile, err := os.Create(dst) 309 | if err != nil { 310 | return err 311 | } 312 | defer dstFile.Close() 313 | 314 | // Copy the content from source to destination 315 | _, err = io.Copy(dstFile, srcFile) 316 | if err != nil { 317 | return err 318 | } 319 | 320 | return nil 321 | } 322 | 323 | func updateDesktopFile(content, bundlePath string, entry *BundleEntry) (string, error) { 324 | // Correct Exec line 325 | updatedExec := fmt.Sprintf("Exec=%s", bundlePath) 326 | 327 | // Define a regular expression to match the Exec line. 328 | reExec := regexp.MustCompile(`(?m)^Exec=.*$`) 329 | content = reExec.ReplaceAllString(content, updatedExec) 330 | logMessage("WRN", fmt.Sprintf("The bundled .desktop file (%s) had an incorrect \"Exec=\" line. It has been corrected", bundlePath)) 331 | 332 | // Determine the icon format based on the available icon paths 333 | var icon string 334 | if entry.Png != "" { 335 | icon = entry.Png 336 | } else if entry.Svg != "" { 337 | icon = entry.Svg 338 | } 339 | 340 | // Correct Icon line 341 | reIcon := regexp.MustCompile(`(?m)^Icon=.*$`) 342 | if icon != "" { 343 | newIconLine := fmt.Sprintf("Icon=%s", icon) 344 | content = reIcon.ReplaceAllString(content, newIconLine) 345 | logMessage("WRN", fmt.Sprintf("The bundled .desktop file (%s) had an incorrect \"Icon=\" line. It has been corrected", bundlePath)) 346 | } 347 | 348 | // Only update the TryExec line if it is present 349 | reTryExec := regexp.MustCompile(`(?m)^TryExec=.*$`) 350 | if reTryExec.MatchString(content) { 351 | newTryExecLine := fmt.Sprintf("TryExec=%s", filepath.Base(bundlePath)) 352 | content = reTryExec.ReplaceAllString(content, newTryExecLine) 353 | logMessage("WRN", fmt.Sprintf("The bundled .desktop file (%s) had an incorrect \"TryExec=\" line. It has been corrected", bundlePath)) 354 | } 355 | 356 | return content, nil 357 | } 358 | 359 | func generateThumbnail(path string, png string) (string, error) { 360 | // Generate the canonical URI for the file path 361 | canonicalURI, err := CanonicalURI(path) 362 | if err != nil { 363 | logMessage("ERR", fmt.Sprintf("Couldn't generate canonical URI: %v", err)) 364 | return "", err 365 | } 366 | 367 | // Compute the MD5 hash of the canonical URI 368 | fileMD5 := HashURI(canonicalURI) 369 | 370 | // Determine the thumbnail path 371 | getThumbnailPath, err := getThumbnailPath(fileMD5, "normal") 372 | if err != nil { 373 | logMessage("ERR", fmt.Sprintf("Couldn't generate an appropriate thumbnail path: %v", err)) 374 | return "", err 375 | } 376 | 377 | // Copy the PNG file to the thumbnail path 378 | err = copyFile(png, getThumbnailPath) 379 | if err != nil { 380 | logMessage("ERR", fmt.Sprintf("Failed to create thumbnail file: %v", err)) 381 | return "", err 382 | } 383 | 384 | return getThumbnailPath, nil 385 | } 386 | 387 | // hashChanged checks if the file at filePath has a different hash than what's recorded in config. 388 | // Returns true if the hash is different or the file is not tracked. 389 | func hashChanged(filePath string, config Config) bool { 390 | // Check if the filePath exists in the config's tracker 391 | entry, exists := config.Tracker[filePath] 392 | if !exists { 393 | return true // File is not tracked, treat as a change 394 | } 395 | 396 | // Compute the current hash of the file 397 | currentHash := computeB3SUM(filePath) 398 | 399 | // Compare with the stored hash 400 | return entry.B3SUM != currentHash 401 | } 402 | 403 | // removeNonPrintable removes non-printable characters from a string, including ANSI escape codes. 404 | func removeAnsi(s string) string { 405 | ansiEscape := regexp.MustCompile(`\x1B\[[0-?9;]*[mK]`) 406 | s = ansiEscape.ReplaceAllString(s, "") 407 | return s 408 | } 409 | -------------------------------------------------------------------------------- /cmd/pelfd/appbundle_support.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "path/filepath" 10 | ) 11 | 12 | func integrateAppBundle(path, appPath string, entry *BundleEntry) { 13 | baseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 14 | entry.Png = executeAppBundle(path, "--pbundle_pngIcon", filepath.Join(appPath, baseName+".png")) 15 | entry.Svg = executeAppBundle(path, "--pbundle_svgIcon", filepath.Join(appPath, baseName+".svg")) 16 | entry.Desktop = executeAppBundle(path, "--pbundle_desktop", filepath.Join(appPath, baseName+".desktop")) 17 | } 18 | 19 | func executeAppBundle(bundle, param, outputFile string) string { 20 | logMessage("INF", fmt.Sprintf("Retrieving metadata from %s with parameter: %s", bundle, param)) 21 | // Prepend `sh -c` to the bundle execution 22 | cmd := exec.Command("sh", "-c", bundle+" "+param) 23 | output, err := cmd.Output() 24 | if err != nil { 25 | logMessage("WRN", fmt.Sprintf("Bundle %s with parameter %s didn't return a metadata file", bundle, param)) 26 | return "" 27 | } 28 | 29 | outputStr := string(output) 30 | 31 | // Remove the escape sequence "^[[1F^[[2K" 32 | // Remove the escape sequence from the output 33 | outputStr = strings.ReplaceAll(outputStr, "\x1b[1F\x1b[2K", "") 34 | 35 | data, err := base64.StdEncoding.DecodeString(outputStr) 36 | if err != nil { 37 | logMessage("ERR", fmt.Sprintf("Failed to decode base64 output for %s %s: %v", bundle, param, err)) 38 | return "" 39 | } 40 | 41 | if err := os.WriteFile(outputFile, data, 0644); err != nil { 42 | logMessage("ERR", fmt.Sprintf("Failed to write file %s: %v", outputFile, err)) 43 | return "" 44 | } 45 | 46 | logMessage("INF", fmt.Sprintf("Successfully wrote file: %s", outputFile)) 47 | return outputFile 48 | } 49 | -------------------------------------------------------------------------------- /cmd/pelfd/appimage_support.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | func integrateAppImage(path, appPath string, entry *BundleEntry) { 12 | baseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 13 | entry.Png = extractAppImageMetadata("icon", path, filepath.Join(appPath, baseName+".png")) 14 | entry.Desktop = extractAppImageMetadata("desktop", path, filepath.Join(appPath, baseName+".desktop")) 15 | } 16 | 17 | func extractAppImageMetadata(metadataType, appImagePath, outputFile string) string { 18 | logMessage("INF", fmt.Sprintf("Extracting %s from AppImage: %s", metadataType, appImagePath)) 19 | 20 | // Create a temporary directory for extraction 21 | tempDir, err := os.MkdirTemp("", "appimage-extract-") 22 | if err != nil { 23 | logMessage("ERR", fmt.Sprintf("Failed to create temporary directory: %v", err)) 24 | return "" 25 | } 26 | // Defer the removal of the tempDir to ensure it is deleted at the end of the function 27 | defer func() { 28 | if err := os.RemoveAll(tempDir); err != nil { 29 | logMessage("ERR", fmt.Sprintf("Failed to remove temporary directory: %v", err)) 30 | } 31 | }() 32 | 33 | if err := os.Chdir(tempDir); err != nil { 34 | logMessage("ERR", fmt.Sprintf("Failed to change directory to %s: %v", tempDir, err)) 35 | return "" 36 | } 37 | 38 | var metadataPath string 39 | 40 | switch metadataType { 41 | case "icon": 42 | cmd := exec.Command("sh", "-c", fmt.Sprintf("%s --appimage-extract .DirIcon", appImagePath)) 43 | if err := cmd.Run(); err != nil { 44 | logMessage("WRN", fmt.Sprintf("Failed to extract .DirIcon from AppImage: %s", appImagePath)) 45 | return "" 46 | } 47 | metadataPath = filepath.Join(tempDir, "squashfs-root", ".DirIcon") 48 | case "desktop": 49 | cmd := exec.Command("sh", "-c", fmt.Sprintf("%s --appimage-extract *.desktop", appImagePath)) 50 | if err := cmd.Run(); err != nil { 51 | logMessage("WRN", fmt.Sprintf("Failed to extract .desktop from AppImage: %s", appImagePath)) 52 | return "" 53 | } 54 | // Find the first .desktop file in the directory 55 | files, err := filepath.Glob(filepath.Join(tempDir, "squashfs-root", "*.desktop")) 56 | if err != nil || len(files) == 0 { 57 | logMessage("WRN", fmt.Sprintf(".desktop file not found in AppImage: %s", appImagePath)) 58 | return "" 59 | } 60 | metadataPath = files[0] 61 | default: 62 | logMessage("ERR", fmt.Sprintf("Unknown metadata type: %s", metadataType)) 63 | return "" 64 | } 65 | 66 | if !fileExists(metadataPath) { 67 | logMessage("WRN", fmt.Sprintf("%s not found in AppImage: %s", strings.Title(metadataType), appImagePath)) 68 | return "" 69 | } 70 | 71 | if err := copyFile(metadataPath, outputFile); err != nil { 72 | logMessage("ERR", fmt.Sprintf("Failed to copy %s file: %v", metadataType, err)) 73 | return "" 74 | } 75 | 76 | logMessage("INF", fmt.Sprintf("Successfully extracted %s to: %s", metadataType, outputFile)) 77 | return outputFile 78 | } 79 | -------------------------------------------------------------------------------- /cmd/pelfd/pelfd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/user" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Version indicates the current PELFD version 14 | const Version = "1.9" 15 | 16 | // Options defines the configuration options for the PELFD daemon. 17 | type Options struct { 18 | DirectoriesToWalk []string `json:"directories_to_walk"` // Directories to scan for .AppBundle and .blob files. 19 | ProbeInterval int `json:"probe_interval"` // Interval in seconds between directory scans. 20 | IconDir string `json:"icon_dir"` // Directory to store extracted icons. 21 | AppDir string `json:"app_dir"` // Directory to store .desktop files. 22 | CorrectDesktopFiles bool `json:"correct_desktop_files"` // Flag to enable automatic correction of .desktop files. 23 | IntegrateFormats []string `json:"integrate_formats"` // Formats to integrate 24 | } 25 | 26 | // Config represents the overall configuration structure for PELFD, including scanning options and a tracker for installed bundles. 27 | type Config struct { 28 | Options Options `json:"options"` // PELFD configuration options. 29 | Tracker map[string]*BundleEntry `json:"tracker"` // Tracker mapping bundle paths to their metadata entries. 30 | } 31 | 32 | // BundleEntry represents metadata associated with an installed bundle. 33 | type BundleEntry struct { 34 | B3SUM string `json:"b3sum"` // B3SUM[0..256] hash of the bundle file. 35 | Png string `json:"png,omitempty"` // Path to the PNG icon file, if extracted. 36 | Svg string `json:"svg,omitempty"` // Path to the SVG icon file, if extracted. 37 | Desktop string `json:"desktop,omitempty"` // Path to the corrected .desktop file, if processed. 38 | Thumbnail string `json:"thumbnail,omitempty"` // Path to the 128x128 png thumbnail file, if processed. 39 | HasMetadata bool `json:"has_metadata"` // Indicates if metadata was found. 40 | // LastUpdated int64 `json:"last_updated"` // Epoch date when the entry was last updated. 41 | } 42 | 43 | func main() { 44 | usr, err := user.Current() 45 | if err != nil { 46 | logMessage("ERR", fmt.Sprintf("Failed to get current user: %v", err)) 47 | return 48 | } 49 | if usr.Username == "root" { 50 | logMessage("ERR", "This program cannot run as root.") 51 | return 52 | } 53 | 54 | // User's config directory and config file path 55 | configDir, err := os.UserConfigDir() 56 | if err != nil { 57 | logMessage("ERR", fmt.Sprintf("Failed to determine config directory: %v", err)) 58 | return 59 | } 60 | configFilePath := filepath.Join(configDir, "pelfd.json") 61 | 62 | // Command line flags 63 | version := flag.Bool("version", false, "Print the version number") 64 | integratePath := flag.String("integrate", "", "Manually integrate a specific file or directory") 65 | deintegratePath := flag.String("deintegrate", "", "Manually de-integrate a specific file or directory") 66 | extractPath := flag.String("extract", "", "Extract .DirIcon and .desktop to the specified directory") 67 | outDir := flag.String("outdir", "", "For use with --extract") 68 | flag.Parse() 69 | 70 | // Handle version flag 71 | if *version { 72 | fmt.Printf("Version: %s\n", Version) 73 | return 74 | } 75 | 76 | config := loadConfig(configFilePath, usr.HomeDir) 77 | 78 | // Handle extract flag 79 | if *extractPath != "" && *outDir != "" { 80 | if !fileExists(*extractPath) { 81 | logMessage("ERR", fmt.Sprintf("Specified file for extraction does not exist: %s", *extractPath)) 82 | return 83 | } 84 | extractMetadata(*extractPath, config.Options.IconDir, *outDir) 85 | return 86 | } 87 | 88 | // Create necessary directories 89 | os.MkdirAll(config.Options.IconDir, 0755) 90 | os.MkdirAll(config.Options.AppDir, 0755) 91 | 92 | // Manual integration mode 93 | if *integratePath != "" { 94 | integrateBundle(config, []string{*integratePath}, usr.HomeDir, configFilePath) 95 | return 96 | } 97 | 98 | // Manual deintegration mode 99 | if *deintegratePath != "" { 100 | deintegrateBundle(config, *deintegratePath, configFilePath) 101 | return 102 | } 103 | 104 | // Automatic probing loop 105 | probeInterval := time.Duration(config.Options.ProbeInterval) * time.Second 106 | for { 107 | integrateBundle(config, config.Options.DirectoriesToWalk, usr.HomeDir, configFilePath) 108 | time.Sleep(probeInterval) 109 | } 110 | } 111 | 112 | func integrateBundle(config Config, paths []string, homeDir string, configFilePath string) { 113 | options := config.Options 114 | entries := config.Tracker 115 | changed := false 116 | 117 | refreshBundle := func(bundle string, b3sum string, entry *BundleEntry, options Options) bool { 118 | if entry == nil || entry.B3SUM != b3sum { 119 | if isExecutable(bundle) { 120 | integrateBundleMetadata(bundle, b3sum, entries, options.IconDir, options.AppDir, config) 121 | return true 122 | } 123 | // Bundle is not executable, remove entry 124 | delete(entries, bundle) 125 | return false 126 | } 127 | return false 128 | } 129 | 130 | for _, filePath := range paths { 131 | // Expand the tilde (~) to the user's home directory 132 | filePath = expand(filePath, homeDir) 133 | 134 | // Check if the path is a file or directory 135 | info, err := os.Stat(filePath) 136 | if err != nil { 137 | logMessage("WRN", fmt.Sprintf("Directory does not exist: %s", filePath)) 138 | continue // Skip this file or handle it as needed 139 | } 140 | 141 | if info.IsDir() { 142 | // If it's a directory, process all files within it 143 | files, err := os.ReadDir(filePath) 144 | if err != nil { 145 | logMessage("ERR", fmt.Sprintf("Failed to read directory %s: %v", filePath, err)) 146 | continue // Handle directory read errors 147 | } 148 | 149 | for _, entry := range files { 150 | if !entry.Type().IsRegular() { 151 | logMessage("INF", fmt.Sprintf("Skipping non-regular file in directory: %s", entry.Name())) 152 | continue // Skip non-regular files (like directories, symlinks, etc.) 153 | } 154 | // Process each file within the directory 155 | filePathToIntegrate := filepath.Join(filePath, entry.Name()) 156 | if !isSupportedFile(filePathToIntegrate, options.IntegrateFormats) { 157 | continue // Skip files that are not supported 158 | } 159 | b3sum := computeB3SUM(filePathToIntegrate) 160 | if entry, exists := entries[filePathToIntegrate]; exists { 161 | changed = refreshBundle(filePathToIntegrate, b3sum, entry, options) || changed 162 | checkAndRecreateFiles(entry, filePathToIntegrate, options, &changed) 163 | } else { 164 | logMessage("INF", fmt.Sprintf("New bundle detected: %s", filepath.Base(filePathToIntegrate))) 165 | changed = refreshBundle(filePathToIntegrate, b3sum, nil, options) || changed 166 | } 167 | } 168 | continue // After processing all files, continue with the next path 169 | } 170 | 171 | // If it's a regular file, proceed as before 172 | bundle := filePath 173 | if !isSupportedFile(bundle, options.IntegrateFormats) { 174 | continue // Skip files that are not supported 175 | } 176 | b3sum := computeB3SUM(bundle) 177 | 178 | // Check if the bundle already exists in entries 179 | if entry, exists := entries[bundle]; exists { 180 | changed = refreshBundle(bundle, b3sum, entry, options) || changed 181 | checkAndRecreateFiles(entry, bundle, options, &changed) 182 | } else { 183 | logMessage("INF", fmt.Sprintf("New bundle detected: %s", filepath.Base(bundle))) 184 | changed = refreshBundle(bundle, b3sum, nil, options) || changed 185 | } 186 | } 187 | 188 | // Check for deintegration of non-existing bundles 189 | for bundlePath := range entries { 190 | if !fileExists(bundlePath) { 191 | logMessage("WRN", fmt.Sprintf("Bundle %s does not exist. Deintegrating...", bundlePath)) 192 | deintegrateBundle(config, bundlePath, configFilePath) 193 | changed = true 194 | } 195 | } 196 | 197 | if changed { 198 | saveConfig(config, configFilePath) 199 | } 200 | } 201 | 202 | func checkAndRecreateFiles(entry *BundleEntry, bundle string, options Options, changed *bool) { 203 | if entry == nil { 204 | return 205 | } 206 | 207 | checkAndRecreateFile := func(filePath *string, param, outputDir, extension string) { 208 | if *filePath != "" && !fileExists(*filePath) { 209 | logMessage("WRN", fmt.Sprintf("The file for %s doesn't exist anymore. Re-creating...", filepath.Base(bundle))) 210 | newFilePath := filepath.Join(outputDir, filepath.Base(remExtension(bundle))+extension) 211 | *filePath = executeAppBundle(bundle, param, newFilePath) 212 | if *filePath != "" { 213 | *changed = true 214 | } 215 | } 216 | } 217 | 218 | // Check and recreate thumbnail if missing 219 | if entry.Thumbnail != "" && !fileExists(entry.Thumbnail) { 220 | logMessage("WRN", fmt.Sprintf("The thumbnail file for %s doesn't exist anymore. Generating new thumbnail...", filepath.Base(bundle))) 221 | thumbnailPath, err := generateThumbnail(bundle, entry.Png) 222 | if err != nil { 223 | logMessage("ERR", fmt.Sprintf("Failed to create thumbnail file: %v", err)) 224 | } else { 225 | entry.Thumbnail = thumbnailPath 226 | logMessage("INF", fmt.Sprintf("A new thumbnail for %s was created", filepath.Base(bundle))) 227 | *changed = true 228 | } 229 | } 230 | 231 | // Check and recreate PNG icon if missing 232 | checkAndRecreateFile(&entry.Png, "--pbundle_pngIcon", options.IconDir, ".png") 233 | 234 | // Check and recreate SVG icon if missing 235 | checkAndRecreateFile(&entry.Svg, "--pbundle_svgIcon", options.IconDir, ".svg") 236 | 237 | // Check and recreate desktop file if missing 238 | checkAndRecreateFile(&entry.Desktop, "--pbundle_desktop", options.AppDir, ".desktop") 239 | } 240 | 241 | func deintegrateBundle(config Config, filePath string, configFilePath string) { 242 | entries := config.Tracker 243 | changed := false 244 | 245 | if entry, checked := entries[filePath]; checked && entry != nil { 246 | cleanupBundle(filePath, entries) 247 | changed = true 248 | } else { 249 | logMessage("WRN", fmt.Sprintf("Bundle %s is not integrated.", filePath)) 250 | } 251 | 252 | // Save config if any changes were made 253 | if changed { 254 | logMessage("INF", fmt.Sprintf("Updating %s", configFilePath)) 255 | saveConfig(config, configFilePath) 256 | } 257 | } 258 | 259 | func cleanupBundle(path string, entries map[string]*BundleEntry) { 260 | entry := entries[path] 261 | if entry == nil { 262 | return 263 | } 264 | filesToRemove := []string{entry.Png, entry.Svg, entry.Desktop, entry.Thumbnail} 265 | for _, file := range filesToRemove { 266 | if file == "" { 267 | continue 268 | } 269 | if err := os.Remove(file); err != nil && !os.IsNotExist(err) { 270 | logMessage("ERR", fmt.Sprintf("Failed to remove file: %s %v", file, err)) 271 | } else { 272 | logMessage("INF", fmt.Sprintf("Removed file: %s", file)) 273 | } 274 | } 275 | delete(entries, path) 276 | } 277 | 278 | func extractMetadata(filePath, iconDir, appDir string) { 279 | baseName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) 280 | 281 | // Extract .DirIcon 282 | iconPath := filepath.Join(iconDir, baseName+".png") 283 | if extractedIcon := extractAppImageMetadata("icon", filePath, iconPath); extractedIcon != "" { 284 | logMessage("INF", fmt.Sprintf("Icon extracted to: %s", extractedIcon)) 285 | } else { 286 | logMessage("WRN", "Failed to extract icon") 287 | } 288 | 289 | // Extract .desktop 290 | desktopPath := filepath.Join(appDir, baseName+".desktop") 291 | if extractedDesktop := extractAppImageMetadata("desktop", filePath, desktopPath); extractedDesktop != "" { 292 | logMessage("INF", fmt.Sprintf("Desktop file extracted to: %s", extractedDesktop)) 293 | } else { 294 | logMessage("WRN", "Failed to extract desktop file") 295 | } 296 | } 297 | 298 | func isSupportedFile(filePath string, integrateFormats []string) bool { 299 | if len(integrateFormats) == 0 { 300 | return strings.HasSuffix(filePath, ".AppBundle") || strings.HasSuffix(filePath, ".AppImage") || strings.HasSuffix(filePath, ".NixAppImage") || strings.HasSuffix(filePath, ".AppDir") 301 | } 302 | for _, format := range integrateFormats { 303 | if strings.HasSuffix(filePath, format) { 304 | return true 305 | } 306 | } 307 | return false 308 | } 309 | 310 | // Function map for handling different formats 311 | var formatHandlers = map[string]func(string, string, *BundleEntry){ 312 | ".AppImage": integrateAppImage, 313 | ".NixAppImage": integrateAppImage, 314 | ".AppBundle": integrateAppBundle, 315 | ".AppDir": integrateAppDir, 316 | } 317 | 318 | func integrateMetadata(path, b3sum string, entries map[string]*BundleEntry, iconPath, appPath string, cfg Config) { 319 | entry := &BundleEntry{B3SUM: b3sum, HasMetadata: false} 320 | baseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) 321 | 322 | ext := filepath.Ext(path) 323 | if handler, ok := formatHandlers[ext]; ok { 324 | handler(path, appPath, entry) 325 | } else { 326 | logMessage("WRN", fmt.Sprintf("Unsupported format: %s", ext)) 327 | return 328 | } 329 | 330 | if entry.Png != "" || entry.Svg != "" || entry.Desktop != "" { 331 | entry.HasMetadata = true 332 | logMessage("INF", fmt.Sprintf("Adding bundle to entries: %s", path)) 333 | entries[path] = entry 334 | } else { 335 | logMessage("WRN", fmt.Sprintf("Bundle does not contain any metadata files. Skipping: %s", path)) 336 | entries[path] = entry 337 | } 338 | 339 | createThumbnailForBundle(entry, path) 340 | updateDesktopFileIfRequired(path, baseName, appPath, entry, cfg) 341 | } 342 | -------------------------------------------------------------------------------- /cmd/pelfd/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/url" 10 | "os" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | 15 | "github.com/goccy/go-json" 16 | "github.com/liamg/tml" 17 | "github.com/zeebo/blake3" 18 | ) 19 | 20 | func logMessage(level, message string) string { 21 | logColors := map[string]string{ 22 | "INF": "INF:", 23 | "WRN": "WRN:", 24 | "ERR": "ERR:", 25 | } 26 | 27 | color, exists := logColors[level] 28 | if !exists { 29 | color = "LOG:" 30 | } 31 | 32 | formattedMessage := tml.Sprintf(fmt.Sprintf("%s %s", color, message)) 33 | log.Println(formattedMessage) 34 | 35 | return fmt.Sprintf("%s %s", level, message) 36 | } 37 | 38 | func createThumbnailForBundle(entry *BundleEntry, path string) { 39 | if entry.Png != "" { 40 | thumbnailPath, err := generateThumbnail(path, entry.Png) 41 | if err != nil { 42 | logMessage("ERR", fmt.Sprintf("Failed to create thumbnail file: %v", err)) 43 | } 44 | entry.Thumbnail = thumbnailPath 45 | logMessage("INF", fmt.Sprintf("A thumbnail for %s was created at: %s", path, thumbnailPath)) 46 | } 47 | } 48 | 49 | func updateDesktopFileIfRequired(path, baseName, appPath string, entry *BundleEntry, cfg Config) { 50 | desktopPath := filepath.Join(appPath, baseName+".desktop") 51 | if _, err := os.Stat(desktopPath); err == nil { 52 | content, err := os.ReadFile(desktopPath) 53 | if err != nil { 54 | logMessage("ERR", fmt.Sprintf("Failed to read .desktop file: %v", err)) 55 | return 56 | } 57 | if cfg.Options.CorrectDesktopFiles { 58 | updatedContent, err := updateDesktopFile(string(content), path, entry) 59 | if err != nil { 60 | logMessage("ERR", fmt.Sprintf("Failed to update .desktop file: %v", err)) 61 | return 62 | } 63 | if err := os.Remove(desktopPath); err != nil && !os.IsNotExist(err) { 64 | logMessage("ERR", fmt.Sprintf("Failed to remove existing .desktop file: %v", err)) 65 | return 66 | } 67 | if err := os.WriteFile(desktopPath, []byte(updatedContent), 0644); err != nil { 68 | logMessage("ERR", fmt.Sprintf("Failed to write updated .desktop file: %v", err)) 69 | return 70 | } 71 | } 72 | } 73 | } 74 | 75 | func loadConfig(configPath string, homeDir string) Config { 76 | config := Config{ 77 | Options: Options{ 78 | DirectoriesToWalk: []string{"~/Applications"}, 79 | ProbeInterval: 5, 80 | IconDir: filepath.Join(homeDir, ".local/share/icons"), 81 | AppDir: filepath.Join(homeDir, ".local/share/applications"), 82 | CorrectDesktopFiles: true, 83 | }, 84 | Tracker: make(map[string]*BundleEntry), 85 | } 86 | 87 | file, err := os.Open(configPath) 88 | if err != nil { 89 | if os.IsNotExist(err) { 90 | logMessage("INF", fmt.Sprintf("Config file does not exist: %s, creating a new one", configPath)) 91 | saveConfig(config, configPath) 92 | return config 93 | } 94 | logMessage("ERR", fmt.Sprintf("Failed to open config file %s: %v", configPath, err)) 95 | os.Exit(1) 96 | } 97 | defer file.Close() 98 | 99 | decoder := json.NewDecoder(file) 100 | if err := decoder.Decode(&config); err != nil { 101 | logMessage("ERR", fmt.Sprintf("Failed to decode config file: %v", err)) 102 | os.Exit(1) 103 | } 104 | 105 | return config 106 | } 107 | 108 | func saveConfig(config Config, path string) { 109 | file, err := os.Create(path) 110 | if err != nil { 111 | logMessage("ERR", fmt.Sprintf("Failed to save config file: %v", err)) 112 | } 113 | defer file.Close() 114 | 115 | encoder := json.NewEncoder(file) 116 | encoder.SetIndent("", " ") 117 | if err := encoder.Encode(config); err != nil { 118 | logMessage("ERR", fmt.Sprintf("Failed to encode config file: %v", err)) 119 | os.Exit(1) 120 | } 121 | } 122 | 123 | // fileExists checks if a file exists. 124 | func fileExists(filePath string) bool { 125 | _, err := os.Stat(filePath) 126 | if err == nil { 127 | return true 128 | } 129 | if os.IsNotExist(err) { 130 | return false 131 | } 132 | // If there's any other error, we consider that the file doesn't exist for simplicity 133 | return false 134 | } 135 | 136 | func remExtension(filePath string) string { 137 | return strings.Split(filePath, ".")[0] 138 | } 139 | 140 | func expand(filePath, homeDir string) string { 141 | // Expand the tilde (~) to the user's home directory 142 | if strings.HasPrefix(filePath, "~") { 143 | filePath = filepath.Join(homeDir, filePath[1:]) // Replace ~ with the home directory 144 | } 145 | return filePath 146 | } 147 | 148 | // HashURI computes the MD5 hash of the canonical URI. 149 | func HashURI(uri string) string { 150 | hash := md5.Sum([]byte(uri)) 151 | return hex.EncodeToString(hash[:]) 152 | } 153 | 154 | // CanonicalURI generates the canonical URI for a given file path. 155 | func CanonicalURI(filePath string) (string, error) { 156 | absPath, err := filepath.Abs(filePath) 157 | if err != nil { 158 | return "", err 159 | } 160 | uri := url.URL{Scheme: "file", Path: absPath} 161 | return uri.String(), nil 162 | } 163 | 164 | func isExecutable(path string) bool { 165 | info, err := os.Stat(path) 166 | if err != nil { 167 | logMessage("ERR", fmt.Sprintf("Failed to stat file %s: %v", path, err)) 168 | return false 169 | } 170 | mode := info.Mode() 171 | return mode&0111 != 0 172 | } 173 | 174 | // isDirectory checks if the given path is a directory. 175 | func isDirectory(path string) bool { 176 | info, err := os.Stat(path) 177 | if os.IsNotExist(err) { 178 | return false // Path does not exist 179 | } 180 | return err == nil && info.IsDir() // Check for error and if it's a directory 181 | } 182 | 183 | // computeB3SUM computes the Blake3 hash of the file at the given path. 184 | func computeB3SUM(path string) string { 185 | file, err := os.Open(path) 186 | if err != nil { 187 | logMessage("ERR", fmt.Sprintf("Failed to open file %s: %v", path, err)) 188 | return "" 189 | } 190 | defer file.Close() 191 | 192 | hasher := blake3.New() 193 | if _, err := io.Copy(hasher, file); err != nil { 194 | logMessage("ERR", fmt.Sprintf("Failed to compute Blake3 hash of %s: %v", path, err)) 195 | os.Exit(1) 196 | } 197 | 198 | return hex.EncodeToString(hasher.Sum(nil)) 199 | } 200 | 201 | // ThumbnailPath returns the path where the thumbnail should be saved. 202 | func getThumbnailPath(fileMD5 string, thumbnailType string) (string, error) { 203 | // Determine the base directory for thumbnails 204 | baseDir, err := os.UserCacheDir() 205 | if err != nil { 206 | return "", err 207 | } 208 | thumbnailDir := filepath.Join(baseDir, "thumbnails") 209 | 210 | // Determine the size directory based on thumbnail type 211 | sizeDir := "" 212 | switch thumbnailType { 213 | case "normal": 214 | sizeDir = "normal" 215 | case "large": 216 | sizeDir = "large" 217 | default: 218 | return "", fmt.Errorf("invalid thumbnail type: %s", thumbnailType) 219 | } 220 | 221 | // Create the full directory path 222 | fullDir := filepath.Join(thumbnailDir, sizeDir) 223 | err = os.MkdirAll(fullDir, os.ModePerm) 224 | if err != nil { 225 | return "", err 226 | } 227 | 228 | // Create the final path for the thumbnail 229 | thumbnailPath := filepath.Join(fullDir, fileMD5+".png") 230 | 231 | return thumbnailPath, nil 232 | } 233 | 234 | // copyFile copies a file from src to dst. 235 | func copyFile(src, dst string) error { 236 | // Open the source file 237 | srcFile, err := os.Open(src) 238 | if err != nil { 239 | return err 240 | } 241 | defer srcFile.Close() 242 | 243 | // Create the destination file 244 | dstFile, err := os.Create(dst) 245 | if err != nil { 246 | return err 247 | } 248 | defer dstFile.Close() 249 | 250 | // Copy the content from source to destination 251 | _, err = io.Copy(dstFile, srcFile) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | return nil 257 | } 258 | 259 | func updateDesktopFile(content, bundlePath string, entry *BundleEntry) (string, error) { 260 | // Correct Exec line 261 | updatedExec := fmt.Sprintf("Exec=%s", bundlePath) 262 | 263 | // Define a regular expression to match the Exec line. 264 | reExec := regexp.MustCompile(`(?m)^Exec=.*$`) 265 | content = reExec.ReplaceAllString(content, updatedExec) 266 | logMessage("WRN", fmt.Sprintf("The bundled .desktop file (%s) had an incorrect \"Exec=\" line. It has been corrected", bundlePath)) 267 | 268 | // Determine the icon format based on the available icon paths 269 | var icon string 270 | if entry.Png != "" { 271 | icon = entry.Png 272 | } else if entry.Svg != "" { 273 | icon = entry.Svg 274 | } 275 | 276 | // Correct Icon line 277 | reIcon := regexp.MustCompile(`(?m)^Icon=.*$`) 278 | if icon != "" { 279 | newIconLine := fmt.Sprintf("Icon=%s", icon) 280 | content = reIcon.ReplaceAllString(content, newIconLine) 281 | logMessage("WRN", fmt.Sprintf("The bundled .desktop file (%s) had an incorrect \"Icon=\" line. It has been corrected", bundlePath)) 282 | } 283 | 284 | // Only update the TryExec line if it is present 285 | reTryExec := regexp.MustCompile(`(?m)^TryExec=.*$`) 286 | if reTryExec.MatchString(content) { 287 | newTryExecLine := fmt.Sprintf("TryExec=%s", filepath.Base(bundlePath)) 288 | content = reTryExec.ReplaceAllString(content, newTryExecLine) 289 | logMessage("WRN", fmt.Sprintf("The bundled .desktop file (%s) had an incorrect \"TryExec=\" line. It has been corrected", bundlePath)) 290 | } 291 | 292 | return content, nil 293 | } 294 | 295 | func generateThumbnail(path string, png string) (string, error) { 296 | // Generate the canonical URI for the file path 297 | canonicalURI, err := CanonicalURI(path) 298 | if err != nil { 299 | logMessage("ERR", fmt.Sprintf("Couldn't generate canonical URI: %v", err)) 300 | return "", err 301 | } 302 | 303 | // Compute the MD5 hash of the canonical URI 304 | fileMD5 := HashURI(canonicalURI) 305 | 306 | // Determine the thumbnail path 307 | getThumbnailPath, err := getThumbnailPath(fileMD5, "normal") 308 | if err != nil { 309 | logMessage("ERR", fmt.Sprintf("Couldn't generate an appropriate thumbnail path: %v", err)) 310 | return "", err 311 | } 312 | 313 | // Copy the PNG file to the thumbnail path 314 | err = copyFile(png, getThumbnailPath) 315 | if err != nil { 316 | logMessage("ERR", fmt.Sprintf("Failed to create thumbnail file: %v", err)) 317 | return "", err 318 | } 319 | 320 | return getThumbnailPath, nil 321 | } 322 | 323 | // hashChanged checks if the file at filePath has a different hash than what's recorded in config. 324 | // Returns true if the hash is different or the file is not tracked. 325 | func hashChanged(filePath string, config Config) bool { 326 | // Check if the filePath exists in the config's tracker 327 | entry, exists := config.Tracker[filePath] 328 | if !exists { 329 | return true // File is not tracked, treat as a change 330 | } 331 | 332 | // Compute the current hash of the file 333 | currentHash := computeB3SUM(filePath) 334 | 335 | // Compare with the stored hash 336 | return entry.B3SUM != currentHash 337 | } 338 | 339 | // removeNonPrintable removes non-printable characters from a string, including ANSI escape codes. 340 | func removeAnsi(s string) string { 341 | ansiEscape := regexp.MustCompile(`\x1B\[[0-?9;]*[mK]`) 342 | s = ansiEscape.ReplaceAllString(s, "") 343 | return s 344 | } 345 | -------------------------------------------------------------------------------- /cmd/pfusermount/cbuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | OPWD="$PWD" 4 | BASE="$(dirname "$(realpath "$0")")" 5 | if [ "$OPWD" != "$BASE" ]; then 6 | echo "... $BASE is not the same as $PWD ..." 7 | echo "Going into $BASE and coming back here in a bit" 8 | cd "$BASE" || exit 1 9 | fi 10 | trap 'cd "$OPWD"' EXIT 11 | 12 | # Function to log to stdout with green color 13 | log() { 14 | _reset="\033[m" 15 | _blue="\033[34m" 16 | printf "${_blue}->${_reset} %s\n" "$*" 17 | } 18 | 19 | # Function to log_warning to stdout with yellow color 20 | log_warning() { 21 | _reset="\033[m" 22 | _yellow="\033[33m" 23 | printf "${_yellow}->${_reset} %s\n" "$*" 24 | } 25 | 26 | # Function to log_error to stdout with red color 27 | log_error() { 28 | _reset="\033[m" 29 | _red="\033[31m" 30 | printf "${_red}->${_reset} %s\n" "$*" 31 | exit 1 32 | } 33 | 34 | unnappear() { 35 | "$@" >/dev/null 2>&1 36 | } 37 | 38 | # Check if a dependency is available. 39 | available() { 40 | unnappear which "$1" || return 1 41 | } 42 | 43 | # Exit if a dependency is not available 44 | require() { 45 | available "$1" || log_error "[$1] is not installed. Please ensure the command is available [$1] and try again." 46 | } 47 | 48 | download() { 49 | log "Downloading $1" 50 | if ! wget -U "dbin" -O "./$(basename "$1")" "https://bin.pkgforge.dev/$(uname -m)_$(uname)/$1"; then 51 | log_error "Unable to download [$1]" 52 | fi 53 | chmod +x "./$1" 54 | } 55 | 56 | build_project() { 57 | # FUSERMOUNT 58 | log 'Compiling "pfusermount"' 59 | go build -o ./fusermount ./fusermount.go 60 | # FUSERMOUNT3 61 | log 'Compiling "pfusermount3"' 62 | go build -o ./fusermount3 ./fusermount3.go 63 | } 64 | 65 | clean_project() { 66 | log "Starting clean process" 67 | echo "rm ./*fusermount" 68 | unnappear rm ./*fusermount 69 | echo "rm ./*fusermount3" 70 | unnappear rm ./*fusermount3 71 | log "Clean process completed" 72 | } 73 | 74 | retrieve_executable() { 75 | readlink -f ./pfusermount 76 | readlink -f ./pfusermount3 77 | } 78 | 79 | # Main case statement for actions 80 | case "$1" in 81 | "" | "build") 82 | require go 83 | #log "Checking if embeddable assets are available" 84 | #if [ ! -f "./fusermount" ] || [ ! -f "./fusermount3" ]; then 85 | log "Procuring embeddable goodies" 86 | download "Baseutils/fuse/fusermount" 87 | download "Baseutils/fuse3/fusermount3" 88 | #fi 89 | log "Starting build process" 90 | build_project 91 | ;; 92 | "clean") 93 | clean_project 94 | ;; 95 | "retrieve") 96 | retrieve_executable 97 | ;; 98 | *) 99 | log_warning "Usage: $0 {build|clean|retrieve}" 100 | exit 1 101 | ;; 102 | esac 103 | -------------------------------------------------------------------------------- /cmd/pfusermount/fusermount.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | ) 11 | 12 | //go:embed fusermount 13 | var fusermount embed.FS 14 | 15 | func main() { 16 | // Extract the fusermount binary to a temporary location 17 | tempDir := os.TempDir() 18 | fusermountPath := filepath.Join(tempDir, "fusermount") 19 | fusermountData, err := fusermount.ReadFile("fusermount") 20 | if err != nil { 21 | fmt.Fprintf(os.Stderr, "Error reading embedded fusermount: %v\n", err) 22 | os.Exit(1) 23 | } 24 | err = os.WriteFile(fusermountPath, fusermountData, 0755) 25 | if err != nil { 26 | fmt.Fprintf(os.Stderr, "Error writing fusermount to temp directory: %v\n", err) 27 | os.Exit(1) 28 | } 29 | 30 | // Check if unshare is available 31 | unshareCmd := exec.Command("unshare") 32 | var out bytes.Buffer 33 | unshareCmd.Stdout = &out 34 | err = unshareCmd.Run() 35 | 36 | var cmd *exec.Cmd 37 | if err == nil { 38 | // unshare is available, use unshare 39 | args := []string{"--mount", "--user", "-r", fusermountPath} 40 | args = append(args, os.Args[1:]...) 41 | cmd = exec.Command("unshare", args...) 42 | } else { 43 | // unshare is not available, run fusermount directly 44 | cmd = exec.Command(fusermountPath, os.Args[1:]...) 45 | } 46 | 47 | // Set stdout and stderr for the command 48 | cmd.Stdout = os.Stdout 49 | cmd.Stderr = os.Stderr 50 | 51 | // Run the command 52 | _ = cmd.Run() 53 | } 54 | -------------------------------------------------------------------------------- /cmd/pfusermount/fusermount3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | ) 11 | 12 | //go:embed fusermount3 13 | var fusermount embed.FS 14 | 15 | func main() { 16 | // Extract the fusermount3 binary to a temporary location 17 | tempDir := os.TempDir() 18 | fusermountPath := filepath.Join(tempDir, "fusermount3") 19 | fusermountData, err := fusermount.ReadFile("fusermount3") 20 | if err != nil { 21 | fmt.Fprintf(os.Stderr, "Error reading embedded fusermount3: %v\n", err) 22 | os.Exit(1) 23 | } 24 | err = os.WriteFile(fusermountPath, fusermountData, 0755) 25 | if err != nil { 26 | fmt.Fprintf(os.Stderr, "Error writing fusermount3 to temp directory: %v\n", err) 27 | os.Exit(1) 28 | } 29 | 30 | // Check if unshare is available 31 | unshareCmd := exec.Command("unshare") 32 | var out bytes.Buffer 33 | unshareCmd.Stdout = &out 34 | err = unshareCmd.Run() 35 | 36 | var cmd *exec.Cmd 37 | if err == nil { 38 | // unshare is available, use unshare 39 | args := []string{"--mount", "--user", "-r", fusermountPath} 40 | args = append(args, os.Args[1:]...) 41 | cmd = exec.Command("unshare", args...) 42 | } else { 43 | // unshare is not available, run fusermount directly 44 | cmd = exec.Command(fusermountPath, os.Args[1:]...) 45 | } 46 | 47 | // Set stdout and stderr for the command 48 | cmd.Stdout = os.Stdout 49 | cmd.Stderr = os.Stderr 50 | 51 | // Run the command 52 | _ = cmd.Run() 53 | } 54 | -------------------------------------------------------------------------------- /docs/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '2025-04-25T15:48:50' 3 | draft = false 4 | title = 'Documentation Index' 5 | [params.author] 6 | name = 'xplshn' 7 | email = 'xplshn@murena.io' 8 | +++ 9 | 10 |

11 | Automate once, package forever, distribute everywhere 12 |

13 | 14 |

15 | The following documents explain how an AppBundle behaves, how, and why. 16 | They also explain the reasoning behind these design choices. 17 |
18 | These documents can be a starting point for reimplementing the existing AppBundle tooling. 19 |

20 | -------------------------------------------------------------------------------- /docs/format.md: -------------------------------------------------------------------------------- 1 | # AppBundle File Format Specification 2 | 3 | This document outlines the structure and composition of an AppBundle, a self-contained executable format designed to package applications with their dependencies for portable execution on Linux systems. 4 | 5 | ## File Structure 6 | 7 | An AppBundle is a single executable file that combines an ELF (Executable and Linkable Format) runtime with an appended filesystem image containing the application's data. The structure is as follows: 8 | 9 | 1. **ELF Runtime**: 10 | - The AppBundle begins with an ELF executable, identifiable by the magic bytes "AB" or optionally "AI" at the start of the file. 11 | - This runtime is responsible for handling the execution logic, including mounting or extracting the filesystem image and setting up the environment. 12 | 13 | 2. **Runtime Information Section (.pbundle_runtime_info)**: 14 | - The ELF file contains a section named `.pbundle_runtime_info`, which stores metadata in CBOR (Concise Binary Object Representation) format. 15 | - The structure of this section is defined in Go as: 16 | ```go 17 | type RuntimeInfo struct { 18 | AppBundleID string `json:"AppBundleID"` // Unique identifier for the AppBundle 19 | PelfVersion string `json:"PelfVersion"` // Version of the pelf tool used to create the AppBundle 20 | HostInfo string `json:"HostInfo"` // System information from `uname -mrsp(v)` 21 | FilesystemType string `json:"FilesystemType"` // Filesystem type: "dwarfs" or "squashfs" 22 | Hash string `json:"Hash"` // Hash of the filesystem image 23 | DisableRandomWorkDir bool `json:"DisableRandomWorkDir"` // Whether to use a fixed working directory 24 | MountOrExtract uint8 `json:"MountOrExtract"` // Run behavior: 0 (FUSE only), 1 (Extract only), 2 (FUSE with extract fallback), 3 (FUSE with extract fallback for files < 350MB) 25 | } 26 | ``` 27 | 28 | 3. **Static Tools Section (.pbundle_static_tools)**: 29 | - The ELF file includes a section named `.pbundle_static_tools`, containing a Zstandard (ZSTD)-compressed tar archive. 30 | - This archive holds tools necessary for mounting or extracting the filesystem image, such as `dwarfs`, `dwarfsextract`, `squashfuse`, or `unsquashfs`, depending on the filesystem type. 31 | 32 | 4. **Filesystem Image**: 33 | - Immediately following the ELF runtime, the AppBundle contains the compressed filesystem image (either DwarFS or SquashFS). 34 | - This image encapsulates the application's AppDir, including all necessary files and dependencies. 35 | 36 | ## Creation of an AppBundle 37 | 38 | An AppBundle is created using the `pelf` tool, which performs the following steps: 39 | 40 | 1. **Prepare the AppDir**: 41 | - The `pelfCreator` tool constructs an AppDir, a directory containing the application's files, including: 42 | - `AppRun`: The entrypoint script that orchestrates the execution. 43 | - `.DirIcon`: An optional icon file (PNG, in sizes 512x512, 256x256, or 128x128). 44 | - `.DirIcon.svg`: An optional SVG icon. 45 | - `program.desktop`: An optional desktop entry file. 46 | - `program.appdata.xml`: An optional AppStream metadata file. 47 | - `proto` or `rootfs`: A directory containing the application's filesystem, typically based on a minimal Linux distribution like Alpine or ArchLinux. 48 | - The AppDir may also include additional binaries and configuration files as needed. 49 | 50 | 2. **Embed Runtime Information**: 51 | - The `pelf` tool embeds the `.pbundle_runtime_info` section with metadata about the AppBundle, including its ID, filesystem type, and runtime behavior. 52 | 53 | 3. **Embed Static Tools**: 54 | - Tools required for mounting or extracting the filesystem (e.g., `dwarfs`, `squashfuse`) are compressed into a ZSTD tar archive and embedded in the `.pbundle_static_tools` section. 55 | 56 | 4. **Append Filesystem Image**: 57 | - The AppDir is compressed into a DwarFS or SquashFS image, depending on the configuration, and appended to the ELF runtime. 58 | - The offset of the filesystem image is recorded in the runtime configuration for access during execution. 59 | 60 | 5. **Finalize the Executable**: 61 | - The `pelf` tool combines the ELF runtime, runtime information, static tools, and filesystem image into a single executable file with the `.AppBundle` extension. 62 | 63 | ## Run Behaviors 64 | 65 | The `MountOrExtract` field in the `.pbundle_runtime_info` section determines how the AppBundle behaves when executed: 66 | 67 | - **0 (FUSE Mounting Only)**: The AppBundle uses FUSE to mount the filesystem image. If FUSE is unavailable, it fails without falling back to extraction. 68 | - **1 (Extract and Run)**: The AppBundle extracts the filesystem image to a temporary directory (typically in `tmpfs`) and executes from there, ignoring FUSE even if available. 69 | - **2 (FUSE with Fallback)**: The AppBundle attempts to use FUSE to mount the filesystem. If FUSE is unavailable, it falls back to extracting the filesystem to `tmpfs`. 70 | - **3 (FUSE with Conditional Fallback)**: Similar to option 2, but fallback to extraction only occurs if the AppBundle file is smaller than 350MB. 71 | 72 | ## Expected Contents of the Filesystem Image 73 | 74 | The filesystem image within the AppBundle is expected to be an AppDir with at least the following: 75 | 76 | - **AppRun**: A shell script that serves as the entrypoint for the application. It sets up the environment and executes the main program. 77 | - **Optional Files**: 78 | - `.DirIcon`: A PNG icon in a standard size (512x512, 256x256, or 128x128). 79 | - `.DirIcon.svg`: An SVG icon 80 | - `program.desktop`: A desktop entry file for integration with desktop environments. 81 | - `program.appdata.xml`: An AppStream metadata file for application metadata. 82 | - `proto` or `rootfs`: A directory containing the application's filesystem, including binaries, libraries, and configuration files. 83 | 84 | ## Notes 85 | 86 | - The AppBundle format is designed to be self-contained, requiring no external dependencies for execution in most cases, assuming the necessary tools are embedded or available on the host system. 87 | - The choice of filesystem (DwarFS or SquashFS) affects the tools included in the `.pbundle_static_tools` section and the runtime behavior. 88 | -------------------------------------------------------------------------------- /docs/runtime.md: -------------------------------------------------------------------------------- 1 | # AppBundle Runtime Execution 2 | 3 | This document describes how the AppBundle runtime operates, including how it reads its own information, extracts static tools, determines environment variables, and handles runtime flags. 4 | 5 | ## Execution Flow 6 | 7 | When an AppBundle is executed, the runtime performs the following steps: 8 | 9 | 1. **Read Runtime Information**: 10 | - The runtime reads the `.pbundle_runtime_info` section from the ELF file, which contains CBOR-encoded metadata. 11 | - This section includes: 12 | - `AppBundleID`: A unique identifier for the AppBundle. (e.g: "com.brave.Browser-xplshn-2025-05-19". You're not forced to follow this format, but if you do, you can create a [dbin](https://github.com/xplshn/dbin) repository that countains your AppBundle by using our [appstream-helper](https://github.com/xplshn/pelf/blob/master/cmd/misc/appstream-helper/appstream-helper.go) tool. `$NAME-$MAINTAINER-$DATE` or preferably: `$APPSTREAM_ID-$MAINTAINER-$DATE`, so that you don't have to include an AppStream file within the AppDir for appstream-helper to get metadata from it) 13 | - `PelfVersion`: The version of the `pelf` tool used to create the AppBundle. 14 | - `HostInfo`: System information from `uname -mrsp(v)` of the build machine. 15 | - `FilesystemType`: Either "dwarfs" or "squashfs". 16 | - `Hash`: A hash of the filesystem image for integrity verification. 17 | - `DisableRandomWorkDir`: A boolean indicating whether to use a fixed working directory. 18 | - `MountOrExtract`: A uint8 value (0–3) specifying the run behavior (see below). 19 | - The runtime uses this information to configure its behavior and locate the filesystem image. 20 | 21 | 2. **Extract Static Tools**: 22 | - The runtime accesses the static tools required for mounting or extracting the filesystem (e.g., `dwarfs`, `dwarfsextract`, `squashfuse`, `unsquashfs`). 23 | - The handling of static tools depends on the build mode: 24 | - **noEmbed Edition**: The tools are embedded in the `.pbundle_static_tools` ELF section as a ZSTD-compressed tar archive. The runtime determines the filesystem mounting and extraction commands at runtime, extracts the needed files from this archive to a temporary directory (`cfg.staticToolsDir`), and uses them to either mount or extract the filesystem. 25 | - **Embed Edition**: The tools are embedded directly in the binary using Go’s `embed` package, without compression. The runtime accesses these tools directly from the embedded filesystem, without needing to extract a compressed archive. 26 | 27 | 3. **Exported Env Variables**: 28 | - The runtime sets up several environment variables to facilitate execution: 29 | - **HOME**: If a portable home directory (`.AppBundleID.home`) exists in the same directory as the AppBundle, it is used as `$HOME`. 30 | - **XDG_DATA_HOME**: If a portable share directory (`.AppBundleID.share`) exists, it is used as `$XDG_DATA_HOME`. 31 | - **XDG_CONFIG_HOME**: If a portable config directory (`.AppBundleID.config`) exists, it is used as `$XDG_CONFIG_HOME`. 32 | - **APPDIR**: Set to the mount or extraction directory 33 | - **SELF**: The absolute path to the AppBundle executable. 34 | - **ARGV0**: The basename of `$SELF` 35 | - **PATH**: Augmented to include the AppBundle's `bin` directory and the directory containing the static tools. 36 | 37 | 4. **Mount or Extract Filesystem**: 38 | - The runtime decides whether to mount or extract the filesystem image based on the `MountOrExtract` value: 39 | - **0**: Mounts the filesystem using FUSE (e.g., `dwarfs` or `squashfuse`) and fails if FUSE is unavailable. 40 | - **1**: Extracts the filesystem to a temporary directory (usually in `tmpfs`) and runs from there. 41 | - **2**: Attempts to mount with FUSE; falls back to extraction if FUSE is unavailable. 42 | - **3**: Similar to 2, but only falls back to extraction if the AppBundle is smaller than 350MB. 43 | 44 | 5. **Execute the Application**: 45 | - The runtime executes the `AppRun` script within the AppDir. 46 | - If a specific command is provided via `--pbundle_link`, the runtime executes that command within the AppBundle's environment, instead of executing the AppRun. 47 | 48 | ## Runtime Flags 49 | 50 | The AppBundle runtime supports several command-line flags to modify its behavior: 51 | 52 | - **`--pbundle_help`**: Displays help information, including the `PelfVersion`, `HostInfo`, and internal configuration variables (e.g., `cfg.exeName`, `cfg.mountDir`). 53 | - **`--pbundle_list`**: Lists the contents of the AppBundle's filesystem, including static tools. 54 | - **`--pbundle_link `**: Executes a specified command within the AppBundle's environment, leveraging its `PATH` and other variables. 55 | - **`--pbundle_pngIcon`**: Outputs the base64-encoded `.DirIcon` (PNG) if it exists; otherwise, exits with error code 1. 56 | - **`--pbundle_svgIcon`**: Outputs the base64-encoded `.DirIcon.svg` if it exists; otherwise, exits with error code 1. 57 | - **`--pbundle_appstream`**: Outputs the base64-encoded first `.xml` file (AppStream metadata) found in the AppDir. 58 | - **`--pbundle_desktop`**: Outputs the base64-encoded first `.desktop` file found in the AppDir. 59 | - **`--pbundle_portableHome`**: Creates a portable home directory (`.AppBundleID.home`) in the same directory as the AppBundle. 60 | - **`--pbundle_portableConfig`**: Creates a portable config directory (`.AppBundleID.config`) in the same directory as the AppBundle. 61 | - **`--pbundle_cleanup`**: Unmounts and removes the AppBundle's working directory and mount point, affecting only instances of the same AppBundle. 62 | - **`--pbundle_mount`**: Mounts the filesystem to a specified or default directory and keeps the mount active. 63 | - **`--pbundle_extract [globs]`**: Extracts the filesystem to a directory (default: `_` or `squashfs-root` for AppImage compatibility). Supports selective extraction with glob patterns. 64 | - **`--pbundle_extract_and_run`**: Extracts the filesystem and immediately executes the entrypoint. 65 | - **`--pbundle_offset`**: Outputs the offset of the filesystem image within the AppBundle. 66 | - **AppImage Compatibility Flags**: 67 | - `--appimage-extract`: Same as `--pbundle_extract`, but uses `squashfs-root` as the output directory. 68 | - `--appimage-extract-and-run`: Same as `--pbundle_extract_and_run`. 69 | - `--appimage-mount`: Same as `--pbundle_mount`. 70 | - `--appimage-offset`: Same as `--pbundle_offset`. 71 | 72 | ## Notes 73 | 74 | - The choice between `noEmbed` and embed modes affects how static tools are stored and accessed. The `noEmbed` mode uses a compressed archive for flexibility, while the embed mode simplifies access by avoiding compression. 75 | - The `AppRun` script (e.g., `AppRun.rootfs-based`, `AppRun.sharun`, or `AppRun.sharun.ovfsProto`) determines sandboxing and execution behavior, such as using `bwrap` or `unionfs-fuse`. 76 | - The runtime ensures cleanup of temporary directories unless `--pbundle_cleanup` is explicitly called or `noCleanup` is set. 77 | - The `noEmbed` build tag for the `appbundle-runtime` allows you to build a single appbundle-runtime binary, that determines which filesystem to use at runtime, after having read its .pbundle_runtime_info and decompressed the .tar.zst data within the .pbundle_static_tools ELF section 78 | - If you're writting a new runtime, I recommend you implement appbundle-runtime.go, cli.go and noEmbed.go. This edition of the runtime is the most portable and flexible. It is simplifies a lot the build process. 79 | -------------------------------------------------------------------------------- /docs/tooling.md: -------------------------------------------------------------------------------- 1 | # pelf 2 | 3 | The `pelf` command is responsible for assembling an AppBundle by combining an ELF runtime, runtime information, static tools, and a compressed filesystem image. 4 | 5 | ### Functionality 6 | 7 | - **Purpose**: Creates an AppBundle from an AppDir, embedding necessary metadata and tools. 8 | - **Key Operations**: 9 | - Reads an AppDir, verifies that it contains an executable AppRun 10 | - Copies the runtime to the output file 11 | - Embeds runtime information (MessagePack format) in the `.pbundle_runtime_info` section of the output file 12 | - If the runtime is a universal runtime (e.g: noEmbed edition), it puts a ZSTD-compressed tar archive of static tools (depending the chosen filesystem: e.g., `dwarfs`, `squashfuse`, `unsquashfs`) in the `.pbundle_static_tools` section of the output file. 13 | - Compresses the AppDir into a DwarFS or SquashFS filesystem image and appends it to the output file 14 | - Sets the AppBundle's executable permissions and finalizes the output file. 15 | 16 | ### Command-Line Usage 17 | 18 | The pelf tool is can be invoked with the following flags: 19 | 20 | - **--add-appdir, -a **: Specifies the AppDir to package. 21 | - **--appbundle-id, -i **: Sets the unique AppBundleID for the AppBundle. 22 | - **--output-to, -o **: Specifies the output file name (e.g., app.dwfs.AppBundle). 23 | - **--compression, -c **: Specifies compression flags for the filesystem. 24 | - **--static-tools-dir **: Specifies a custom directory for static tools. 25 | - **--runtime **: Specifies the runtime binary to use. 26 | - **--upx**: Enables UPX compression for static tools. (upx must be in the host system) 27 | - **--filesystem, -j :** Selects the filesystem type (squashfs or [dwarfs]). 28 | - **--prefer-tools-in-path:** Prefers tools in `$PATH` over embedded ones. 29 | - **--list-static-tools:** Lists embedded tools with their B3SUMs. 30 | - **--disable-use-random-workdir, -d:** Disables random working directory usage. This making AppBundles leave their mountpoint open and reusing it in each launch. This is ideal for big programs that need to launch ultra-fast, such as web browsers, messaging clients, etc 31 | - **--run-behavior, -b <0|1|2|3>:** Sets runtime behavior (0: FUSE only, 1: Extract only, 2: FUSE with extract fallback, 3: FUSE with extract fallback if ≤ 350MB). 32 | - **--appimage-compat, -A:** Sets the "AI" magic-bytes, so that AppBundles are detected as AppImages by AppImage integration software like [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) 33 | - **--add-runtime-info-section :** Adds custom runtime information fields. (e.g: '.MyCustomRuntimeInfoSection:Hello') 34 | - **--add-elf-section :** Adds a custom ELF section from a .elfS file., where the filename of the .elfS file minus the extension is the section name, and the file contents are the data 35 | - **--add-updinfo :** Adds an upd_info ELF section with the given string. 36 | 37 | # pelfCreator 38 | 39 | The `pelfCreator` command is a higher-level utility that prepares an AppDir and invokes `pelf` to create an AppBundle. It supports multiple modes for different use cases. 40 | 41 | ### Functionality 42 | 43 | - **Purpose**: Creates an AppDir, populates it with a root filesystem, application files, and dependencies, and then packages it into an AppBundle. 44 | - **Key Operations**: 45 | - Sets up a temporary directory for processing. 46 | - Downloads or uses a local root filesystem (e.g., Alpine or ArchLinux). 47 | - Installs specified packages using `apk` (Alpine) or `pacman` (ArchLinux). 48 | - Configures the AppRun script and entrypoint. 49 | - Optionally processes binaries with `lib4bin` for `sharun` mode. 50 | - Trims the filesystem based on `--keep` or `--getrid` flags. 51 | - Calls `pelf` to finalize the AppBundle. 52 | 53 | ### Command-Line Usage 54 | 55 | The `pelfCreator` tool is invoked with the following flags: 56 | 57 | - **`--maintainer `**: Specifies the maintainer's name (required). 58 | - **`--name `**: Sets the application name (required). 59 | - **`--appbundle-id `**: Sets the `AppBundleID` (optional; defaults to `--`). 60 | - **`--pkg-add `**: Specifies packages to install in the root filesystem (required). 61 | - **`--entrypoint `**: Sets the entrypoint command or desktop file (required unless using `--multicall`). 62 | - **`--keep `**: Specifies files to keep in the `proto` directory. 63 | - **`--getrid `**: Specifies files to remove from the `proto` directory. 64 | - **`--filesystem `**: Selects the filesystem type (`dwfs` or `squashfs`; default: `dwfs`). 65 | - **`--output-to `**: Specifies the output AppBundle file (optional; defaults to `..AppBundle`). 66 | - **`--local `**: Specifies a directory or archive containing resources (e.g., `rootfs.tar`, `AppRun`, `bwrap`). 67 | - **`--preserve-rootfs-permissions`**: Preserves original filesystem permissions. 68 | - **`--dontpack`**: Stops short of packaging the AppDir into an AppBundle, leaving only the AppDir. 69 | - **`--sharun `**: Processes specified binaries with `lib4bin` and uses `AppRun.sharun` or `AppRun.sharun.ovfsProto`. 70 | - **`--sandbox`**: Enables sandbox mode using `AppRun.rootfs-based` with `bwrap`. 71 | 72 | ### Modes of Operation 73 | 74 | 1. **Sandbox Mode** (`--sandbox`): 75 | - Retains and binds the `proto` directory as the root filesystem, with host directory bindings (e.g., `/home`, `/tmp`, `/etc`). 76 | - Supports trimming of the `proto` directory using `--keep` or `--getrid` flags to reduce size. 77 | - Uses `AppRun.rootfs-based` to run the application in a `bwrap` sandbox. 78 | - Can be customized via env vars such as `SHARE_LOOK`, `SHARE_FONTS`, `SHARE_AUDIO`, and `UID0_GID0` for fine-grained control over sandboxing. 79 | - Suitable for applications requiring strict isolation from the host system. Or those that refuse to work with the default mode (hybrid) 80 | 81 | 2. **Sharun Mode** (`--sharun `): 82 | - Processes specified binaries with `lib4bin` to ensure compatibility and portability. 83 | - Uses `AppRun.sharun` (if `proto` is removed) or `AppRun.sharun.ovfsProto` (if `proto` is retained). 84 | - When using `AppRun.sharun.ovfsProto`, employs `unionfs-fuse` to create a copy-on-write overlay of the `proto` directory. 85 | - Sets `LD_LIBRARY_PATH` to include library paths from the AppDir, ensuring binaries can find their dependencies. 86 | - Ideal for lightweight applications or when minimizing filesystem size is a priority. 87 | 88 | 3. **Default Mode** (can be combined with `--sharun`, to ship lightweight AppBundles that include a default config file, etc, but otherwise use the system's files unless they're missing): 89 | - Retains the `proto` directory 90 | - Supports trimming of the `proto` directory using `--keep` or `--getrid` flags to reduce size. 91 | - Uses `AppRun.sharun.ovfsProto` to execute the application with a `unionfs-fuse` overlay of the user's `/` & the AppDir's `proto`. 92 | - Suitable for most applications, as it allows the AppBundle to use files from the system if they don't exist in the AppDir's `proto` and vice-versa 93 | 94 | ## Notes 95 | 96 | - The `pelfCreator` tool supports extensibility through custom root filesystems and package managers via the `--local` flag. 97 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pelf 2 | 3 | go 1.24 4 | 5 | require ( 6 | fyne.io/fyne/v2 v2.5.5 7 | github.com/adrg/xdg v0.5.3 8 | github.com/emmansun/base64 v0.7.0 9 | github.com/fxamacker/cbor/v2 v2.8.0 10 | github.com/goccy/go-json v0.10.5 11 | github.com/joho/godotenv v1.5.1 12 | github.com/klauspost/compress v1.18.0 13 | github.com/liamg/memit v0.0.3 14 | github.com/liamg/tml v0.7.0 15 | github.com/mholt/archives v0.1.1 16 | github.com/minio/md5-simd v1.1.2 17 | github.com/pkg/xattr v0.4.10 18 | github.com/shamaton/msgpack/v2 v2.2.3 19 | github.com/shirou/gopsutil/v4 v4.25.4 20 | github.com/u-root/u-root v0.14.0 21 | github.com/urfave/cli/v3 v3.3.3 22 | github.com/zeebo/blake3 v0.2.4 23 | golang.org/x/image v0.25.0 24 | golang.org/x/sys v0.33.0 25 | gopkg.in/yaml.v3 v3.0.1 26 | lukechampine.com/blake3 v1.4.1 27 | pgregory.net/rand v1.0.2 28 | ) 29 | 30 | require ( 31 | fyne.io/systray v1.11.0 // indirect 32 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 33 | github.com/STARRY-S/zip v0.2.3 // indirect 34 | github.com/andybalholm/brotli v1.1.1 // indirect 35 | github.com/bodgit/plumbing v1.3.0 // indirect 36 | github.com/bodgit/sevenzip v1.6.0 // indirect 37 | github.com/bodgit/windows v1.0.1 // indirect 38 | github.com/davecgh/go-spew v1.1.1 // indirect 39 | github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect 40 | github.com/ebitengine/purego v0.8.4 // indirect 41 | github.com/fredbi/uri v1.1.0 // indirect 42 | github.com/fsnotify/fsnotify v1.7.0 // indirect 43 | github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect 44 | github.com/fyne-io/glfw-js v0.0.0-20241126112943-313d8a0fe1d0 // indirect 45 | github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect 46 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect 47 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect 48 | github.com/go-ole/go-ole v1.3.0 // indirect 49 | github.com/go-text/render v0.2.0 // indirect 50 | github.com/go-text/typesetting v0.2.0 // indirect 51 | github.com/godbus/dbus/v5 v5.1.0 // indirect 52 | github.com/gopherjs/gopherjs v1.17.2 // indirect 53 | github.com/hashicorp/errwrap v1.1.0 // indirect 54 | github.com/hashicorp/go-multierror v1.1.1 // indirect 55 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 56 | github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect 57 | github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect 58 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 59 | github.com/klauspost/pgzip v1.2.6 // indirect 60 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 61 | github.com/minio/minlz v1.0.0 // indirect 62 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 63 | github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect 64 | github.com/nwaples/rardecode/v2 v2.1.1 // indirect 65 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 66 | github.com/pmezard/go-difflib v1.0.0 // indirect 67 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 68 | github.com/rymdport/portal v0.3.0 // indirect 69 | github.com/sorairolake/lzip-go v0.3.7 // indirect 70 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect 71 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect 72 | github.com/stretchr/testify v1.10.0 // indirect 73 | github.com/therootcompany/xz v1.0.1 // indirect 74 | github.com/tklauser/go-sysconf v0.3.15 // indirect 75 | github.com/tklauser/numcpus v0.10.0 // indirect 76 | github.com/ulikunitz/xz v0.5.12 // indirect 77 | github.com/x448/float16 v0.8.4 // indirect 78 | github.com/yuin/goldmark v1.7.1 // indirect 79 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 80 | go4.org v0.0.0-20230225012048-214862532bf5 // indirect 81 | golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect 82 | golang.org/x/net v0.40.0 // indirect 83 | golang.org/x/text v0.25.0 // indirect 84 | ) 85 | -------------------------------------------------------------------------------- /pelf_linker: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Initialize variables 4 | PELF_BINDIRS="" 5 | PELF_LIBDIRS="" 6 | 7 | # Function to concatenate existing directories from *_binDir environment variables into PELF_BINDIRS 8 | concatenate_bindirs() { 9 | # Find all environment variables ending with _binDir 10 | vars="$(env | grep ".*_binDir=" | cut -f 1 -d '=')" 11 | for v in $vars; do 12 | # Get the value of the variable 13 | eval "vval=\$$v" 14 | 15 | # Save the current IFS and change it to handle colon-separated paths 16 | old_ifs="$IFS" 17 | IFS=":" 18 | 19 | # Loop through each path in the variable 20 | for dir in $vval; do 21 | # Check if the directory exists 22 | if [ -d "$dir" ]; then 23 | # Append to PELF_BINDIRS if the directory exists 24 | if [ -z "$PELF_BINDIRS" ]; then 25 | PELF_BINDIRS="$dir" 26 | else 27 | PELF_BINDIRS="$PELF_BINDIRS:$dir" 28 | fi 29 | fi 30 | done 31 | 32 | # Restore the original IFS 33 | IFS="$old_ifs" 34 | done 35 | 36 | # Print the concatenated PELF_BINDIRS 37 | if [ -z "$1" ]; then 38 | echo "PELF_BINDIRS=\"$PELF_BINDIRS\"" 39 | fi 40 | } 41 | 42 | # Function to concatenate existing directories from *_libDir environment variables into PELF_LIBDIRS 43 | concatenate_libdirs() { 44 | # Find all environment variables ending with _libDir 45 | vars="$(env | grep ".*_libDir=" | cut -f 1 -d '=')" 46 | for v in $vars; do 47 | # Get the value of the variable 48 | eval "vval=\$$v" 49 | 50 | # Save the current IFS and change it to handle colon-separated paths 51 | old_ifs="$IFS" 52 | IFS=":" 53 | 54 | # Loop through each path in the variable 55 | for dir in $vval; do 56 | # Check if the directory exists 57 | if [ -d "$dir" ]; then 58 | # Append to PELF_LIBDIRS if the directory exists 59 | if [ -z "$PELF_LIBDIRS" ]; then 60 | PELF_LIBDIRS="$dir" 61 | else 62 | PELF_LIBDIRS="$PELF_LIBDIRS:$dir" 63 | fi 64 | fi 65 | done 66 | 67 | # Restore the original IFS 68 | IFS="$old_ifs" 69 | done 70 | 71 | # Print the concatenated PELF_LIBDIRS 72 | if [ -z "$1" ]; then 73 | echo "PELF_LIBDIRS=\"$PELF_LIBDIRS\"" 74 | fi 75 | } 76 | 77 | # Call the functions 78 | concatenate_bindirs "$1" 79 | concatenate_libdirs "$1" 80 | 81 | if [ "$1" = "--export" ]; then 82 | export PELF_LIBDIRS="$PELF_LIBDIRS" 83 | export PELF_BINDIRS="$PELF_BINDIRS" 84 | else 85 | LD_LIBRARY_PATH="$PELF_LIBDIRS" PATH="$PATH:$PELF_BINDIRS" "$@" 86 | fi 87 | -------------------------------------------------------------------------------- /www/archetypes/default.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '{{ .Date }}' 3 | draft = true 4 | title = '{{ replace .File.ContentBaseName "-" " " | title }}' 5 | +++ 6 | -------------------------------------------------------------------------------- /www/config.toml: -------------------------------------------------------------------------------- 1 | title = "AppBundle Documentation & Implementation Details" 2 | baseURL = "https://pelf.xplshn.com.ar/" 3 | languageCode = "en-us" 4 | theme = "werx" 5 | publishDir = "pub" 6 | enableRobotsTXT = true 7 | 8 | ignoreFiles = ["\\.Rmd$", "_files$", "_cache$"] 9 | preserveTaxonomyNames = true 10 | enableEmoji = true 11 | footnotereturnlinkcontents = "↩" 12 | 13 | [module] 14 | [[module.mounts]] 15 | source = 'assets' 16 | target = 'assets' 17 | [[module.mounts]] 18 | source = 'static' 19 | target = 'assets' 20 | 21 | [permalinks] 22 | post = "/post/:year/:month/:day/:slug/" 23 | 24 | [[menu.main]] 25 | name = "Home" 26 | url = "/" 27 | weight = 1 28 | #[[menu.main]] 29 | # name = "Categories" 30 | # url = "/categories/" 31 | # weight = 2 32 | #[[menu.main]] 33 | # name = "Tags" 34 | # url = "/tags/" 35 | # weight = 3 36 | [[menu.feed]] 37 | name = "Subscribe" 38 | url = "/index.xml" 39 | weight = 100 40 | [[menu.feed]] 41 | name = "neoblog" 42 | url = "https://fatbuffalo.neocities.org/def" 43 | weight = 90 44 | [[menu.feed]] 45 | name = "dbin" 46 | url = "https://github.com/xplshn/dbin" 47 | weight = 80 48 | [[menu.feed]] 49 | name = "harmful.cat-v.org" 50 | url = "https://harmful.cat-v.org" 51 | weight = 70 52 | [[menu.feed]] 53 | name = "nosystemd.org" 54 | url = "https://nosystemd.org" 55 | weight = 60 56 | [[menu.feed]] 57 | name = "suckless.org" 58 | url = "https://suckless.org" 59 | weight = 50 60 | [[menu.feed]] 61 | name = "copacabana.pindorama.net.br" 62 | url = "https://copacabana.pindorama.net.br" 63 | weight = 40 64 | [[menu.feed]] 65 | name = "shithub.us" 66 | url = "https://shithub.us" 67 | weight = 30 68 | [[menu.feed]] 69 | name = "managainstthestate.blogspot.com" 70 | url = "https://web.archive.org/web/20231123031907/https://managainstthestate.blogspot.com/2011/08/anarcho-capitalist-resources-by-wesker.html" 71 | weight = 20 72 | [[menu.feed]] 73 | name = "musl.libc.org" 74 | url = "https://musl.libc.org" 75 | weight = 10 76 | 77 | [taxonomies] 78 | category = "categories" 79 | series = "series" 80 | tag = "tags" 81 | 82 | [params] 83 | subtitle = "Labs" 84 | brandIconFile = "assets/images/icon.svg" 85 | abbrDateFmt = "Jan 2" 86 | dateFmt = "01.02.2006 15:04" 87 | themeVariant = "theme_blue.css" 88 | printSidebar = false 89 | 90 | #[[social]] 91 | # name = "Github" 92 | # url = "https://github.com/xplshn/alicelinux" 93 | #[[social]] 94 | # name = "Telegram" 95 | # url = "https://t.me/alicelinux" 96 | 97 | [markup.goldmark.renderer] 98 | hardWraps = false 99 | unsafe = true 100 | 101 | [markup.goldmark.renderHooks.image] 102 | enableDefault = true 103 | -------------------------------------------------------------------------------- /www/content/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Home' 3 | --- 4 | ### PELF - The AppBundle format and the AppBundle Creation Tool 5 | ###### PELF used to stand for Pack an Elf, but we slowly evolved into a much simpler yet more featureful alternative to .AppImages 6 | ###### PELF now refers to the tool used to create .AppBundles 7 | 8 | --- 9 | 10 | > .AppBundles are an executable *packaging format* designed to pack applications, toolchains, window managers, and multiple programs into a *single portable file*. 11 | 12 | AppBundles can serve as a drop-in replacement for AppImages. Both AppBundles and AppImages utilize the AppDir specification, making it easy to unpack an AppImage and re-package it as an AppBundle, gaining many features, such as faster start-up times, better compression and file de-duplication, and faster build-time. A completely customizable and flexible format. 13 | 14 | #### Advantages 15 | - **Support for multiple filesystem formats**: Support for multiple mountable filesystem formats, we currently support `squashfs` and `dwarfs`. With ongoing efforts to add a third alternative that isn't copylefted/propietary 16 | - **Simplicity**: PELF is a minimalistic Go program that makes creating portable POSIX executables a trivial task. 17 | - **Flexibility of AppBundles**: AppBundles do not force compliance with the AppDir standard. For example, you can bundle window managers and basic GUI utilities into a single file (as done with `Sway.AppBundle`). You can even package toolchains as single-file executables. 18 | - **Endless Possibilities**: With a custom AppRun script, you can create versatile `.AppBundles`. For instance, packaging a Rick Roll video with a video player that works on both glibc and musl systems is straightforward. You can even generate AppBundles that overlay on top of each other. 19 | - **Complete tooling**: The `pelfd` daemon (and its GUI version) are available for use as system integrators, they're in charge of adding the AppBundles that you put under ~/Applications in your "start menu". This is one of the many programs that are part of the tooling, another great tool is pelfCreator, which lets you create programs via simple one-liners (by default it uses an Alpine rootfs + bwrap, but you can get smaller binaries via using -x to only keep the binaries you want), a one-liner to pack Chromium into a single-file executable looks like this: `pelfCreator --maintainer "xplshn" --name "org.chromium.Chromium" --pkg-add "chromium" --entrypoint "chromium.desktop"` 20 | - **Predictable mount directories**: Our mount directories contain the AppBundle's ID, making it clear to which AppBundle the mount directory belongs 21 | - **Reliable unmount**: The AppBundle starts a background task to unmount the filesystem, and it retries 5 times, then it forces the unmount if all 5 tries failed 22 | - **Leverages many handy env variables**: Thus making .AppBundles very flexible and scriptable 23 | - **AppImage compatibility**: The --appimage-* flags are supported by our runtime, making us an actual drop-in replacement 24 | 25 | ### Usage 26 | ``` 27 | ./pelf --add-appdir "nano-14_02_2025.AppDir" --appbundle-id "nano-14_02_2025-xplshn" --output-to "nano-14_02_2025.dwfs.AppBundle" 28 | ``` 29 | OR 30 | ``` 31 | ./pelf --add-appdir "nano-14_02_2025.AppDir" --appbundle-id "nano-14_02_2025-xplshn" --output-to "nano-14_02_2025.sqfs.AppBundle" 32 | ``` 33 | 34 | ### Build ./pelf 35 | 1. Get yourself an up-to-date `go` toolchain and install `dbin` into your system or put it anywhere in your `$PATH` 36 | 2. execute `./cbuild.sh` 37 | 3. Put the resulting `./pelf` binary in your `$PATH` 38 | 4. Spread the joy of AppBundles! :) 39 | 40 | ### Usage of the Resulting `.AppBundle` 41 | > By using the `--pbundle_link` option, you can access files contained within the `./bin` or `./usr/bin` directories of an `.AppBundle`, inheriting environment variables like `PATH`. This allows multiple AppBundles to stack on top of each other, sharing libraries and binaries across "parent" bundles. 42 | 43 | #### Explanation 44 | You specify an `AppDir` to be packed and an ID for the app. This ID will be used when mounting the `.AppBundle` and should include the packing date, the project or program name, and the maintainer's information. While you can choose an arbitrary name, it’s not recommended. 45 | 46 | Additionally, we embed the tools used for mounting and unmounting the `.AppBundle`, such as `dwarfs` when using `pelf`. 47 | 48 |

49 | Screenshot showcasing a bunch of AppBundles with their icons correctly set in a thunar file manager window 50 |

51 | 52 | #### Known working distros/OSes: 53 | - Ubuntu (10.04 onwards) & derivatives, Ubuntu Touch 54 | - Alpine Linux 2.+ onwards 55 | - Void Linux Musl/Glibc 56 | - Debian/Devuan, and derivatives 57 | - Fedora 58 | - *SUSE 59 | - Maemo leste 60 | - AliceLinux 61 | - FreeBSD's Linuxlator 62 | - FreeBSD native 63 | - Chimera Linux 64 | - LFS (Linux from Scratch) 65 | - Most if not all Musl linux distributions 66 | - etc (please contribute to this list if you're a user of AppBundles) 67 | 68 | #### Resources: 69 | - [AppBundle format documentation & specifications](https://xplshn.github.io/pelf/docs) 70 | - The [AppBundleHUB](https://github.com/xplshn/AppBundleHUB) a repo which builds a ton of portable AppBundles in an automated fashion, using GH actions. (we have a [webStore](https://xplshn.github.io/AppBundleHUB) too, tho that is WIP) 71 | - [dbin](https://github.com/xplshn/dbin) a self-contained, portable, statically linked, package manager, +4000 binaries (portable, self-contained/static) are available in its repos at the time of writting. Among these, are the AppBundles from the AppBundleHUB and from pkgforge 72 | -------------------------------------------------------------------------------- /www/content/docs/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '2025-04-25T15:48:50' 3 | draft = false 4 | title = 'Documentation Index' 5 | [params.author] 6 | name = 'xplshn' 7 | email = 'xplshn@murena.io' 8 | +++ 9 | 10 |

11 | Automate once, package forever, distribute everywhere 12 |

13 | 14 |

15 | The following documents explain how an AppBundle behaves, how, and why. 16 | They also explain the reasoning behind these design choices. 17 |
18 | These documents can be a starting point for reimplementing the existing AppBundle tooling. 19 |

20 | -------------------------------------------------------------------------------- /www/content/docs/format.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '2025-05-25T00:00:29' 3 | draft = false 4 | title = 'format.md' 5 | [params.author] 6 | name = 'xplshn' 7 | email = 'xplshn@murena.io' 8 | +++ 9 | # AppBundle File Format Specification 10 | 11 | This document outlines the structure and composition of an AppBundle, a self-contained executable format designed to package applications with their dependencies for portable execution on Linux systems. 12 | 13 | ## File Structure 14 | 15 | An AppBundle is a single executable file that combines an ELF (Executable and Linkable Format) runtime with an appended filesystem image containing the application's data. The structure is as follows: 16 | 17 | 1. **ELF Runtime**: 18 | - The AppBundle begins with an ELF executable, identifiable by the magic bytes "AB" or optionally "AI" at the start of the file. 19 | - This runtime is responsible for handling the execution logic, including mounting or extracting the filesystem image and setting up the environment. 20 | 21 | 2. **Runtime Information Section (.pbundle_runtime_info)**: 22 | - The ELF file contains a section named `.pbundle_runtime_info`, which stores metadata in CBOR (Concise Binary Object Representation) format. 23 | - The structure of this section is defined in Go as: 24 | ```go 25 | type RuntimeInfo struct { 26 | AppBundleID string `json:"AppBundleID"` // Unique identifier for the AppBundle 27 | PelfVersion string `json:"PelfVersion"` // Version of the pelf tool used to create the AppBundle 28 | HostInfo string `json:"HostInfo"` // System information from `uname -mrsp(v)` 29 | FilesystemType string `json:"FilesystemType"` // Filesystem type: "dwarfs" or "squashfs" 30 | Hash string `json:"Hash"` // Hash of the filesystem image 31 | DisableRandomWorkDir bool `json:"DisableRandomWorkDir"` // Whether to use a fixed working directory 32 | MountOrExtract uint8 `json:"MountOrExtract"` // Run behavior: 0 (FUSE only), 1 (Extract only), 2 (FUSE with extract fallback), 3 (FUSE with extract fallback for files < 350MB) 33 | } 34 | ``` 35 | 36 | 3. **Static Tools Section (.pbundle_static_tools)**: 37 | - The ELF file includes a section named `.pbundle_static_tools`, containing a Zstandard (ZSTD)-compressed tar archive. 38 | - This archive holds tools necessary for mounting or extracting the filesystem image, such as `dwarfs`, `dwarfsextract`, `squashfuse`, or `unsquashfs`, depending on the filesystem type. 39 | 40 | 4. **Filesystem Image**: 41 | - Immediately following the ELF runtime, the AppBundle contains the compressed filesystem image (either DwarFS or SquashFS). 42 | - This image encapsulates the application's AppDir, including all necessary files and dependencies. 43 | 44 | ## Creation of an AppBundle 45 | 46 | An AppBundle is created using the `pelf` tool, which performs the following steps: 47 | 48 | 1. **Prepare the AppDir**: 49 | - The `pelfCreator` tool constructs an AppDir, a directory containing the application's files, including: 50 | - `AppRun`: The entrypoint script that orchestrates the execution. 51 | - `.DirIcon`: An optional icon file (PNG, in sizes 512x512, 256x256, or 128x128). 52 | - `.DirIcon.svg`: An optional SVG icon. 53 | - `program.desktop`: An optional desktop entry file. 54 | - `program.appdata.xml`: An optional AppStream metadata file. 55 | - `proto` or `rootfs`: A directory containing the application's filesystem, typically based on a minimal Linux distribution like Alpine or ArchLinux. 56 | - The AppDir may also include additional binaries and configuration files as needed. 57 | 58 | 2. **Embed Runtime Information**: 59 | - The `pelf` tool embeds the `.pbundle_runtime_info` section with metadata about the AppBundle, including its ID, filesystem type, and runtime behavior. 60 | 61 | 3. **Embed Static Tools**: 62 | - Tools required for mounting or extracting the filesystem (e.g., `dwarfs`, `squashfuse`) are compressed into a ZSTD tar archive and embedded in the `.pbundle_static_tools` section. 63 | 64 | 4. **Append Filesystem Image**: 65 | - The AppDir is compressed into a DwarFS or SquashFS image, depending on the configuration, and appended to the ELF runtime. 66 | - The offset of the filesystem image is recorded in the runtime configuration for access during execution. 67 | 68 | 5. **Finalize the Executable**: 69 | - The `pelf` tool combines the ELF runtime, runtime information, static tools, and filesystem image into a single executable file with the `.AppBundle` extension. 70 | 71 | ## Run Behaviors 72 | 73 | The `MountOrExtract` field in the `.pbundle_runtime_info` section determines how the AppBundle behaves when executed: 74 | 75 | - **0 (FUSE Mounting Only)**: The AppBundle uses FUSE to mount the filesystem image. If FUSE is unavailable, it fails without falling back to extraction. 76 | - **1 (Extract and Run)**: The AppBundle extracts the filesystem image to a temporary directory (typically in `tmpfs`) and executes from there, ignoring FUSE even if available. 77 | - **2 (FUSE with Fallback)**: The AppBundle attempts to use FUSE to mount the filesystem. If FUSE is unavailable, it falls back to extracting the filesystem to `tmpfs`. 78 | - **3 (FUSE with Conditional Fallback)**: Similar to option 2, but fallback to extraction only occurs if the AppBundle file is smaller than 350MB. 79 | 80 | ## Expected Contents of the Filesystem Image 81 | 82 | The filesystem image within the AppBundle is expected to be an AppDir with at least the following: 83 | 84 | - **AppRun**: A shell script that serves as the entrypoint for the application. It sets up the environment and executes the main program. 85 | - **Optional Files**: 86 | - `.DirIcon`: A PNG icon in a standard size (512x512, 256x256, or 128x128). 87 | - `.DirIcon.svg`: An SVG icon 88 | - `program.desktop`: A desktop entry file for integration with desktop environments. 89 | - `program.appdata.xml`: An AppStream metadata file for application metadata. 90 | - `proto` or `rootfs`: A directory containing the application's filesystem, including binaries, libraries, and configuration files. 91 | 92 | ## Notes 93 | 94 | - The AppBundle format is designed to be self-contained, requiring no external dependencies for execution in most cases, assuming the necessary tools are embedded or available on the host system. 95 | - The choice of filesystem (DwarFS or SquashFS) affects the tools included in the `.pbundle_static_tools` section and the runtime behavior. 96 | -------------------------------------------------------------------------------- /www/content/docs/runtime.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '2025-05-24T23:55:33' 3 | draft = false 4 | title = 'runtime.md' 5 | [params.author] 6 | name = 'xplshn' 7 | email = 'xplshn@murena.io' 8 | +++ 9 | # AppBundle Runtime Execution 10 | 11 | This document describes how the AppBundle runtime operates, including how it reads its own information, extracts static tools, determines environment variables, and handles runtime flags. 12 | 13 | ## Execution Flow 14 | 15 | When an AppBundle is executed, the runtime performs the following steps: 16 | 17 | 1. **Read Runtime Information**: 18 | - The runtime reads the `.pbundle_runtime_info` section from the ELF file, which contains CBOR-encoded metadata. 19 | - This section includes: 20 | - `AppBundleID`: A unique identifier for the AppBundle. (e.g: "com.brave.Browser-xplshn-2025-05-19". You're not forced to follow this format, but if you do, you can create a [dbin](https://github.com/xplshn/dbin) repository that countains your AppBundle by using our [appstream-helper](https://github.com/xplshn/pelf/blob/master/cmd/misc/appstream-helper/appstream-helper.go) tool. `$NAME-$MAINTAINER-$DATE` or preferably: `$APPSTREAM_ID-$MAINTAINER-$DATE`, so that you don't have to include an AppStream file within the AppDir for appstream-helper to get metadata from it) 21 | - `PelfVersion`: The version of the `pelf` tool used to create the AppBundle. 22 | - `HostInfo`: System information from `uname -mrsp(v)` of the build machine. 23 | - `FilesystemType`: Either "dwarfs" or "squashfs". 24 | - `Hash`: A hash of the filesystem image for integrity verification. 25 | - `DisableRandomWorkDir`: A boolean indicating whether to use a fixed working directory. 26 | - `MountOrExtract`: A uint8 value (0–3) specifying the run behavior (see below). 27 | - The runtime uses this information to configure its behavior and locate the filesystem image. 28 | 29 | 2. **Extract Static Tools**: 30 | - The runtime accesses the static tools required for mounting or extracting the filesystem (e.g., `dwarfs`, `dwarfsextract`, `squashfuse`, `unsquashfs`). 31 | - The handling of static tools depends on the build mode: 32 | - **noEmbed Edition**: The tools are embedded in the `.pbundle_static_tools` ELF section as a ZSTD-compressed tar archive. The runtime determines the filesystem mounting and extraction commands at runtime, extracts the needed files from this archive to a temporary directory (`cfg.staticToolsDir`), and uses them to either mount or extract the filesystem. 33 | - **Embed Edition**: The tools are embedded directly in the binary using Go’s `embed` package, without compression. The runtime accesses these tools directly from the embedded filesystem, without needing to extract a compressed archive. 34 | 35 | 3. **Exported Env Variables**: 36 | - The runtime sets up several environment variables to facilitate execution: 37 | - **HOME**: If a portable home directory (`.AppBundleID.home`) exists in the same directory as the AppBundle, it is used as `$HOME`. 38 | - **XDG_DATA_HOME**: If a portable share directory (`.AppBundleID.share`) exists, it is used as `$XDG_DATA_HOME`. 39 | - **XDG_CONFIG_HOME**: If a portable config directory (`.AppBundleID.config`) exists, it is used as `$XDG_CONFIG_HOME`. 40 | - **APPDIR**: Set to the mount or extraction directory 41 | - **SELF**: The absolute path to the AppBundle executable. 42 | - **ARGV0**: The basename of `$SELF` 43 | - **PATH**: Augmented to include the AppBundle's `bin` directory and the directory containing the static tools. 44 | 45 | 4. **Mount or Extract Filesystem**: 46 | - The runtime decides whether to mount or extract the filesystem image based on the `MountOrExtract` value: 47 | - **0**: Mounts the filesystem using FUSE (e.g., `dwarfs` or `squashfuse`) and fails if FUSE is unavailable. 48 | - **1**: Extracts the filesystem to a temporary directory (usually in `tmpfs`) and runs from there. 49 | - **2**: Attempts to mount with FUSE; falls back to extraction if FUSE is unavailable. 50 | - **3**: Similar to 2, but only falls back to extraction if the AppBundle is smaller than 350MB. 51 | 52 | 5. **Execute the Application**: 53 | - The runtime executes the `AppRun` script within the AppDir. 54 | - If a specific command is provided via `--pbundle_link`, the runtime executes that command within the AppBundle's environment, instead of executing the AppRun. 55 | 56 | ## Runtime Flags 57 | 58 | The AppBundle runtime supports several command-line flags to modify its behavior: 59 | 60 | - **`--pbundle_help`**: Displays help information, including the `PelfVersion`, `HostInfo`, and internal configuration variables (e.g., `cfg.exeName`, `cfg.mountDir`). 61 | - **`--pbundle_list`**: Lists the contents of the AppBundle's filesystem, including static tools. 62 | - **`--pbundle_link `**: Executes a specified command within the AppBundle's environment, leveraging its `PATH` and other variables. 63 | - **`--pbundle_pngIcon`**: Outputs the base64-encoded `.DirIcon` (PNG) if it exists; otherwise, exits with error code 1. 64 | - **`--pbundle_svgIcon`**: Outputs the base64-encoded `.DirIcon.svg` if it exists; otherwise, exits with error code 1. 65 | - **`--pbundle_appstream`**: Outputs the base64-encoded first `.xml` file (AppStream metadata) found in the AppDir. 66 | - **`--pbundle_desktop`**: Outputs the base64-encoded first `.desktop` file found in the AppDir. 67 | - **`--pbundle_portableHome`**: Creates a portable home directory (`.AppBundleID.home`) in the same directory as the AppBundle. 68 | - **`--pbundle_portableConfig`**: Creates a portable config directory (`.AppBundleID.config`) in the same directory as the AppBundle. 69 | - **`--pbundle_cleanup`**: Unmounts and removes the AppBundle's working directory and mount point, affecting only instances of the same AppBundle. 70 | - **`--pbundle_mount`**: Mounts the filesystem to a specified or default directory and keeps the mount active. 71 | - **`--pbundle_extract [globs]`**: Extracts the filesystem to a directory (default: `_` or `squashfs-root` for AppImage compatibility). Supports selective extraction with glob patterns. 72 | - **`--pbundle_extract_and_run`**: Extracts the filesystem and immediately executes the entrypoint. 73 | - **`--pbundle_offset`**: Outputs the offset of the filesystem image within the AppBundle. 74 | - **AppImage Compatibility Flags**: 75 | - `--appimage-extract`: Same as `--pbundle_extract`, but uses `squashfs-root` as the output directory. 76 | - `--appimage-extract-and-run`: Same as `--pbundle_extract_and_run`. 77 | - `--appimage-mount`: Same as `--pbundle_mount`. 78 | - `--appimage-offset`: Same as `--pbundle_offset`. 79 | 80 | ## Notes 81 | 82 | - The choice between `noEmbed` and embed modes affects how static tools are stored and accessed. The `noEmbed` mode uses a compressed archive for flexibility, while the embed mode simplifies access by avoiding compression. 83 | - The `AppRun` script (e.g., `AppRun.rootfs-based`, `AppRun.sharun`, or `AppRun.sharun.ovfsProto`) determines sandboxing and execution behavior, such as using `bwrap` or `unionfs-fuse`. 84 | - The runtime ensures cleanup of temporary directories unless `--pbundle_cleanup` is explicitly called or `noCleanup` is set. 85 | - The `noEmbed` build tag for the `appbundle-runtime` allows you to build a single appbundle-runtime binary, that determines which filesystem to use at runtime, after having read its .pbundle_runtime_info and decompressed the .tar.zst data within the .pbundle_static_tools ELF section 86 | - If you're writting a new runtime, I recommend you implement appbundle-runtime.go, cli.go and noEmbed.go. This edition of the runtime is the most portable and flexible. It is simplifies a lot the build process. 87 | -------------------------------------------------------------------------------- /www/content/docs/tooling.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '2025-05-24T23:55:33' 3 | draft = false 4 | title = 'tooling.md' 5 | [params.author] 6 | name = 'xplshn' 7 | email = 'xplshn@murena.io' 8 | +++ 9 | # pelf 10 | 11 | The `pelf` command is responsible for assembling an AppBundle by combining an ELF runtime, runtime information, static tools, and a compressed filesystem image. 12 | 13 | ### Functionality 14 | 15 | - **Purpose**: Creates an AppBundle from an AppDir, embedding necessary metadata and tools. 16 | - **Key Operations**: 17 | - Reads an AppDir, verifies that it contains an executable AppRun 18 | - Copies the runtime to the output file 19 | - Embeds runtime information (MessagePack format) in the `.pbundle_runtime_info` section of the output file 20 | - If the runtime is a universal runtime (e.g: noEmbed edition), it puts a ZSTD-compressed tar archive of static tools (depending the chosen filesystem: e.g., `dwarfs`, `squashfuse`, `unsquashfs`) in the `.pbundle_static_tools` section of the output file. 21 | - Compresses the AppDir into a DwarFS or SquashFS filesystem image and appends it to the output file 22 | - Sets the AppBundle's executable permissions and finalizes the output file. 23 | 24 | ### Command-Line Usage 25 | 26 | The pelf tool is can be invoked with the following flags: 27 | 28 | - **--add-appdir, -a **: Specifies the AppDir to package. 29 | - **--appbundle-id, -i **: Sets the unique AppBundleID for the AppBundle. 30 | - **--output-to, -o **: Specifies the output file name (e.g., app.dwfs.AppBundle). 31 | - **--compression, -c **: Specifies compression flags for the filesystem. 32 | - **--static-tools-dir **: Specifies a custom directory for static tools. 33 | - **--runtime **: Specifies the runtime binary to use. 34 | - **--upx**: Enables UPX compression for static tools. (upx must be in the host system) 35 | - **--filesystem, -j :** Selects the filesystem type (squashfs or [dwarfs]). 36 | - **--prefer-tools-in-path:** Prefers tools in `$PATH` over embedded ones. 37 | - **--list-static-tools:** Lists embedded tools with their B3SUMs. 38 | - **--disable-use-random-workdir, -d:** Disables random working directory usage. This making AppBundles leave their mountpoint open and reusing it in each launch. This is ideal for big programs that need to launch ultra-fast, such as web browsers, messaging clients, etc 39 | - **--run-behavior, -b <0|1|2|3>:** Sets runtime behavior (0: FUSE only, 1: Extract only, 2: FUSE with extract fallback, 3: FUSE with extract fallback if ≤ 350MB). 40 | - **--appimage-compat, -A:** Sets the "AI" magic-bytes, so that AppBundles are detected as AppImages by AppImage integration software like [AppImageUpdate](https://github.com/AppImageCommunity/AppImageUpdate) 41 | - **--add-runtime-info-section :** Adds custom runtime information fields. (e.g: '.MyCustomRuntimeInfoSection:Hello') 42 | - **--add-elf-section :** Adds a custom ELF section from a .elfS file., where the filename of the .elfS file minus the extension is the section name, and the file contents are the data 43 | - **--add-updinfo :** Adds an upd_info ELF section with the given string. 44 | 45 | # pelfCreator 46 | 47 | The `pelfCreator` command is a higher-level utility that prepares an AppDir and invokes `pelf` to create an AppBundle. It supports multiple modes for different use cases. 48 | 49 | ### Functionality 50 | 51 | - **Purpose**: Creates an AppDir, populates it with a root filesystem, application files, and dependencies, and then packages it into an AppBundle. 52 | - **Key Operations**: 53 | - Sets up a temporary directory for processing. 54 | - Downloads or uses a local root filesystem (e.g., Alpine or ArchLinux). 55 | - Installs specified packages using `apk` (Alpine) or `pacman` (ArchLinux). 56 | - Configures the AppRun script and entrypoint. 57 | - Optionally processes binaries with `lib4bin` for `sharun` mode. 58 | - Trims the filesystem based on `--keep` or `--getrid` flags. 59 | - Calls `pelf` to finalize the AppBundle. 60 | 61 | ### Command-Line Usage 62 | 63 | The `pelfCreator` tool is invoked with the following flags: 64 | 65 | - **`--maintainer `**: Specifies the maintainer's name (required). 66 | - **`--name `**: Sets the application name (required). 67 | - **`--appbundle-id `**: Sets the `AppBundleID` (optional; defaults to `--`). 68 | - **`--pkg-add `**: Specifies packages to install in the root filesystem (required). 69 | - **`--entrypoint `**: Sets the entrypoint command or desktop file (required unless using `--multicall`). 70 | - **`--keep `**: Specifies files to keep in the `proto` directory. 71 | - **`--getrid `**: Specifies files to remove from the `proto` directory. 72 | - **`--filesystem `**: Selects the filesystem type (`dwfs` or `squashfs`; default: `dwfs`). 73 | - **`--output-to `**: Specifies the output AppBundle file (optional; defaults to `..AppBundle`). 74 | - **`--local `**: Specifies a directory or archive containing resources (e.g., `rootfs.tar`, `AppRun`, `bwrap`). 75 | - **`--preserve-rootfs-permissions`**: Preserves original filesystem permissions. 76 | - **`--dontpack`**: Stops short of packaging the AppDir into an AppBundle, leaving only the AppDir. 77 | - **`--sharun `**: Processes specified binaries with `lib4bin` and uses `AppRun.sharun` or `AppRun.sharun.ovfsProto`. 78 | - **`--sandbox`**: Enables sandbox mode using `AppRun.rootfs-based` with `bwrap`. 79 | 80 | ### Modes of Operation 81 | 82 | 1. **Sandbox Mode** (`--sandbox`): 83 | - Retains and binds the `proto` directory as the root filesystem, with host directory bindings (e.g., `/home`, `/tmp`, `/etc`). 84 | - Supports trimming of the `proto` directory using `--keep` or `--getrid` flags to reduce size. 85 | - Uses `AppRun.rootfs-based` to run the application in a `bwrap` sandbox. 86 | - Can be customized via env vars such as `SHARE_LOOK`, `SHARE_FONTS`, `SHARE_AUDIO`, and `UID0_GID0` for fine-grained control over sandboxing. 87 | - Suitable for applications requiring strict isolation from the host system. Or those that refuse to work with the default mode (hybrid) 88 | 89 | 2. **Sharun Mode** (`--sharun `): 90 | - Processes specified binaries with `lib4bin` to ensure compatibility and portability. 91 | - Uses `AppRun.sharun` (if `proto` is removed) or `AppRun.sharun.ovfsProto` (if `proto` is retained). 92 | - When using `AppRun.sharun.ovfsProto`, employs `unionfs-fuse` to create a copy-on-write overlay of the `proto` directory. 93 | - Sets `LD_LIBRARY_PATH` to include library paths from the AppDir, ensuring binaries can find their dependencies. 94 | - Ideal for lightweight applications or when minimizing filesystem size is a priority. 95 | 96 | 3. **Default Mode** (can be combined with `--sharun`, to ship lightweight AppBundles that include a default config file, etc, but otherwise use the system's files unless they're missing): 97 | - Retains the `proto` directory 98 | - Supports trimming of the `proto` directory using `--keep` or `--getrid` flags to reduce size. 99 | - Uses `AppRun.sharun.ovfsProto` to execute the application with a `unionfs-fuse` overlay of the user's `/` & the AppDir's `proto`. 100 | - Suitable for most applications, as it allows the AppBundle to use files from the system if they don't exist in the AppDir's `proto` and vice-versa 101 | 102 | ## Notes 103 | 104 | - The `pelfCreator` tool supports extensibility through custom root filesystems and package managers via the `--local` flag. 105 | -------------------------------------------------------------------------------- /www/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | check_directory() { 4 | if [ ! "$(basename "$PWD")" = "www" ] || [ ! -f "$PWD/config.toml" ]; then 5 | if [ -d "$PWD/www" ]; then 6 | echo "You must enter ./www" 7 | else 8 | echo "Where the fuck are we? You must enter https://github.com/xplshn/alicelinux/www, run me within of the ./www directory!" 9 | fi 10 | exit 1 11 | fi 12 | } 13 | 14 | process_markdown_files() { 15 | mkdir -p "$2" 16 | for FILE in "$1"/*.md; do 17 | if [ "$FILE" = "_index.md" ]; then 18 | echo "Skipping \"$FILE\"" 19 | fi 20 | FILENAME="$(basename "$FILE")" 21 | DATE="$(git log -1 --format="%ai" -- "$FILE" | awk '{print $1 "T" $2}')" 22 | TITLE="$(basename "$FILE")" 23 | AUTHOR_NAME="$(git log --follow --format="%an" -- "$FILE" | tail -n 1)" 24 | AUTHOR_EMAIL="$(git log --follow --format="%ae" -- "$FILE" | tail -n 1)" 25 | 26 | case "$TITLE" in 27 | "_index.md") 28 | cp "$FILE" "./content/docs" 29 | continue 30 | ;; 31 | "index.md") 32 | echo "Skipping \"$FILE\"" 33 | continue 34 | ;; 35 | esac 36 | 37 | { 38 | echo "+++" 39 | echo "date = '$DATE'" 40 | echo "draft = false" 41 | echo "title = '$TITLE'" 42 | echo "[params.author]" 43 | echo " name = '$AUTHOR_NAME'" 44 | echo " email = '$AUTHOR_EMAIL'" 45 | echo "+++" 46 | cat "$FILE" 47 | } >"$2/$FILENAME" 48 | done 49 | 50 | # Disabled because I manually created the ./content/docs/_index.md 51 | #if [ "$(find "$2" -maxdepth 1 -type f | wc -l)" -gt 0 ]; then 52 | # { 53 | # echo "---" 54 | # echo "title: '$3'" 55 | # echo "---" 56 | # } >"$2/_index.md" 57 | #fi 58 | } 59 | 60 | # Main script execution 61 | check_directory 62 | rm -rf -- ./content/docs/* 63 | rm -rf -- ./static/assets/* 64 | process_markdown_files "../docs" "./content/docs" "Documentation" 65 | find ../assets/ -type f ! -name '*AppRun*' ! -name '*LAUNCH*' -exec cp {} ./static/assets/ \; 66 | { 67 | echo "---" 68 | echo "title: 'Home'" 69 | echo "---" 70 | } >./content/_index.md 71 | sed 's|src="files/|src="assets/|g' ../README.md >>./content/_index.md 72 | 73 | # Build with Hugo 74 | hugo 75 | -------------------------------------------------------------------------------- /www/static/assets/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------