├── assets ├── screenshot.png ├── AppRun.override ├── AppRun.goToolchain ├── AppRun.multiBinary ├── AppRun.sharun ├── LAUNCH-multicall.rootfs.entrypoint ├── AppRun.rootfs-based.stable ├── AppRun.generic ├── AppRun.gccToolchain ├── AppRun.sharun.ovfsProto ├── pin.svg └── AppRun.rootfs-based ├── .gitmodules ├── www ├── archetypes │ └── default.md ├── content │ ├── docs │ │ ├── _index.md │ │ ├── format.md │ │ ├── tooling.md │ │ └── runtime.md │ └── _index.md ├── gen.sh ├── config.toml └── static │ └── assets │ └── pin.svg ├── .gitattributes ├── cmd ├── dynexec │ ├── README.md │ ├── dynexec.go │ ├── C │ │ └── dynexec.c │ └── lib4bin │ │ └── lib4bin.go ├── misc │ ├── getlibs │ ├── thumbgen │ ├── rootfs2sharun │ └── BS2AppBundle ├── pfusermount │ ├── fusermount.go │ ├── fusermount3.go │ └── cbuild.sh ├── pelfd │ ├── appbundle_support.go │ ├── appimage_support.go │ ├── util.go │ └── pelfd.go └── pelfd-gui.deprecated │ └── util.go ├── .gitignore ├── docs ├── _index.md ├── appBundleID.md ├── format.md ├── tooling.md └── runtime.md ├── LICENSE ├── appbundle-runtime ├── embed_squashfs.go ├── embed.go ├── embed_dwarfs.go ├── cli.go └── noEmbed.go ├── .github └── workflows │ ├── static.yml │ └── buildTooling.yml ├── pelf_linker ├── go.mod └── README.md /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xplshn/pelf/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "www/themes/werx"] 2 | path = www/themes/werx 3 | url = https://github.com/xplshn/werx 4 | -------------------------------------------------------------------------------- /www/archetypes/default.md: -------------------------------------------------------------------------------- 1 | +++ 2 | date = '{{ .Date }}' 3 | draft = true 4 | title = '{{ replace .File.ContentBaseName "-" " " | title }}' 5 | +++ 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | **/*.b linguist-language=C linguist-display-name="B" linguist-hex-color="#555555" 2 | **/*.sh linguist-language=Shell linguist-display-name="Shell" 3 | pgit-blacklist="Modula-2" 4 | pgit-blacklist="XML" 5 | pgit-blacklist="GDScript3" 6 | pgit-blacklist="JSON" 7 | pgit-blacklist="YAML" 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/_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 | -------------------------------------------------------------------------------- /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.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | { 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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /appbundle-runtime/embed.go: -------------------------------------------------------------------------------- 1 | //go:build !noEmbed 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | // "syscall" 13 | 14 | "github.com/liamg/memit" 15 | ) 16 | 17 | type memitCmd struct { 18 | *exec.Cmd 19 | file *os.File 20 | } 21 | 22 | func (c *memitCmd) SetStdout(w io.Writer) { 23 | c.Cmd.Stdout = w 24 | } 25 | func (c *memitCmd) SetStderr(w io.Writer) { 26 | c.Cmd.Stderr = w 27 | } 28 | func (c *memitCmd) SetStdin(r io.Reader) { 29 | c.Cmd.Stdin = r 30 | } 31 | func (c *memitCmd) CombinedOutput() ([]byte, error) { 32 | return c.Cmd.CombinedOutput() 33 | } 34 | func (c *memitCmd) Run() error { 35 | defer c.file.Close() 36 | return c.Cmd.Run() 37 | } 38 | 39 | func newMemitCmd(cfg *RuntimeConfig, binary []byte, name string, args ...string) (*memitCmd, error) { 40 | if getEnv(globalEnv, "NO_MEMFDEXEC") == "1" { 41 | tempDir := filepath.Join(cfg.workDir, ".static") 42 | if err := os.MkdirAll(tempDir, 0755); err != nil { 43 | return nil, fmt.Errorf("failed to create temporary directory: %v", err) 44 | } 45 | tempFile := filepath.Join(tempDir, name) 46 | if err := os.WriteFile(tempFile, binary, 0755); err != nil { 47 | return nil, fmt.Errorf("failed to write temporary file: %v", err) 48 | } 49 | cmd := exec.Command(tempFile, args...) 50 | cmd.Env = globalEnv 51 | //// Detach FUSE binary so it survives runtime death 52 | //cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 53 | return &memitCmd{Cmd: cmd}, nil 54 | } 55 | cmd, file, err := memit.Command(bytes.NewReader(binary), args...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | cmd.Args[0] = name 60 | cmd.Env = globalEnv 61 | //// Detach FUSE binary so it survives runtime death 62 | //cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 63 | return &memitCmd{Cmd: cmd, file: file}, nil 64 | } 65 | 66 | func checkDeps(cfg *RuntimeConfig, fh *fileHandler) (*Filesystem, error) { 67 | fs, ok := getFilesystem(cfg.appBundleFS) 68 | if !ok { 69 | return nil, fmt.Errorf("unsupported filesystem: %s", cfg.appBundleFS) 70 | } 71 | return fs, nil 72 | } 73 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | { 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/appBundleID.md: -------------------------------------------------------------------------------- 1 | # AppBundleID Format Specification 2 | 3 | An `AppBundleID` is required for every AppBundle to ensure proper functionality, such as generating the `mountDir` and enabling tools like `appstream-helper` to figure out info about the AppBundle. It can be non-compliant (i.e., not follow Type I, II, or III) if distribution via AppBundleHUB or `dbin` is not intended. 4 | 5 | ## Supported Formats 6 | 7 | ### Type I: Legacy Format 8 | - **Structure**: `name-versionString-maintainer` or `name-date-maintainer` 9 | - **Examples**: `steam-v128.0.3-xplshn`, `steam-20250101-xplshn` 10 | - **Description**: Consists of the application name, either a version or date, and the maintainer/repository identifier. Suitable for filenames on systems with restrictive character support (e.g., no `#`, `:`). 11 | - **Use Case**: Legacy distribution; preferred only for filenames 12 | 13 | ### Type II: Modern Format without Date 14 | - **Structure**: `name#repo[:version]` 15 | - **Examples**: `steam#xplshn:v128.0.3`, `steam#xplshn` 16 | - **Description**: Includes the application name, repository/maintainer, and an optional version. Uses `#` to separate name and repository, with `:` for version. 17 | - **Use Case**: Most preferred format, in its short-form version 18 | 19 | ### Type III: Modern Format with Optional Date 20 | - **Structure**: `name#repo[:version][@date]` 21 | - **Examples**: `steam#xplshn:v128.0.3@20250101`, `steam#xplshn@20250101` 22 | - **Description**: The most flexible format, including application name, repository/maintainer, optional version, and optional date. Uses `#`, `:`, and `@` as separators. 23 | - **Use Case**: Most preferred format, as it contains the most complete data for `appstream-helper` to parse 24 | 25 | ## Requirements and Usage 26 | 27 | - **AppBundleHUB Distribution**: For inclusion in AppBundleHUB or `dbin` repositories, the `AppBundleID` must adhere to Type I, II, or III, as these formats allow `appstream-helper` to parse metadata (name/appstreamID, version, maintainer, date) for automated repository indexing. 28 | - **Recommendation**: Type III is encouraged for the AppBundleID, as it is the most complete while Type I is recommended for AppBundle filenames on systems that may not support special characters like `#`, `:`, or `@` used in Type II and Type III. 29 | 30 | # NOTEs 31 | 32 | 1. The program's AppStreamID should be used as the Name in the AppBundleID if the AppBundle does not ship with an .xml appstream metainfo file in the top-level of its AppDir. This way `appstream-helper` can use scrapped Flathub data to automatically populate a .description, .icon, .screenshots, .rank & .version entry for the `dbin` repository index file 33 | 2. Type I, II & III are all parsable by `appstream-helper` 34 | 35 | -------------------------------------------------------------------------------- /www/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | OPWD="$PWD" 4 | BASE="$(dirname "$(realpath "$0")")" 5 | TEMP_DIR="/tmp/pelf_build_$(date +%s)" 6 | # Change to BASE directory if not already there 7 | if [ "$OPWD" != "$BASE" ]; then 8 | echo "Changing to $BASE" 9 | cd "$BASE" || exit 1 10 | fi 11 | trap 'cd "$OPWD"; [ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"' EXIT 12 | 13 | # Check if we're really in WWW 14 | if [ ! "$(basename "$BASE")" = "www" ] || [ ! -f "$PWD/config.toml" ]; then 15 | echo "\"\$(basename \"\$BASE\")\" != \"www\" || \"\$PWD/config.toml\" does not exist" 16 | exit 1 17 | fi 18 | 19 | process_markdown_files() { 20 | mkdir -p "$2" 21 | for FILE in "$1"/*.md; do 22 | if [ ! -f "$FILE" ]; then continue; fi 23 | if [ "$(basename "$FILE")" = "_index.md" ]; then 24 | echo "Skipping \"$FILE\"" 25 | cp "$FILE" "./content/docs" 26 | _GENERATE_EMPTY_INDEX=0 27 | continue 28 | fi 29 | if [ "$(basename "$FILE")" = "index.md" ]; then 30 | echo "Skipping \"$FILE\"" 31 | continue 32 | fi 33 | 34 | FILENAME="$(basename "$FILE")" 35 | DATE="$(git log -1 --format="%ai" -- "$FILE" | awk '{print $1 "T" $2}')" 36 | TITLE=$(head -n 1 "$FILE" | grep '^#' | sed 's/^# //') 37 | [ -z "$TITLE" ] && { 38 | TITLE="$FILENAME" 39 | FILENAME_AS_TITLE=1 40 | } 41 | AUTHOR_NAME="$(git log --follow --format="%an" -- "$FILE" | tail -n 1)" 42 | AUTHOR_EMAIL="$(git log --follow --format="%ae" -- "$FILE" | tail -n 1)" 43 | 44 | { 45 | echo "+++" 46 | echo "date = '$DATE'" 47 | echo "draft = false" 48 | echo "title = '$TITLE'" 49 | echo "[params.author]" 50 | echo " name = '$AUTHOR_NAME'" 51 | echo " email = '$AUTHOR_EMAIL'" 52 | echo "+++" 53 | [ -z "$FILENAME_AS_TITLE" ] && sed '1{/^#/d}' "$FILE" 54 | } >"$2/$FILENAME" 55 | done 56 | 57 | if [ "$_GENERATE_EMPTY_INDEX" != "0" ]; then 58 | echo "Automatically generated an empty \"_index.md\"" 59 | if [ "$(find "$2" -maxdepth 1 -type f | wc -l)" -gt 0 ]; then 60 | { 61 | echo "---" 62 | echo "title: '$3'" 63 | echo "---" 64 | } >"$2/_index.md" 65 | fi 66 | fi 67 | } 68 | 69 | # Start actual processing 70 | rm -rf -- ./content/docs/* 71 | rm -rf -- ./static/assets/* 72 | process_markdown_files "../docs" "./content/docs" "Documentation" 73 | find ../assets/ -type f ! -name '*AppRun*' ! -name '*LAUNCH*' -exec cp {} ./static/assets/ \; 74 | { 75 | echo "---" 76 | echo "title: 'Home'" 77 | echo "---" 78 | } >./content/_index.md 79 | sed 's|src="files/|src="assets/|g' ../README.md >>./content/_index.md 80 | 81 | # Build with Hugo 82 | hugo 83 | -------------------------------------------------------------------------------- /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 = "git.xplshn.com.ar" 42 | url = "https://git.xplshn.com.ar" 43 | weight = 90 44 | [[menu.feed]] 45 | name = "harmful.cat-v.org" 46 | url = "https://harmful.cat-v.org" 47 | weight = 80 48 | [[menu.feed]] 49 | name = "nosystemd.org" 50 | url = "https://nosystemd.org" 51 | weight = 70 52 | [[menu.feed]] 53 | name = "suckless.org" 54 | url = "https://suckless.org" 55 | weight = 60 56 | [[menu.feed]] 57 | name = "copacabana.pindorama.net.br" 58 | url = "https://copacabana.pindorama.net.br" 59 | weight = 50 60 | [[menu.feed]] 61 | name = "shithub.us" 62 | url = "https://shithub.us" 63 | weight = 40 64 | [[menu.feed]] 65 | name = "managainstthestate.blogspot.com" 66 | url = "https://web.archive.org/web/20231123031907/https://managainstthestate.blogspot.com/2011/08/anarcho-capitalist-resources-by-wesker.html" 67 | weight = 20 68 | [[menu.feed]] 69 | name = "musl.libc.org" 70 | url = "https://musl.libc.org" 71 | weight = 10 72 | 73 | [taxonomies] 74 | category = "categories" 75 | series = "series" 76 | tag = "tags" 77 | 78 | [params] 79 | subtitle = "Labs" 80 | brandIconFile = "assets/images/icon.svg" 81 | abbrDateFmt = "Jan 2" 82 | dateFmt = "01.02.2006 15:04" 83 | themeVariant = "theme_blue.css" 84 | printSidebar = false 85 | 86 | [[social]] 87 | name = "Github" 88 | url = "https://github.com/xplshn" 89 | 90 | [markup.goldmark.renderer] 91 | hardWraps = false 92 | unsafe = true 93 | 94 | [markup.goldmark.extensions] 95 | [markup.goldmark.extensions.passthrough] 96 | enable = true 97 | [markup.goldmark.extensions.passthrough.delimiters] 98 | block = [['\[', '\]'], ['$$', '$$']] 99 | inline = [['\(', '\)']] 100 | 101 | [markup.goldmark.renderHooks.image] 102 | enableDefault = true 103 | 104 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xplshn/pelf //module pelf 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | fyne.io/fyne/v2 v2.5.5 7 | github.com/emmansun/base64 v0.7.0 8 | github.com/go-ini/ini v1.67.0 9 | github.com/goccy/go-json v0.10.5 10 | github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 11 | github.com/joho/godotenv v1.5.1 12 | github.com/klauspost/compress v1.18.1 13 | github.com/liamg/memit v0.0.3 14 | github.com/liamg/tml v0.7.0 15 | github.com/mholt/archives v0.1.2 16 | github.com/minio/md5-simd v1.1.2 17 | github.com/pkg/xattr v0.4.12 18 | github.com/shamaton/msgpack/v2 v2.4.0 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.6.1 22 | github.com/zeebo/blake3 v0.2.4 23 | golang.org/x/sys v0.38.0 24 | pgregory.net/rand v1.0.2 25 | ) 26 | 27 | require ( 28 | fyne.io/systray v1.11.0 // indirect 29 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 30 | github.com/STARRY-S/zip v0.2.3 // indirect 31 | github.com/andybalholm/brotli v1.2.0 // indirect 32 | github.com/bodgit/plumbing v1.3.0 // indirect 33 | github.com/bodgit/sevenzip v1.6.1 // indirect 34 | github.com/bodgit/windows v1.0.1 // indirect 35 | github.com/davecgh/go-spew v1.1.1 // indirect 36 | github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect 37 | github.com/ebitengine/purego v0.8.4 // indirect 38 | github.com/fredbi/uri v1.1.0 // indirect 39 | github.com/fsnotify/fsnotify v1.7.0 // indirect 40 | github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect 41 | github.com/fyne-io/glfw-js v0.0.0-20241126112943-313d8a0fe1d0 // indirect 42 | github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect 43 | github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect 44 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect 45 | github.com/go-ole/go-ole v1.3.0 // indirect 46 | github.com/go-text/render v0.2.0 // indirect 47 | github.com/go-text/typesetting v0.2.0 // indirect 48 | github.com/godbus/dbus/v5 v5.1.0 // indirect 49 | github.com/gopherjs/gopherjs v1.17.2 // indirect 50 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 51 | github.com/jeandeaual/go-locale v0.0.0-20240223122105-ce5225dcaa49 // indirect 52 | github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect 53 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 54 | github.com/klauspost/pgzip v1.2.6 // indirect 55 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 56 | github.com/mattn/go-runewidth v0.0.16 // indirect 57 | github.com/minio/minlz v1.0.1 // indirect 58 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 59 | github.com/nicksnyder/go-i18n/v2 v2.4.0 // indirect 60 | github.com/nwaples/rardecode/v2 v2.1.0 // indirect 61 | github.com/olekukonko/tablewriter v0.0.5 // indirect 62 | github.com/pierrec/lz4/v4 v4.1.22 // indirect 63 | github.com/pmezard/go-difflib v1.0.0 // indirect 64 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 65 | github.com/rivo/uniseg v0.4.7 // indirect 66 | github.com/rymdport/portal v0.3.0 // indirect 67 | github.com/sorairolake/lzip-go v0.3.7 // indirect 68 | github.com/spf13/afero v1.14.0 // indirect 69 | github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect 70 | github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect 71 | github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect 72 | github.com/stretchr/testify v1.11.1 // 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.14 // indirect 77 | github.com/yuin/goldmark v1.7.1 // indirect 78 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 79 | go4.org v0.0.0-20230225012048-214862532bf5 // indirect 80 | golang.org/x/image v0.25.0 // 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.26.0 // indirect 84 | gopkg.in/yaml.v3 v3.0.1 // indirect 85 | ) 86 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | workflow_dispatch: 7 | #schedule: 8 | # - cron: "0 14 * * 0" 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 | - /tmp/node20:/__e/node20 27 | steps: 28 | - name: Patch native Alpine NodeJS into Runner environment 29 | if: matrix.arch == 'aarch64' 30 | run: | 31 | apk add nodejs gcompat openssl 32 | sed -i "s:ID=alpine:ID=NotpineForGHA:" /etc/os-release 33 | # --- old workaround --- 34 | #ls /host/home/runner/*/* 35 | #cd /host/home/runner/runners/*/externals/ 36 | #rm -rf node20/* 37 | #mkdir node20/bin 38 | #ln -sfT /usr/bin/node node20/bin/node 39 | # --- second workaround --- 40 | mkdir -p /__e/node20/bin 41 | ln -sfT /usr/bin/node /__e/node20/bin/node 42 | ln -sfT /usr/bin/npm /__e/node20/bin/npm 43 | 44 | - name: Checkout repository 45 | uses: actions/checkout@v4 46 | with: 47 | fetch-depth: 0 48 | - name: Set up GOBIN and install lib4bin 49 | run: | 50 | set -x 51 | apk add zstd git bash file binutils patchelf findutils grep sed strace go fuse3 fuse curl yq-go b3sum 52 | export GOBIN="$GITHUB_WORKSPACE/.local/bin" CGO_ENABLED=0 GO_LDFLAGS='-buildmode=static-pie' GOFLAGS='-ldflags=-static-pie -ldflags=-s -ldflags=-w' 53 | export DBIN_INSTALL_DIR="$GOBIN" DBIN_NOCONFIG=1 PATH="$GOBIN:$PATH" 54 | mkdir -p "$GOBIN" 55 | wget -qO- "https://raw.githubusercontent.com/xplshn/dbin/master/stubdl" | sh -s -- --install "$DBIN_INSTALL_DIR/dbin" -v 56 | "$DBIN_INSTALL_DIR/dbin" --silent add yq upx 57 | echo "PATH=$PATH" >> $GITHUB_ENV 58 | echo "DBIN_INSTALL_DIR=$GOBIN" >> $GITHUB_ENV 59 | echo "WITH_SHARUN=1" >> $GITHUB_ENV 60 | echo "GEN_LIB_PATH=1" >> $GITHUB_ENV 61 | echo "ANY_EXECUTABLE=1" >> $GITHUB_ENV 62 | mkdir "$GITHUB_WORKSPACE/dist" 63 | ROOTFS_URL="$(curl -qsL https://dl-cdn.alpinelinux.org/alpine/edge/releases/${{ matrix.arch }}/latest-releases.yaml | yq '.[0].file')" 64 | echo "https://dl-cdn.alpinelinux.org/alpine/edge/releases/${{ matrix.arch }}/${ROOTFS_URL}" >"$GITHUB_WORKSPACE/dist/alpineLinuxEdge.${{ matrix.arch }}.rootfsURL" 65 | ROOTFS_URL="https://dl-cdn.alpinelinux.org/alpine/edge/releases/${{ matrix.arch }}/${ROOTFS_URL}" 66 | export ROOTFS_URL 67 | echo "ROOTFS_URL=$ROOTFS_URL" >> "$GITHUB_ENV" 68 | apk add coreutils 69 | - name: Build AppBundle tooling 70 | run: | 71 | cd "$GITHUB_WORKSPACE" 72 | export CGO_ENABLED=0 GOFLAGS="-ldflags=-static-pie -ldflags=-s -ldflags=-w" GO_LDFLAGS="-buildmode=static-pie -s -w" 73 | export _RELEASE="1" 74 | ./cbuild.sh && ./cbuild.sh pelfCreator_extensions 75 | B3SUM_CHECKSUM="$(b3sum ./pelf | awk '{print $1}')" 76 | mv ./pelf "$GITHUB_WORKSPACE/dist/pelf_${{ matrix.arch }}" 77 | mv ./cmd/pelfCreator/pelfCreator "$GITHUB_WORKSPACE/dist/pelfCreator_${{ matrix.arch }}" 78 | mv ./cmd/pelfCreator/pelfCreatorExtension_archLinux.tar.zst "$GITHUB_WORKSPACE/dist/pelfCreatorExtension_archLinux_${{ matrix.arch }}".tar.zst 79 | mv ./cmd/misc/appstream-helper/appstream-helper "$GITHUB_WORKSPACE/dist/appstream-helper_${{ matrix.arch }}" 80 | echo "RELEASE_TAG=$(date +%d%m%Y)-$B3SUM_CHECKSUM" >> $GITHUB_ENV 81 | - name: Upload artifact 82 | uses: actions/upload-artifact@v4.6.1 83 | with: 84 | name: AppBundle-${{ matrix.arch }} 85 | path: ${{ github.workspace }}/dist/* 86 | - name: Set build output 87 | id: build_output 88 | run: | 89 | echo "release_tag=$(date +%d%m%Y)-$(b3sum ./pelf | awk '{print $1}')" >> $GITHUB_OUTPUT 90 | 91 | release: 92 | name: Create Release 93 | needs: build 94 | runs-on: ubuntu-latest 95 | steps: 96 | - name: Download all artifacts 97 | uses: actions/download-artifact@v4 98 | with: 99 | path: artifacts 100 | merge-multiple: true 101 | 102 | - name: List files 103 | run: find artifacts -type f | sort 104 | 105 | - name: Create Release 106 | uses: softprops/action-gh-release@v2.2.1 107 | with: 108 | name: "Build ${{ needs.build.outputs.release_tag || github.run_number }}" 109 | tag_name: "${{ needs.build.outputs.release_tag || github.run_number }}" 110 | prerelease: false 111 | draft: false 112 | generate_release_notes: false 113 | make_latest: true 114 | files: | 115 | artifacts/* 116 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 70 | --- 71 | 72 |

73 | 74 | Ask DeepWiki 75 | 76 |

77 | -------------------------------------------------------------------------------- /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"` // Contents should be as specified [appBundleID.md](../appBundleID.md) 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/tooling.md: -------------------------------------------------------------------------------- 1 | # Tooling Usage & Explanation 2 | 3 | ## `pelf` 4 | 5 | The `pelf` command is responsible for assembling an AppBundle by combining an ELF runtime, runtime information, static tools, and a compressed filesystem image. 6 | 7 | ### Functionality 8 | 9 | - **Purpose**: Creates an AppBundle from an AppDir, embedding necessary metadata and tools. 10 | - **Key Operations**: 11 | - Reads an AppDir, verifies that it contains an executable AppRun 12 | - Copies the runtime to the output file 13 | - Embeds runtime information (MessagePack format) in the `.pbundle_runtime_info` section of the output file 14 | - 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. 15 | - Compresses the AppDir into a DwarFS or SquashFS filesystem image and appends it to the output file 16 | - Sets the AppBundle's executable permissions and finalizes the output file. 17 | 18 | ### Command-Line Usage 19 | 20 | The pelf tool is can be invoked with the following flags: 21 | 22 | - **--add-appdir, -a **: Specifies the AppDir to package. 23 | - **--appbundle-id, -i **: Sets the unique AppBundleID for the AppBundle. 24 | - **--output-to, -o **: Specifies the output file name (e.g., app.dwfs.AppBundle). 25 | - **--compression, -c **: Specifies compression flags for the filesystem. 26 | - **--static-tools-dir **: Specifies a custom directory for static tools. 27 | - **--runtime **: Specifies the runtime binary to use. 28 | - **--upx**: Enables UPX compression for static tools. (upx must be in the host system) 29 | - **--filesystem, -j :** Selects the filesystem type (squashfs or [dwarfs]). 30 | - **--prefer-tools-in-path:** Prefers tools in `$PATH` over embedded ones. 31 | - **--list-static-tools:** Lists embedded tools with their B3SUMs. 32 | - **--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 33 | - **--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). 34 | - **--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) 35 | - **--add-runtime-info-section :** Adds custom runtime information fields. (e.g: '.MyCustomRuntimeInfoSection:Hello') 36 | - **--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 37 | - **--add-updinfo :** Adds an upd_info ELF section with the given string. 38 | 39 | ## pelfCreator 40 | 41 | 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. 42 | 43 | ### Functionality 44 | 45 | - **Purpose**: Creates an AppDir, populates it with a root filesystem, application files, and dependencies, and then packages it into an AppBundle. 46 | - **Key Operations**: 47 | - Sets up a temporary directory for processing. 48 | - Downloads or uses a local root filesystem (e.g., Alpine or ArchLinux). 49 | - Installs specified packages using `apk` (Alpine) or `pacman` (ArchLinux). 50 | - Configures the AppRun script and entrypoint. 51 | - Optionally processes binaries with `lib4bin` for `sharun` mode. 52 | - Trims the filesystem based on `--keep` or `--getrid` flags. 53 | - Calls `pelf` to finalize the AppBundle. 54 | 55 | ### Command-Line Usage 56 | 57 | The `pelfCreator` tool is invoked with the following flags: 58 | 59 | - **`--maintainer `**: Specifies the maintainer's name (required). 60 | - **`--name `**: Sets the application name (required). 61 | - **`--appbundle-id `**: Sets the `AppBundleID` (optional; defaults to `--`). 62 | - **`--pkg-add `**: Specifies packages to install in the root filesystem (required). 63 | - **`--entrypoint `**: Sets the entrypoint command or desktop file (required unless using `--multicall`). 64 | - **`--keep `**: Specifies files to keep in the `proto` directory. 65 | - **`--getrid `**: Specifies files to remove from the `proto` directory. 66 | - **`--filesystem `**: Selects the filesystem type (`dwfs` or `squashfs`; default: `dwfs`). 67 | - **`--output-to `**: Specifies the output AppBundle file (optional; defaults to `..AppBundle`). 68 | - **`--local `**: Specifies a directory or archive containing resources (e.g., `rootfs.tar`, `AppRun`, `bwrap`). 69 | - **`--preserve-rootfs-permissions`**: Preserves original filesystem permissions. 70 | - **`--dontpack`**: Stops short of packaging the AppDir into an AppBundle, leaving only the AppDir. 71 | - **`--sharun `**: Processes specified binaries with `lib4bin` and uses `AppRun.sharun` or `AppRun.sharun.ovfsProto`. 72 | - **`--sandbox`**: Enables sandbox mode using `AppRun.rootfs-based` with `bwrap`. 73 | 74 | ### Modes of Operation 75 | 76 | 1. **Sandbox Mode** (`--sandbox`): 77 | - Retains and binds the `proto` directory as the root filesystem, with host directory bindings (e.g., `/home`, `/tmp`, `/etc`). 78 | - Supports trimming of the `proto` directory using `--keep` or `--getrid` flags to reduce size. 79 | - Uses `AppRun.rootfs-based` to run the application in a `bwrap` sandbox. 80 | - Can be customized via env vars such as `SHARE_LOOK`, `SHARE_FONTS`, `SHARE_AUDIO`, and `UID0_GID0` for fine-grained control over sandboxing. 81 | - Suitable for applications requiring strict isolation from the host system. Or those that refuse to work with the default mode (hybrid) 82 | 83 | 2. **Sharun Mode** (`--sharun `): 84 | - Processes specified binaries with `lib4bin` to ensure compatibility and portability. 85 | - Uses `AppRun.sharun` (if `proto` is removed) or `AppRun.sharun.ovfsProto` (if `proto` is retained). 86 | - When using `AppRun.sharun.ovfsProto`, employs `unionfs-fuse` to create a copy-on-write overlay of the `proto` directory. 87 | - Sets `LD_LIBRARY_PATH` to include library paths from the AppDir, ensuring binaries can find their dependencies. 88 | - Ideal for lightweight applications or when minimizing filesystem size is a priority. 89 | 90 | 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): 91 | - Retains the `proto` directory 92 | - Supports trimming of the `proto` directory using `--keep` or `--getrid` flags to reduce size. 93 | - Uses `AppRun.sharun.ovfsProto` to execute the application with a `unionfs-fuse` overlay of the user's `/` & the AppDir's `proto`. 94 | - 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 95 | 96 | ## Notes 97 | 98 | - The `pelfCreator` tool supports extensibility through custom root filesystems and package managers via the `--local` flag. 99 | -------------------------------------------------------------------------------- /docs/runtime.md: -------------------------------------------------------------------------------- 1 | # AppBundle Runtime Logic Specification 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/$name" 2>/dev/null) || unset TEMP_DIR 84 | if [ -z "$TEMP_DIR" ]; then 85 | base="${TMPDIR:-/tmp}/.doNotDelete_youWillLoseHOME_$$" 86 | n=0 87 | while true; do 88 | TEMP_DIR="${base}_${n}" 89 | # mkdir is atomic in POSIX when used like this 90 | mkdir "$TEMP_DIR" 2>/dev/null && break 91 | n=$((n + 1)) 92 | # Sanity limit — if we ever hit this, something is horribly wrong 93 | [ "$n" -gt 5 ] && { echo "Cannot create temp dir" >&2; exit 1; } 94 | done 95 | fi 96 | [ -d "$TEMP_DIR" ] || { echo "Failed to create temporary directory" >&2; exit 1; } 97 | 98 | printf '%s\n' \ 99 | "DANGER: This directory contains a symlink or bind-mount to your HOME." \ 100 | "Deleting it with rm -rf will destroy your personal files." \ 101 | > "$TEMP_DIR/DO_NOT_DELETE_OR_YOU_WILL_LOSE_YOUR_HOME" 102 | MOUNT_DIR="$TEMP_DIR/mount_dir" 103 | mkdir -p "$MOUNT_DIR" 104 | 105 | # Mount the unionfs 106 | "$UNIONFS_BIN" -o cow,preserve_branch "$PROTO=RO:/=RW" "$MOUNT_DIR" 107 | 108 | cleanup() { 109 | FUSERMOUNT="fusermount3" 110 | command -v "fusermount" && FUSERMOUNT="fusermount" 111 | 112 | ( 113 | # Attempt to unmount 114 | "$FUSERMOUNT" -u "$MOUNT_DIR" 2>/dev/null 115 | 116 | # Wait and check if the mount point is unmounted 117 | _wait() { 118 | for i in 1 2 3 4 5; do 119 | if mountpoint -q "$MOUNT_DIR"; then 120 | sleep "$i" 121 | else 122 | break 123 | fi 124 | done 125 | }; _wait 126 | 127 | # Force unmount if still mounted 128 | if mountpoint -q "$MOUNT_DIR"; then 129 | "$FUSERMOUNT" -uz "$MOUNT_DIR" 2>/dev/null 130 | fi 131 | 132 | _wait 133 | 134 | # Safe remove ops 135 | if ! mountpoint -q "$MOUNT_DIR"; then 136 | rmdir --ignore-fail-on-non-empty "$UNIONFS_DIR" 137 | rmdir --ignore-fail-on-non-empty "$TEMP_DIR" 138 | fi 139 | ) & # Run cleanup in the background 140 | } 141 | trap cleanup INT TERM HUP QUIT EXIT 142 | 143 | bool() { 144 | case "$1" in 145 | false | 0) echo "0" ;; 146 | true | 1) echo "1" ;; 147 | *) echo "Invalid boolean value: $1" >&2; exit 1 ;; 148 | esac 149 | } 150 | 151 | # Function to check if a feature is enabled by file presence 152 | is_enabled() { 153 | _propName="$1" 154 | [ -f "$APPDIR/.enabled/$_propName" ] && echo "1" && return 0 155 | [ -f "$APPDIR/.disabled/$_propName" ] && echo "0" && return 0 156 | echo "$2" ; return 5 157 | } 158 | 159 | # Defaults with file-based overrides 160 | #SHARE_LOOK="$(is_enabled "SHARE_LOOK" 1)" 161 | #SHARE_FONTS="$(is_enabled "SHARE_FONTS" 1)" 162 | #SHARE_AUDIO="$(is_enabled "SHARE_AUDIO" 1)" 163 | SHARE_XDG_RUNTIME_DIR="$(is_enabled "SHARE_XDG_RUNTIME_DIR" 1)" 164 | SHARE_VAR="$(is_enabled "SHARE_VAR" 1)" 165 | SHARE_RUN="$(is_enabled "SHARE_RUN" 1)" 166 | UID0_GID0="$(is_enabled "UID0_GID0" 0)" 167 | 168 | # Initialize the bwrap command 169 | TMPDIR="${TMPDIR:-/tmp}" 170 | bwrap_cmd="$BWRAP_BIN --dev-bind \"$MOUNT_DIR\" / --bind-try \"$TMPDIR\" \"$TMPDIR\" --bind-try \"$HOME\" \"$HOME\"" 171 | [ "$UID0_GID0" = "1" ] && bwrap_cmd="$bwrap_cmd --uid 0 --gid 0" 172 | [ "$SHARE_VAR" = "1" ] && bwrap_cmd="$bwrap_cmd --bind-try /var /var" 173 | [ "$SHARE_RUN" = "1" ] && bwrap_cmd="$bwrap_cmd --bind-try /run /run" 174 | 175 | # Add optional XDG_RUNTIME_DIR binding if enabled 176 | [ "$SHARE_XDG_RUNTIME_DIR" = "1" ] && [ -n "$XDG_RUNTIME_DIR" ] && bwrap_cmd="$bwrap_cmd --bind-try $XDG_RUNTIME_DIR $XDG_RUNTIME_DIR" 177 | 178 | # Themes & Icons 179 | #if [ "$SHARE_LOOK" = "1" ]; then 180 | # [ -d /usr/share/icons ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/icons /usr/share/icons" 181 | # [ -d /usr/share/themes ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --ro-bind-try /usr/share/themes /usr/share/themes" 182 | #fi 183 | # 184 | ## Fonts 185 | #if [ "$SHARE_FONTS" = 1 ]; then 186 | # [ -d /usr/share/fontconfig ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --bind-try /usr/share/fontconfig /usr/share/fontconfig" 187 | # [ -d /usr/share/fonts ] && BWRAP_OPTIONS="$BWRAP_OPTIONS --bind-try /usr/share/fonts /usr/share/fonts" 188 | #fi 189 | # 190 | ## Audio support 191 | #if [ "$SHARE_AUDIO" = "1" ] && [ -n "$XDG_RUNTIME_DIR" ]; then 192 | # bwrap_cmd="$bwrap_cmd \ 193 | # --bind-try /etc/asound.conf /etc/asound.conf \ 194 | # --bind-try $XDG_RUNTIME_DIR/pulse $XDG_RUNTIME_DIR/pulse \ 195 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0 $XDG_RUNTIME_DIR/pipewire-0 \ 196 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0.lock $XDG_RUNTIME_DIR/pipewire-0.lock \ 197 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0-manager $XDG_RUNTIME_DIR/pipewire-0-manager \ 198 | # --bind-try $XDG_RUNTIME_DIR/pipewire-0-manager.lock $XDG_RUNTIME_DIR/pipewire-0-manager.lock" 199 | #fi 200 | 201 | # Add special flags which are needed to "unsandbox" bwrap ("not a security boundary"): 202 | bwrap_cmd="$bwrap_cmd --proc /proc" 203 | bwrap_cmd="$bwrap_cmd --dev-bind /dev /dev --ro-bind-try /sys /sys" 204 | bwrap_cmd="$bwrap_cmd --cap-add CAP_SYS_ADMIN" 205 | bwrap_cmd="$bwrap_cmd --share-net" 206 | 207 | eval "$bwrap_cmd" -- "$_cmd" $@ 208 | -------------------------------------------------------------------------------- /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 | detachedCleanup(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 | -------------------------------------------------------------------------------- /assets/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /www/static/assets/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/AppRun.rootfs-based: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # DATE OF LAST REVISION: 28-07-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" ] && echo "bwrap not at $BWRAP_BIN" 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 | --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 --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 | printf '%s\n' "$BWRAP_OPTIONS" 203 | } 204 | 205 | # Build and execute the bwrap options 206 | BWRAP_OPTIONS="$(build_bwrap_options "$@")" 207 | 208 | eval "exec $BWRAP_BIN $BWRAP_OPTIONS $SELF_ARGS" 209 | -------------------------------------------------------------------------------- /appbundle-runtime/noEmbed.go: -------------------------------------------------------------------------------- 1 | //go:build noEmbed 2 | 3 | package main 4 | 5 | import ( 6 | "archive/tar" 7 | "bytes" 8 | "debug/elf" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | // "syscall" 16 | 17 | "github.com/klauspost/compress/zstd" 18 | ) 19 | 20 | const runtimeEdition = "noEmbed" 21 | 22 | type osExecCmd struct { 23 | *exec.Cmd 24 | } 25 | 26 | func (c *osExecCmd) SetStdout(w io.Writer) { c.Cmd.Stdout = w } 27 | func (c *osExecCmd) SetStderr(w io.Writer) { c.Cmd.Stderr = w } 28 | func (c *osExecCmd) SetStdin(r io.Reader) { c.Cmd.Stdin = r } 29 | func (c *osExecCmd) CombinedOutput() ([]byte, error) { return c.Cmd.CombinedOutput() } 30 | 31 | var Filesystems = []*Filesystem{ 32 | { 33 | Type: "squashfs", 34 | Commands: []string{"squashfuse", "unsquashfs"}, 35 | MountCmd: func(cfg *RuntimeConfig) CommandRunner { 36 | executable, err := lookPath("squashfuse", globalPath) 37 | if err != nil { 38 | println(globalPath) 39 | logError("squashfuse not available", err, cfg) 40 | } 41 | args := []string{ 42 | "-o", "ro,nodev", 43 | "-o", "uid=0,gid=0", 44 | "-o", fmt.Sprintf("offset=%d", cfg.archiveOffset), 45 | cfg.selfPath, 46 | cfg.mountDir, 47 | } 48 | if getEnv(globalEnv, "ENABLE_FUSE_DEBUG") != "" { 49 | logWarning("squashfuse's debug mode implies foreground. The AppRun won't be called.") 50 | args = append(args, "-o", "debug") 51 | } 52 | cmd := exec.Command(executable, args...) 53 | cmd.Env = globalEnv 54 | //// Detach FUSE binary so it survives runtime death 55 | //cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 56 | return &osExecCmd{cmd} 57 | }, 58 | ExtractCmd: func(cfg *RuntimeConfig, query string) CommandRunner { 59 | executable, err := lookPath("unsquashfs", globalPath) 60 | if err != nil { 61 | logError("unsquashfs not available", err, cfg) 62 | } 63 | args := []string{"-d", cfg.mountDir, "-o", fmt.Sprintf("%d", cfg.archiveOffset), cfg.selfPath} 64 | if query != "" { 65 | for _, file := range strings.Split(query, " ") { 66 | args = append(args, "-e", file) 67 | } 68 | } 69 | cmd := exec.Command(executable, args...) 70 | cmd.Env = globalEnv 71 | //// Detach FUSE binary so it survives runtime death 72 | //cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 73 | return &osExecCmd{cmd} 74 | }, 75 | }, 76 | { 77 | Type: "dwarfs", 78 | Commands: []string{"dwarfs", "dwarfsextract"}, 79 | MountCmd: func(cfg *RuntimeConfig) CommandRunner { 80 | executable, err := lookPath("dwarfs", globalPath) 81 | if err != nil { 82 | logError("dwarfs not available", err, cfg) 83 | } 84 | cacheSize := getDwarfsCacheSize() 85 | args := []string{ 86 | "-o", "ro,nodev", 87 | "-o", "cache_files,no_cache_image,clone_fd", 88 | "-o", "block_allocator=" + getEnvWithDefault(globalEnv, "DWARFS_BLOCK_ALLOCATOR", DWARFS_BLOCK_ALLOCATOR), 89 | "-o", getEnvWithDefault(globalEnv, "DWARFS_TIDY_STRATEGY", DWARFS_TIDY_STRATEGY), 90 | "-o", "debuglevel=" + T(getEnv(globalEnv, "ENABLE_FUSE_DEBUG") != "", "debug", "error"), 91 | "-o", "readahead=" + getEnvWithDefault(globalEnv, "DWARFS_READAHEAD", DWARFS_READAHEAD), 92 | "-o", "blocksize=" + getEnvWithDefault(globalEnv, "DWARFS_BLOCKSIZE", DWARFS_BLOCKSIZE), 93 | "-o", "cachesize=" + cacheSize, 94 | "-o", "workers=" + getDwarfsWorkers(&cacheSize), 95 | "-o", fmt.Sprintf("offset=%d", cfg.archiveOffset), 96 | cfg.selfPath, 97 | cfg.mountDir, 98 | } 99 | if e := getEnv(globalEnv, "DWARFS_ANALYSIS_FILE"); e != "" { 100 | args = append(args, "-o", "analysis_file="+e) 101 | } 102 | if e := getEnv(globalEnv, "DWARFS_PRELOAD_ALL"); e != "" { 103 | args = append(args, "-o", "preload_all") 104 | } else { 105 | args = append(args, "-o", "preload_category=hotness") 106 | } 107 | cmd := exec.Command(executable, args...) 108 | cmd.Env = globalEnv 109 | //// Detach FUSE binary so it survives runtime death 110 | //cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 111 | return &osExecCmd{cmd} 112 | }, 113 | ExtractCmd: func(cfg *RuntimeConfig, query string) CommandRunner { 114 | executable, err := lookPath("dwarfsextract", globalPath) 115 | if err != nil { 116 | logError("dwarfsextract not available", err, cfg) 117 | } 118 | args := []string{ 119 | "--input", cfg.selfPath, 120 | "--image-offset", fmt.Sprintf("%d", cfg.archiveOffset), 121 | "--output", cfg.mountDir, 122 | } 123 | if query != "" { 124 | for _, pattern := range strings.Split(query, " ") { 125 | args = append(args, "--pattern", pattern) 126 | } 127 | } 128 | cmd := exec.Command(executable, args...) 129 | cmd.Env = globalEnv 130 | //// Detach FUSE binary so it survives runtime death 131 | //cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} 132 | return &osExecCmd{cmd} 133 | }, 134 | }, 135 | } 136 | 137 | func (f *fileHandler) extractStaticTools(cfg *RuntimeConfig) error { 138 | elfFile, err := elf.NewFile(f.file) 139 | if err != nil { 140 | return fmt.Errorf("parse ELF: %w", err) 141 | } 142 | 143 | staticToolsSection := elfFile.Section(".pbundle_static_tools") 144 | if staticToolsSection == nil { 145 | return fmt.Errorf("static_tools section not found") 146 | } 147 | 148 | staticToolsData, err := staticToolsSection.Data() 149 | if err != nil { 150 | return fmt.Errorf("failed to read static_tools section: %w", err) 151 | } 152 | 153 | decoder, err := zstd.NewReader(bytes.NewReader(staticToolsData)) 154 | if err != nil { 155 | return fmt.Errorf("zstd init: %w", err) 156 | } 157 | defer decoder.Close() 158 | 159 | sizeCache := make(map[string]int64) 160 | err = filepath.Walk(cfg.staticToolsDir, func(path string, info os.FileInfo, err error) error { 161 | if err != nil { 162 | return err 163 | } 164 | if !info.IsDir() { 165 | relPath, err := filepath.Rel(cfg.staticToolsDir, path) 166 | if err != nil { 167 | return err 168 | } 169 | sizeCache[relPath] = info.Size() 170 | } 171 | return nil 172 | }) 173 | if err != nil { 174 | return fmt.Errorf("failed to cache file sizes: %w", err) 175 | } 176 | 177 | tr := tar.NewReader(decoder) 178 | for { 179 | hdr, err := tr.Next() 180 | if err == io.EOF { 181 | break 182 | } 183 | if err != nil { 184 | return fmt.Errorf("tar read: %w", err) 185 | } 186 | 187 | fpath := filepath.Join(cfg.staticToolsDir, hdr.Name) 188 | relPath, err := filepath.Rel(cfg.staticToolsDir, fpath) 189 | if err != nil { 190 | return fmt.Errorf("failed to get relative path: %w", err) 191 | } 192 | 193 | if _, exists := sizeCache[relPath]; exists { 194 | continue 195 | } 196 | 197 | switch hdr.Typeflag { 198 | case tar.TypeDir: 199 | if err := os.MkdirAll(fpath, 0755); err != nil { 200 | return fmt.Errorf("mkdir %s: %w", fpath, err) 201 | } 202 | case tar.TypeReg: 203 | if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { 204 | return fmt.Errorf("mkdir parent: %w", err) 205 | } 206 | f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode)) 207 | if err != nil { 208 | return fmt.Errorf("create: %w", err) 209 | } 210 | _, err = io.Copy(f, tr) 211 | f.Close() 212 | if err != nil { 213 | return fmt.Errorf("write: %w", err) 214 | } 215 | case tar.TypeSymlink: 216 | if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { 217 | return fmt.Errorf("mkdir parent: %w", err) 218 | } 219 | if err := os.Symlink(hdr.Linkname, fpath); err != nil { 220 | return fmt.Errorf("symlink: %w", err) 221 | } 222 | case tar.TypeLink: 223 | if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { 224 | return fmt.Errorf("mkdir parent: %w", err) 225 | } 226 | if err := os.Link(hdr.Linkname, fpath); err != nil { 227 | return fmt.Errorf("hardlink: %w", err) 228 | } 229 | } 230 | } 231 | 232 | return nil 233 | } 234 | 235 | func checkDeps(cfg *RuntimeConfig, fh *fileHandler) (*Filesystem, error) { 236 | fs, ok := getFilesystem(cfg.appBundleFS) 237 | if !ok { 238 | return nil, fmt.Errorf("unsupported filesystem: %s", cfg.appBundleFS) 239 | } 240 | 241 | updatePath("PATH", cfg.staticToolsDir) 242 | var missingCmd bool 243 | for _, cmd := range fs.Commands { 244 | if _, err := lookPath(cmd, globalPath); err != nil { 245 | missingCmd = true 246 | break 247 | } 248 | } 249 | 250 | if missingCmd { 251 | if err := os.MkdirAll(cfg.staticToolsDir, 0755); err != nil { 252 | return nil, fmt.Errorf("failed to create static tools directory: %v", err) 253 | } 254 | 255 | if err := fh.extractStaticTools(cfg); err != nil { 256 | return nil, fmt.Errorf("failed to extract static tools: %v", err) 257 | } 258 | } 259 | 260 | return fs, nil 261 | } 262 | -------------------------------------------------------------------------------- /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/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/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 | --------------------------------------------------------------------------------