├── .github └── FUNDING.yml ├── .gitignore ├── .goreleaser.yaml ├── .woodpecker.yml ├── LICENSE ├── Makefile ├── README.md ├── assets └── logo.png ├── build.go ├── docs ├── README.md ├── configuration.md ├── packages │ ├── README.md │ ├── adding-packages.md │ ├── build-scripts.md │ └── conventions.md └── usage.md ├── fix.go ├── gen.go ├── go.mod ├── go.sum ├── helper.go ├── info.go ├── install.go ├── internal ├── cliutils │ └── prompt.go ├── config │ ├── config.go │ ├── lang.go │ ├── paths.go │ └── version.go ├── cpu │ └── cpu.go ├── db │ ├── db.go │ └── db_test.go ├── dl │ ├── dl.go │ ├── file.go │ ├── git.go │ └── torrent.go ├── dlcache │ ├── dlcache.go │ └── dlcache_test.go ├── osutils │ └── move.go ├── overrides │ ├── overrides.go │ └── overrides_test.go ├── pager │ ├── highlighting.go │ └── pager.go ├── shutils │ ├── decoder │ │ ├── decoder.go │ │ └── decoder_test.go │ ├── handlers │ │ ├── exec.go │ │ ├── exec_test.go │ │ ├── fakeroot.go │ │ ├── nop.go │ │ ├── nop_test.go │ │ └── restricted.go │ └── helpers │ │ └── helpers.go ├── translations │ ├── files │ │ ├── lure.en.toml │ │ └── lure.ru.toml │ └── translations.go └── types │ ├── build.go │ ├── config.go │ └── repo.go ├── list.go ├── main.go ├── pkg ├── build │ ├── build.go │ └── install.go ├── distro │ └── osrelease.go ├── gen │ ├── funcs.go │ ├── pip.go │ └── tmpls │ │ └── pip.tmpl.sh ├── loggerctx │ └── log.go ├── manager │ ├── apk.go │ ├── apt.go │ ├── dnf.go │ ├── managers.go │ ├── pacman.go │ ├── yum.go │ └── zypper.go ├── repos │ ├── find.go │ ├── find_test.go │ ├── pull.go │ └── pull_test.go └── search │ └── search.go ├── repo.go ├── scripts ├── completion │ ├── bash │ └── zsh └── install.sh └── upgrade.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: lure 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lure 2 | /lure-api-server 3 | /cmd/lure-api-server/lure-api-server 4 | /dist/ 5 | /internal/config/version.txt 6 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - id: lure 6 | env: 7 | - CGO_ENABLED=0 8 | binary: lure 9 | ldflags: 10 | - -X go.elara.ws/lure/internal/config.Version={{.Version}} 11 | goos: 12 | - linux 13 | goarch: 14 | - amd64 15 | - 386 16 | - arm64 17 | - arm 18 | - riscv64 19 | archives: 20 | - name_template: >- 21 | {{- .ProjectName}}- 22 | {{- .Version}}- 23 | {{- .Os}}- 24 | {{- if .Arch | eq "amd64"}}x86_64 25 | {{- else if .Arch | eq "386"}}i386 26 | {{- else if .Arch | eq "arm64"}}aarch64 27 | {{- else }}{{ .Arch }}{{ end -}} 28 | files: 29 | - scripts/completion/* 30 | nfpms: 31 | - id: lure 32 | package_name: linux-user-repository 33 | file_name_template: >- 34 | {{- .PackageName}}- 35 | {{- .Version}}- 36 | {{- .Os}}- 37 | {{- if .Arch | eq "amd64"}}x86_64 38 | {{- else if .Arch | eq "386"}}i386 39 | {{- else if .Arch | eq "arm64"}}aarch64 40 | {{- else }}{{ .Arch }}{{ end -}} 41 | description: "Linux User REpository" 42 | homepage: 'https://lure.sh' 43 | maintainer: 'Elara Musayelyan ' 44 | license: GPLv3 45 | formats: 46 | - apk 47 | - deb 48 | - rpm 49 | - archlinux 50 | provides: 51 | - linux-user-repository 52 | conflicts: 53 | - linux-user-repository 54 | recommends: 55 | - aria2 56 | contents: 57 | - src: scripts/completion/bash 58 | dst: /usr/share/bash-completion/completions/lure 59 | - src: scripts/completion/zsh 60 | dst: /usr/share/zsh/site-functions/_lure 61 | aurs: 62 | - name: linux-user-repository-bin 63 | homepage: 'https://lure.sh' 64 | description: "Linux User REpository" 65 | maintainers: 66 | - 'Elara Musayelyan ' 67 | license: GPLv3 68 | private_key: '{{ .Env.AUR_KEY }}' 69 | git_url: 'ssh://aur@aur.archlinux.org/linux-user-repository-bin.git' 70 | provides: 71 | - linux-user-repository 72 | conflicts: 73 | - linux-user-repository 74 | depends: 75 | - sudo 76 | - pacman 77 | optdepends: 78 | - 'aria2: for downloading torrent sources' 79 | package: |- 80 | # binaries 81 | install -Dm755 ./lure "${pkgdir}/usr/bin/lure" 82 | 83 | # completions 84 | install -Dm755 ./scripts/completion/bash ${pkgdir}/usr/share/bash-completion/completions/lure 85 | install -Dm755 ./scripts/completion/zsh ${pkgdir}/usr/share/zsh/site-functions/_lure 86 | release: 87 | gitea: 88 | owner: lure 89 | name: lure 90 | gitea_urls: 91 | api: 'https://gitea.elara.ws/api/v1/' 92 | download: 'https://gitea.elara.ws' 93 | skip_tls_verify: false 94 | checksum: 95 | name_template: 'checksums.txt' 96 | snapshot: 97 | name_template: "{{ incpatch .Version }}-next" 98 | changelog: 99 | sort: asc -------------------------------------------------------------------------------- /.woodpecker.yml: -------------------------------------------------------------------------------- 1 | platform: linux/amd64 2 | pipeline: 3 | release: 4 | image: goreleaser/goreleaser 5 | commands: 6 | - goreleaser release 7 | secrets: [ gitea_token, aur_key ] 8 | when: 9 | event: tag -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | GIT_VERSION = $(shell git describe --tags ) 3 | 4 | lure: 5 | CGO_ENABLED=0 go build -ldflags="-X 'go.elara.ws/lure/internal/config.Version=$(GIT_VERSION)'" 6 | 7 | clean: 8 | rm -f lure 9 | 10 | install: lure installmisc 11 | install -Dm755 lure $(DESTDIR)$(PREFIX)/bin/lure 12 | 13 | installmisc: 14 | install -Dm755 scripts/completion/bash $(DESTDIR)$(PREFIX)/share/bash-completion/completions/lure 15 | install -Dm755 scripts/completion/zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_lure 16 | 17 | uninstall: 18 | rm -f /usr/local/bin/lure 19 | 20 | .PHONY: install clean uninstall installmisc lure -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LURE Logo 2 | 3 | # LURE (Linux User REpository) 4 | 5 | [![Go Report Card](https://goreportcard.com/badge/go.elara.ws/lure)](https://goreportcard.com/report/go.elara.ws/lure) 6 | [![status-badge](https://ci.elara.ws/api/badges/lure/lure/status.svg)](https://ci.elara.ws/lure/lure) 7 | [![SWH](https://archive.softwareheritage.org/badge/origin/https://gitea.elara.ws/lure/lure.git/)](https://archive.softwareheritage.org/browse/origin/?origin_url=https://gitea.elara.ws/lure/lure.git) 8 | [![linux-user-repository-bin AUR package](https://img.shields.io/aur/version/linux-user-repository-bin?label=linux-user-repository-bin&logo=archlinux)](https://aur.archlinux.org/packages/linux-user-repository-bin/) 9 | 10 | LURE is a distro-agnostic build system for Linux, similar to the [AUR](https://wiki.archlinux.org/title/Arch_User_Repository). It is currently in **beta**. Most major bugs have been fixed, and most major features have been added. LURE is ready for general use, but may still break or change occasionally. 11 | 12 | LURE is written in pure Go and has zero dependencies after building. The only things LURE requires are a command for privilege elevation such as `sudo`, `doas`, etc. as well as a supported package manager. Currently, LURE supports `apt`, `pacman`, `apk`, `dnf`, `yum`, and `zypper`. If a supported package manager exists on your system, it will be detected and used automatically. 13 | 14 | --- 15 | 16 | ## Installation 17 | 18 | ### Install script 19 | 20 | The LURE install script will automatically download and install the appropriate LURE package on your system. To use it, simply run the following command: 21 | 22 | ```bash 23 | curl -fsSL lure.sh/install | bash 24 | ``` 25 | 26 | **IMPORTANT**: This will download and run the script from https://lure.sh/install. Please look through any script you download from the internet (including this one) before running it. 27 | 28 | ### Packages 29 | 30 | Distro packages and binary archives are provided at the latest Gitea release: https://gitea.elara.ws/lure/lure/releases/latest 31 | 32 | LURE is also available on the AUR as [linux-user-repository-bin](https://aur.archlinux.org/packages/linux-user-repository-bin) 33 | 34 | ### Building from source 35 | 36 | To build LURE from source, you'll need Go 1.18 or newer. Once Go is installed, clone this repo and run: 37 | 38 | ```shell 39 | sudo make install 40 | ``` 41 | 42 | --- 43 | 44 | ## Why? 45 | 46 | LURE was created because packaging software for multiple Linux distros can be difficult and error-prone, and installing those packages can be a nightmare for users unless they're available in their distro's official repositories. It automates the process of building and installing unofficial packages. 47 | 48 | --- 49 | 50 | ## Documentation 51 | 52 | The documentation for LURE is in the [docs](docs) directory in this repo. 53 | 54 | --- 55 | 56 | ## Web Interface 57 | 58 | LURE has an open source web interface, licensed under the AGPLv3 (https://gitea.elara.ws/lure/lure-web), and it's available at https://lure.sh/. 59 | 60 | --- 61 | 62 | ## Repositories 63 | 64 | LURE's repos are git repositories that contain a directory for each package, with a `lure.sh` file inside. The `lure.sh` file tells LURE how to build the package and information about it. `lure.sh` scripts are similar to the AUR's PKGBUILD scripts. 65 | 66 | --- 67 | 68 | ## Acknowledgements 69 | 70 | Thanks to the following projects for making LURE possible: 71 | 72 | - https://github.com/mvdan/sh 73 | - https://github.com/go-git/go-git 74 | - https://github.com/mholt/archiver 75 | - https://github.com/goreleaser/nfpm 76 | - https://github.com/charmbracelet/bubbletea 77 | - https://gitlab.com/cznic/sqlite -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lure-sh/lure/5999c1c8e6e569c0a265bda1e55531ffddbada87/assets/logo.png -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/urfave/cli/v2" 26 | "lure.sh/lure/internal/config" 27 | "lure.sh/lure/internal/osutils" 28 | "lure.sh/lure/internal/types" 29 | "lure.sh/lure/pkg/build" 30 | "lure.sh/lure/pkg/loggerctx" 31 | "lure.sh/lure/pkg/manager" 32 | "lure.sh/lure/pkg/repos" 33 | ) 34 | 35 | var buildCmd = &cli.Command{ 36 | Name: "build", 37 | Usage: "Build a local package", 38 | Flags: []cli.Flag{ 39 | &cli.StringFlag{ 40 | Name: "script", 41 | Aliases: []string{"s"}, 42 | Value: "lure.sh", 43 | Usage: "Path to the build script", 44 | }, 45 | &cli.StringFlag{ 46 | Name: "package", 47 | Aliases: []string{"p"}, 48 | Usage: "Name of the package to build and its repo (example: default/go-bin)", 49 | }, 50 | &cli.BoolFlag{ 51 | Name: "clean", 52 | Aliases: []string{"c"}, 53 | Usage: "Build package from scratch even if there's an already built package available", 54 | }, 55 | }, 56 | Action: func(c *cli.Context) error { 57 | ctx := c.Context 58 | log := loggerctx.From(ctx) 59 | 60 | script := c.String("script") 61 | if c.String("package") != "" { 62 | script = filepath.Join(config.GetPaths(ctx).RepoDir, c.String("package"), "lure.sh") 63 | } 64 | 65 | err := repos.Pull(ctx, config.Config(ctx).Repos) 66 | if err != nil { 67 | log.Fatal("Error pulling repositories").Err(err).Send() 68 | } 69 | 70 | mgr := manager.Detect() 71 | if mgr == nil { 72 | log.Fatal("Unable to detect a supported package manager on the system").Send() 73 | } 74 | 75 | pkgPaths, _, err := build.BuildPackage(ctx, types.BuildOpts{ 76 | Script: script, 77 | Manager: mgr, 78 | Clean: c.Bool("clean"), 79 | Interactive: c.Bool("interactive"), 80 | }) 81 | if err != nil { 82 | log.Fatal("Error building package").Err(err).Send() 83 | } 84 | 85 | wd, err := os.Getwd() 86 | if err != nil { 87 | log.Fatal("Error getting working directory").Err(err).Send() 88 | } 89 | 90 | for _, pkgPath := range pkgPaths { 91 | name := filepath.Base(pkgPath) 92 | err = osutils.Move(pkgPath, filepath.Join(wd, name)) 93 | if err != nil { 94 | log.Fatal("Error moving the package").Err(err).Send() 95 | } 96 | } 97 | 98 | return nil 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # LURE Docs 2 | 3 | - [Configuration](configuration.md) 4 | - [Usage](usage.md) 5 | - [Packages](packages) 6 | - [Build Scripts](packages/build-scripts.md) 7 | - [Package Conventions](packages/conventions.md) 8 | - [Adding Packages to LURE's repo](packages/adding-packages.md) -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | This page describes the configuration of LURE 4 | 5 | --- 6 | 7 | ## Table of Contents 8 | 9 | - [Config file](#config-file) 10 | - [rootCmd](#rootcmd) 11 | - [repo](#repo) 12 | 13 | --- 14 | 15 | ## File locations 16 | 17 | | Path | Description 18 | | --: | :-- 19 | | ~/.config/lure/lure.toml | Config file 20 | | ~/.cache/lure/pkgs | here the packages are built and stored 21 | | ~/.cache/lure/repo | here are the git repos with all the `lure.sh` files 22 | | | Example: `~/.cache/lure/repo/default/itd-bin/lure.sh` 23 | 24 | --- 25 | 26 | ## Config file 27 | 28 | ### rootCmd 29 | 30 | The `rootCmd` field in the config specifies which command should be used for privilege elevation. The default value is `sudo`. 31 | 32 | ### repo 33 | 34 | The `repo` array in the config specifies which repos are added to LURE. Each repo must have a name and URL. A repo looks like this in the config: 35 | 36 | ```toml 37 | [[repo]] 38 | name = 'default' 39 | url = 'https://github.com/Elara6331/lure-repo.git' 40 | ``` 41 | 42 | The `default` repo is added by default. Any amount of repos may be added. 43 | 44 | --- 45 | -------------------------------------------------------------------------------- /docs/packages/README.md: -------------------------------------------------------------------------------- 1 | # LURE Docs > Packages 2 | 3 | - [Build Scripts](build-scripts.md) 4 | - [Package Conventions](conventions.md) 5 | - [Adding Packages to LURE's repo](adding-packages.md) -------------------------------------------------------------------------------- /docs/packages/adding-packages.md: -------------------------------------------------------------------------------- 1 | # Adding Packages to LURE's repo 2 | 3 | ## Requirements 4 | 5 | - `go` (1.18+) 6 | - `git` 7 | - `lure-analyzer` 8 | - `go install go.elara.ws/lure-repo-bot/cmd/lure-analyzer@latest` 9 | - `shfmt` 10 | - May be available in distro repos 11 | - `go install mvdan.cc/sh/v3/cmd/shfmt@latest` 12 | 13 | --- 14 | 15 | ## How to test a package 16 | 17 | To test packages you can first create [a `lure.sh` shell file](./build-scripts.md) and then run the `lure build` comand to build the local `lure.sh` file into a package for your distro (more info about the `build` command [here](./usage.md#build)). You can then install this file to your distro and test it. 18 | 19 | ## How to submit a package 20 | 21 | LURE's repo is hosted on Github at https://github.com/Elara6331/lure-repo. In it, there are multiple directories each containing a `lure.sh` file. In order to add a package to LURE's repo, simply create a PR with a [build script](./build-scripts.md) and place it in a directory with the same name as the package. 22 | 23 | Upon submitting the PR, [lure-repo-bot](https://github.com/Elara6331/lure-repo-bot) will pull your PR and analyze it, providing suggestions for fixes as review comments. If there are no problems, the bot will approve your changes. If there are issues, re-request review from the bot after you've finished applying the fixes and it will automatically review the PR again. 24 | 25 | All scripts submitted to the LURE repo should be formatted with `shfmt`. If they are not properly formatted, Github Actions will add suggestions in the "Files Changed" tab of the PR. 26 | 27 | Once your PR is merged, LURE will pull the changed repo and your package will be available for people to install. 28 | -------------------------------------------------------------------------------- /docs/packages/conventions.md: -------------------------------------------------------------------------------- 1 | # Package Conventions 2 | 3 | ## General 4 | 5 | Packages should have the name(s) of what they contain in their `provides` and `conflicts` arrays. That way, they can be installed by users without needing to know the full package name. For example, there are two LURE packages for ITD: `itd-bin`, and `itd-git`. Both of them have provides and conflicts arrays specifying the two commands they install: `itd`, and `itctl`. This means that if a user wants to install ITD, they simply have to type `lure in itd` and LURE will prompt them for which one they want to install. 6 | 7 | ## Binary packages 8 | 9 | Packages that install download and install precompiled binaries should have a `-bin` suffix. 10 | 11 | ## Git packages 12 | 13 | Packages that build and install programs from source code cloned directly from Git should have a `-git` suffix. 14 | 15 | The versions of these packages should consist of the amount of revisions followed by the current revision, separated by a period. For example: `183.80187b0`. Note that unlike the AUR, there is no `r` at the beginning. This is because some package managers refuse to install packages whose version numbers don't start with a digit. 16 | 17 | This version number can be obtained using the following command: 18 | 19 | ```bash 20 | printf "%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" 21 | ``` 22 | 23 | The `version()` function for such packages should use the LURE-provided `git-version` helper command, like so: 24 | 25 | ```bash 26 | version() { 27 | cd "$srcdir/$name" 28 | git-version 29 | } 30 | ``` 31 | 32 | This uses LURE's embedded Git implementation, which ensures that the user doesn't need Git installed on their system in order to install `-git` packages. 33 | 34 | ## Other packages 35 | 36 | Packages that download sources for a specific version of a program should not have any suffix, even if those sources are downloaded from Git. -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Table of Contents 4 | 5 | - [Commands](#commands) 6 | - [install](#install) 7 | - [remove](#remove) 8 | - [upgrade](#upgrade) 9 | - [info](#info) 10 | - [list](#list) 11 | - [build](#build) 12 | - [addrepo](#addrepo) 13 | - [removerepo](#removerepo) 14 | - [refresh](#refresh) 15 | - [fix](#fix) 16 | - [version](#version) 17 | - [Environment Variables](#environment-variables) 18 | - [LURE_DISTRO](#lure_distro) 19 | - [LURE_PKG_FORMAT](#lure_pkg_format) 20 | - [LURE_ARM_VARIANT](#lure_arm_variant) 21 | 22 | --- 23 | 24 | ## Commands 25 | 26 | ### install 27 | 28 | The install command installs a package from the LURE repos. Any packages that aren't found in LURE's repos get forwarded to the system package manager for installation. 29 | 30 | The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard. 31 | 32 | If multiple packages are found, you will be prompted to select which you want to install. 33 | 34 | By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build. 35 | 36 | Examples: 37 | 38 | ```shell 39 | lure in itd-bin # only finds itd-bin 40 | lure in itd # finds itd-bin and itd-git 41 | lure in it% # finds itd-bin, itd-git, and itgui-git 42 | lure in -c itd-bin 43 | ``` 44 | 45 | ### remove 46 | 47 | The remove command is for convenience. All it does is forwards the remove command to the system package manager. 48 | 49 | Example: 50 | 51 | ```shell 52 | lure rm firefox 53 | ``` 54 | 55 | ### upgrade 56 | 57 | The upgrade command looks through the packages installed on your system and sees if any of them match LURE repo packages. If they do, their versions are compared using the `rpmvercmp` algorithm. If LURE repos contain a newer version, the package is upgraded. 58 | 59 | By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build. 60 | 61 | Example: 62 | 63 | ```shell 64 | lure up 65 | ``` 66 | 67 | ### info 68 | 69 | The info command displays information about a package in LURE's repos. 70 | 71 | The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard. 72 | 73 | If multiple packages are found, you will be prompted to select which you want to show. 74 | 75 | Example: 76 | 77 | ```shell 78 | lure info itd-bin # only finds itd-bin 79 | lure info itd # finds itd-bin and itd-git 80 | lure info it% # finds itd-bin, itd-git, and itgui-git 81 | ``` 82 | 83 | ### list 84 | 85 | The list command lists all LURE repo packages as well as their versions 86 | 87 | This command accepts a single optional argument. This argument is a pattern to filter found packages against. 88 | 89 | The pattern does not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard. 90 | 91 | There is a `-I` or `--installed` flag that filters out any packages that are not installed on the system 92 | 93 | Examples: 94 | 95 | ```shell 96 | lure ls # lists all LURE packages 97 | lure ls -I # lists all installed packages 98 | lure ls i% # lists all packages starting with "i" 99 | lure ls %d # lists all packages ending with "d" 100 | lure ls -I i% # lists all installed packages that start with "i" 101 | ``` 102 | 103 | ### build 104 | 105 | The build command builds a package using a `lure.sh` build script in the current directory. The path to the script can be changed with the `-s` flag. 106 | 107 | Example: 108 | 109 | ```shell 110 | lure build 111 | ``` 112 | 113 | ### addrepo 114 | 115 | The addrepo command adds a repository to LURE if it doesn't already exist. The `-n` flag sets the name of the repository, and the `-u` flag is the URL to the repository. Both are required. 116 | 117 | Example: 118 | 119 | ```shell 120 | lure ar -n default -u https://github.com/Elara6331/lure-repo 121 | ``` 122 | 123 | ### removerepo 124 | 125 | The removerepo command removes a repository from LURE and deletes its contents if it exists. The `-n` flag specifies the name of the repo to be deleted. 126 | 127 | Example: 128 | 129 | ```shell 130 | lure rr -n default 131 | ``` 132 | 133 | ### refresh 134 | 135 | The refresh command pulls all changes from all LURE repos that have changed. 136 | 137 | Example: 138 | 139 | ```shell 140 | lure ref 141 | ``` 142 | 143 | ### fix 144 | 145 | The fix command attempts to fix issues with LURE by deleting and rebuilding LURE's cache 146 | 147 | Example: 148 | 149 | ```shell 150 | lure fix 151 | ``` 152 | 153 | ### version 154 | 155 | The version command returns the current LURE version and exits 156 | 157 | Example: 158 | 159 | ```shell 160 | lure version 161 | ``` 162 | 163 | --- 164 | 165 | ## Environment Variables 166 | 167 | ### LURE_DISTRO 168 | 169 | The `LURE_DISTRO` environment variable should be set to the distro for which the package should be built. It tells LURE which overrides to use. Values should be the same as the `ID` field in `/etc/os-release` or `/usr/lib/os-release`. Possible values include: 170 | 171 | - `arch` 172 | - `alpine` 173 | - `opensuse` 174 | - `debian` 175 | 176 | ### LURE_PKG_FORMAT 177 | 178 | The `LURE_PKG_FORMAT` environment variable should be set to the packaging format that should be used. Valid values are: 179 | 180 | - `archlinux` 181 | - `apk` 182 | - `rpm` 183 | - `deb` 184 | 185 | ### LURE_ARM_VARIANT 186 | 187 | The `LURE_ARM_VARIANT` environment variable dictates which ARM variant to build for, if LURE is running on an ARM system. Possible values include: 188 | 189 | - `arm5` 190 | - `arm6` 191 | - `arm7` 192 | 193 | --- 194 | 195 | ## Cross-packaging for other Distributions 196 | 197 | You can create packages for different distributions 198 | setting the environment variables `LURE_DISTRO` and `LURE_PKG_FORMAT` as mentioned above. 199 | 200 | Examples: 201 | 202 | ``` 203 | LURE_DISTRO=arch LURE_PKG_FORMAT=archlinux lure build 204 | LURE_DISTRO=alpine LURE_PKG_FORMAT=apk lure build 205 | LURE_DISTRO=opensuse LURE_PKG_FORMAT=rpm lure build 206 | LURE_DISTRO=debian LURE_PKG_FORMAT=deb lure build 207 | ``` 208 | 209 | --- -------------------------------------------------------------------------------- /fix.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "os" 23 | 24 | "github.com/urfave/cli/v2" 25 | "lure.sh/lure/internal/config" 26 | "lure.sh/lure/internal/db" 27 | "lure.sh/lure/pkg/loggerctx" 28 | "lure.sh/lure/pkg/repos" 29 | ) 30 | 31 | var fixCmd = &cli.Command{ 32 | Name: "fix", 33 | Usage: "Attempt to fix problems with LURE", 34 | Action: func(c *cli.Context) error { 35 | ctx := c.Context 36 | log := loggerctx.From(ctx) 37 | 38 | db.Close() 39 | paths := config.GetPaths(ctx) 40 | 41 | log.Info("Removing cache directory").Send() 42 | 43 | err := os.RemoveAll(paths.CacheDir) 44 | if err != nil { 45 | log.Fatal("Unable to remove cache directory").Err(err).Send() 46 | } 47 | 48 | log.Info("Rebuilding cache").Send() 49 | 50 | err = os.MkdirAll(paths.CacheDir, 0o755) 51 | if err != nil { 52 | log.Fatal("Unable to create new cache directory").Err(err).Send() 53 | } 54 | 55 | err = repos.Pull(ctx, config.Config(ctx).Repos) 56 | if err != nil { 57 | log.Fatal("Error pulling repos").Err(err).Send() 58 | } 59 | 60 | log.Info("Done").Send() 61 | 62 | return nil 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/urfave/cli/v2" 7 | "lure.sh/lure/pkg/gen" 8 | ) 9 | 10 | var genCmd = &cli.Command{ 11 | Name: "generate", 12 | Usage: "Generate a LURE script from a template", 13 | Aliases: []string{"gen"}, 14 | Subcommands: []*cli.Command{ 15 | genPipCmd, 16 | }, 17 | } 18 | 19 | var genPipCmd = &cli.Command{ 20 | Name: "pip", 21 | Usage: "Generate a LURE script for a pip module", 22 | Flags: []cli.Flag{ 23 | &cli.StringFlag{ 24 | Name: "name", 25 | Aliases: []string{"n"}, 26 | Required: true, 27 | }, 28 | &cli.StringFlag{ 29 | Name: "version", 30 | Aliases: []string{"v"}, 31 | Required: true, 32 | }, 33 | &cli.StringFlag{ 34 | Name: "description", 35 | Aliases: []string{"d"}, 36 | }, 37 | }, 38 | Action: func(c *cli.Context) error { 39 | return gen.Pip(os.Stdout, gen.PipOptions{ 40 | Name: c.String("name"), 41 | Version: c.String("version"), 42 | Description: c.String("description"), 43 | }) 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module lure.sh/lure 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.3 6 | 7 | require ( 8 | github.com/AlecAivazis/survey/v2 v2.3.7 9 | github.com/PuerkitoBio/purell v1.2.0 10 | github.com/alecthomas/chroma/v2 v2.9.1 11 | github.com/charmbracelet/bubbles v0.16.1 12 | github.com/charmbracelet/bubbletea v0.24.2 13 | github.com/charmbracelet/lipgloss v0.8.0 14 | github.com/go-git/go-billy/v5 v5.5.0 15 | github.com/go-git/go-git/v5 v5.9.0 16 | github.com/goreleaser/nfpm/v2 v2.33.0 17 | github.com/jmoiron/sqlx v1.3.5 18 | github.com/mattn/go-isatty v0.0.19 19 | github.com/mholt/archiver/v4 v4.0.0-alpha.8 20 | github.com/mitchellh/mapstructure v1.5.0 21 | github.com/muesli/reflow v0.3.0 22 | github.com/pelletier/go-toml/v2 v2.1.0 23 | github.com/schollz/progressbar/v3 v3.13.1 24 | github.com/urfave/cli/v2 v2.25.7 25 | github.com/vmihailenco/msgpack/v5 v5.3.5 26 | go.elara.ws/logger v0.0.0-20230421022458-e80700db2090 27 | go.elara.ws/translate v0.0.0-20230421025926-32ccfcd110e6 28 | go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 29 | golang.org/x/crypto v0.13.0 30 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 31 | golang.org/x/sys v0.12.0 32 | golang.org/x/text v0.13.0 33 | gopkg.in/yaml.v3 v3.0.1 34 | lure.sh/fakeroot v0.0.0-20231024000108-b130d64a68ee 35 | modernc.org/sqlite v1.25.0 36 | mvdan.cc/sh/v3 v3.7.0 37 | ) 38 | 39 | require ( 40 | dario.cat/mergo v1.0.0 // indirect 41 | github.com/AlekSi/pointer v1.2.0 // indirect 42 | github.com/Masterminds/goutils v1.1.1 // indirect 43 | github.com/Masterminds/semver/v3 v3.2.1 // indirect 44 | github.com/Masterminds/sprig/v3 v3.2.3 // indirect 45 | github.com/Microsoft/go-winio v0.6.1 // indirect 46 | github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect 47 | github.com/acomagu/bufpipe v1.0.4 // indirect 48 | github.com/andybalholm/brotli v1.0.4 // indirect 49 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 50 | github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect 51 | github.com/bodgit/plumbing v1.2.0 // indirect 52 | github.com/bodgit/sevenzip v1.3.0 // indirect 53 | github.com/bodgit/windows v1.0.0 // indirect 54 | github.com/cavaliergopher/cpio v1.0.1 // indirect 55 | github.com/cloudflare/circl v1.3.3 // indirect 56 | github.com/connesc/cipherio v0.2.1 // indirect 57 | github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect 58 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 59 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 60 | github.com/dlclark/regexp2 v1.10.0 // indirect 61 | github.com/dsnet/compress v0.0.1 // indirect 62 | github.com/dustin/go-humanize v1.0.1 // indirect 63 | github.com/emirpasic/gods v1.18.1 // indirect 64 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 65 | github.com/gobwas/glob v0.2.3 // indirect 66 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 67 | github.com/golang/snappy v0.0.4 // indirect 68 | github.com/google/rpmpack v0.5.0 // indirect 69 | github.com/google/uuid v1.3.0 // indirect 70 | github.com/gookit/color v1.5.1 // indirect 71 | github.com/goreleaser/chglog v0.5.0 // indirect 72 | github.com/goreleaser/fileglob v1.3.0 // indirect 73 | github.com/hashicorp/errwrap v1.0.0 // indirect 74 | github.com/hashicorp/go-multierror v1.1.1 // indirect 75 | github.com/huandu/xstrings v1.3.3 // indirect 76 | github.com/imdario/mergo v0.3.16 // indirect 77 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 78 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 79 | github.com/kevinburke/ssh_config v1.2.0 // indirect 80 | github.com/klauspost/compress v1.17.0 // indirect 81 | github.com/klauspost/pgzip v1.2.6 // indirect 82 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 83 | github.com/mattn/go-colorable v0.1.2 // indirect 84 | github.com/mattn/go-localereader v0.0.1 // indirect 85 | github.com/mattn/go-runewidth v0.0.15 // indirect 86 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 87 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 88 | github.com/mitchellh/copystructure v1.2.0 // indirect 89 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 90 | github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect 91 | github.com/muesli/cancelreader v0.2.2 // indirect 92 | github.com/muesli/termenv v0.15.2 // indirect 93 | github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect 94 | github.com/pierrec/lz4/v4 v4.1.15 // indirect 95 | github.com/pjbgf/sha1cd v0.3.0 // indirect 96 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 97 | github.com/rivo/uniseg v0.4.4 // indirect 98 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 99 | github.com/sergi/go-diff v1.2.0 // indirect 100 | github.com/shopspring/decimal v1.2.0 // indirect 101 | github.com/skeema/knownhosts v1.2.0 // indirect 102 | github.com/spf13/cast v1.5.1 // indirect 103 | github.com/therootcompany/xz v1.0.1 // indirect 104 | github.com/ulikunitz/xz v0.5.11 // indirect 105 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 106 | github.com/xanzy/ssh-agent v0.3.3 // indirect 107 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 108 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 109 | gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect 110 | go4.org v0.0.0-20200411211856-f5505b9728dd // indirect 111 | golang.org/x/mod v0.12.0 // indirect 112 | golang.org/x/net v0.15.0 // indirect 113 | golang.org/x/sync v0.3.0 // indirect 114 | golang.org/x/term v0.12.0 // indirect 115 | golang.org/x/tools v0.13.0 // indirect 116 | gopkg.in/warnings.v0 v0.1.2 // indirect 117 | lukechampine.com/uint128 v1.2.0 // indirect 118 | modernc.org/cc/v3 v3.40.0 // indirect 119 | modernc.org/ccgo/v3 v3.16.13 // indirect 120 | modernc.org/libc v1.24.1 // indirect 121 | modernc.org/mathutil v1.5.0 // indirect 122 | modernc.org/memory v1.6.0 // indirect 123 | modernc.org/opt v0.1.3 // indirect 124 | modernc.org/strutil v1.1.3 // indirect 125 | modernc.org/token v1.0.1 // indirect 126 | ) 127 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/urfave/cli/v2" 9 | "lure.sh/lure/internal/cpu" 10 | "lure.sh/lure/internal/shutils/helpers" 11 | "lure.sh/lure/pkg/distro" 12 | "lure.sh/lure/pkg/loggerctx" 13 | "mvdan.cc/sh/v3/expand" 14 | "mvdan.cc/sh/v3/interp" 15 | ) 16 | 17 | var helperCmd = &cli.Command{ 18 | Name: "helper", 19 | Usage: "Run a LURE helper command", 20 | ArgsUsage: ``, 21 | Subcommands: []*cli.Command{helperListCmd}, 22 | Flags: []cli.Flag{ 23 | &cli.StringFlag{ 24 | Name: "dest-dir", 25 | Aliases: []string{"d"}, 26 | Usage: "The directory that the install commands will install to", 27 | Value: "dest", 28 | }, 29 | }, 30 | Action: func(c *cli.Context) error { 31 | ctx := c.Context 32 | log := loggerctx.From(ctx) 33 | 34 | if c.Args().Len() < 1 { 35 | cli.ShowSubcommandHelpAndExit(c, 1) 36 | } 37 | 38 | helper, ok := helpers.Helpers[c.Args().First()] 39 | if !ok { 40 | log.Fatal("No such helper command").Str("name", c.Args().First()).Send() 41 | } 42 | 43 | wd, err := os.Getwd() 44 | if err != nil { 45 | log.Fatal("Error getting working directory").Err(err).Send() 46 | } 47 | 48 | info, err := distro.ParseOSRelease(ctx) 49 | if err != nil { 50 | log.Fatal("Error getting working directory").Err(err).Send() 51 | } 52 | 53 | hc := interp.HandlerContext{ 54 | Env: expand.ListEnviron( 55 | "pkgdir="+c.String("dest-dir"), 56 | "DISTRO_ID="+info.ID, 57 | "DISTRO_ID_LIKE="+strings.Join(info.Like, " "), 58 | "ARCH="+cpu.Arch(), 59 | ), 60 | Dir: wd, 61 | Stdin: os.Stdin, 62 | Stdout: os.Stdout, 63 | Stderr: os.Stderr, 64 | } 65 | 66 | return helper(hc, c.Args().First(), c.Args().Slice()[1:]) 67 | }, 68 | CustomHelpTemplate: cli.CommandHelpTemplate, 69 | BashComplete: func(ctx *cli.Context) { 70 | for name := range helpers.Helpers { 71 | fmt.Println(name) 72 | } 73 | }, 74 | } 75 | 76 | var helperListCmd = &cli.Command{ 77 | Name: "list", 78 | Usage: "List all the available helper commands", 79 | Aliases: []string{"ls"}, 80 | Action: func(ctx *cli.Context) error { 81 | for name := range helpers.Helpers { 82 | fmt.Println(name) 83 | } 84 | return nil 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /info.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "os" 24 | 25 | "github.com/urfave/cli/v2" 26 | "lure.sh/lure/internal/cliutils" 27 | "lure.sh/lure/internal/config" 28 | "lure.sh/lure/internal/overrides" 29 | "lure.sh/lure/pkg/distro" 30 | "lure.sh/lure/pkg/loggerctx" 31 | "lure.sh/lure/pkg/repos" 32 | "gopkg.in/yaml.v3" 33 | ) 34 | 35 | var infoCmd = &cli.Command{ 36 | Name: "info", 37 | Usage: "Print information about a package", 38 | Flags: []cli.Flag{ 39 | &cli.BoolFlag{ 40 | Name: "all", 41 | Aliases: []string{"a"}, 42 | Usage: "Show all information, not just for the current distro", 43 | }, 44 | }, 45 | Action: func(c *cli.Context) error { 46 | ctx := c.Context 47 | log := loggerctx.From(ctx) 48 | 49 | args := c.Args() 50 | if args.Len() < 1 { 51 | log.Fatalf("Command info expected at least 1 argument, got %d", args.Len()).Send() 52 | } 53 | 54 | err := repos.Pull(ctx, config.Config(ctx).Repos) 55 | if err != nil { 56 | log.Fatal("Error pulling repositories").Err(err).Send() 57 | } 58 | 59 | found, _, err := repos.FindPkgs(ctx, args.Slice()) 60 | if err != nil { 61 | log.Fatal("Error finding packages").Err(err).Send() 62 | } 63 | 64 | if len(found) == 0 { 65 | os.Exit(1) 66 | } 67 | 68 | pkgs := cliutils.FlattenPkgs(ctx, found, "show", c.Bool("interactive")) 69 | 70 | var names []string 71 | all := c.Bool("all") 72 | 73 | if !all { 74 | info, err := distro.ParseOSRelease(ctx) 75 | if err != nil { 76 | log.Fatal("Error parsing os-release file").Err(err).Send() 77 | } 78 | names, err = overrides.Resolve( 79 | info, 80 | overrides.DefaultOpts. 81 | WithLanguages([]string{config.SystemLang()}), 82 | ) 83 | if err != nil { 84 | log.Fatal("Error resolving overrides").Err(err).Send() 85 | } 86 | } 87 | 88 | for _, pkg := range pkgs { 89 | if !all { 90 | err = yaml.NewEncoder(os.Stdout).Encode(overrides.ResolvePackage(&pkg, names)) 91 | if err != nil { 92 | log.Fatal("Error encoding script variables").Err(err).Send() 93 | } 94 | } else { 95 | err = yaml.NewEncoder(os.Stdout).Encode(pkg) 96 | if err != nil { 97 | log.Fatal("Error encoding script variables").Err(err).Send() 98 | } 99 | } 100 | 101 | fmt.Println("---") 102 | } 103 | 104 | return nil 105 | }, 106 | } 107 | -------------------------------------------------------------------------------- /install.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | 24 | "github.com/urfave/cli/v2" 25 | "lure.sh/lure/internal/cliutils" 26 | "lure.sh/lure/internal/config" 27 | "lure.sh/lure/internal/db" 28 | "lure.sh/lure/internal/types" 29 | "lure.sh/lure/pkg/build" 30 | "lure.sh/lure/pkg/loggerctx" 31 | "lure.sh/lure/pkg/manager" 32 | "lure.sh/lure/pkg/repos" 33 | ) 34 | 35 | var installCmd = &cli.Command{ 36 | Name: "install", 37 | Usage: "Install a new package", 38 | Aliases: []string{"in"}, 39 | Flags: []cli.Flag{ 40 | &cli.BoolFlag{ 41 | Name: "clean", 42 | Aliases: []string{"c"}, 43 | Usage: "Build package from scratch even if there's an already built package available", 44 | }, 45 | }, 46 | Action: func(c *cli.Context) error { 47 | ctx := c.Context 48 | log := loggerctx.From(ctx) 49 | 50 | args := c.Args() 51 | if args.Len() < 1 { 52 | log.Fatalf("Command install expected at least 1 argument, got %d", args.Len()).Send() 53 | } 54 | 55 | mgr := manager.Detect() 56 | if mgr == nil { 57 | log.Fatal("Unable to detect a supported package manager on the system").Send() 58 | } 59 | 60 | err := repos.Pull(ctx, config.Config(ctx).Repos) 61 | if err != nil { 62 | log.Fatal("Error pulling repositories").Err(err).Send() 63 | } 64 | 65 | found, notFound, err := repos.FindPkgs(ctx, args.Slice()) 66 | if err != nil { 67 | log.Fatal("Error finding packages").Err(err).Send() 68 | } 69 | 70 | pkgs := cliutils.FlattenPkgs(ctx, found, "install", c.Bool("interactive")) 71 | build.InstallPkgs(ctx, pkgs, notFound, types.BuildOpts{ 72 | Manager: mgr, 73 | Clean: c.Bool("clean"), 74 | Interactive: c.Bool("interactive"), 75 | }) 76 | return nil 77 | }, 78 | BashComplete: func(c *cli.Context) { 79 | log := loggerctx.From(c.Context) 80 | result, err := db.GetPkgs(c.Context, "true") 81 | if err != nil { 82 | log.Fatal("Error getting packages").Err(err).Send() 83 | } 84 | defer result.Close() 85 | 86 | for result.Next() { 87 | var pkg db.Package 88 | err = result.StructScan(&pkg) 89 | if err != nil { 90 | log.Fatal("Error iterating over packages").Err(err).Send() 91 | } 92 | 93 | fmt.Println(pkg.Name) 94 | } 95 | }, 96 | } 97 | 98 | var removeCmd = &cli.Command{ 99 | Name: "remove", 100 | Usage: "Remove an installed package", 101 | Aliases: []string{"rm"}, 102 | Action: func(c *cli.Context) error { 103 | log := loggerctx.From(c.Context) 104 | 105 | args := c.Args() 106 | if args.Len() < 1 { 107 | log.Fatalf("Command remove expected at least 1 argument, got %d", args.Len()).Send() 108 | } 109 | 110 | mgr := manager.Detect() 111 | if mgr == nil { 112 | log.Fatal("Unable to detect a supported package manager on the system").Send() 113 | } 114 | 115 | err := mgr.Remove(nil, c.Args().Slice()...) 116 | if err != nil { 117 | log.Fatal("Error removing packages").Err(err).Send() 118 | } 119 | 120 | return nil 121 | }, 122 | } 123 | -------------------------------------------------------------------------------- /internal/cliutils/prompt.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package cliutils 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "strings" 25 | 26 | "github.com/AlecAivazis/survey/v2" 27 | "lure.sh/lure/internal/config" 28 | "lure.sh/lure/internal/db" 29 | "lure.sh/lure/internal/pager" 30 | "lure.sh/lure/internal/translations" 31 | "lure.sh/lure/pkg/loggerctx" 32 | ) 33 | 34 | // YesNoPrompt asks the user a yes or no question, using def as the default answer 35 | func YesNoPrompt(ctx context.Context, msg string, interactive, def bool) (bool, error) { 36 | if interactive { 37 | var answer bool 38 | err := survey.AskOne( 39 | &survey.Confirm{ 40 | Message: translations.Translator(ctx).TranslateTo(msg, config.Language(ctx)), 41 | Default: def, 42 | }, 43 | &answer, 44 | ) 45 | return answer, err 46 | } else { 47 | return def, nil 48 | } 49 | } 50 | 51 | // PromptViewScript asks the user if they'd like to see a script, 52 | // shows it if they answer yes, then asks if they'd still like to 53 | // continue, and exits if they answer no. 54 | func PromptViewScript(ctx context.Context, script, name, style string, interactive bool) error { 55 | log := loggerctx.From(ctx) 56 | 57 | if !interactive { 58 | return nil 59 | } 60 | 61 | scriptPrompt := translations.Translator(ctx).TranslateTo("Would you like to view the build script for", config.Language(ctx)) + " " + name 62 | view, err := YesNoPrompt(ctx, scriptPrompt, interactive, false) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if view { 68 | err = ShowScript(script, name, style) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | cont, err := YesNoPrompt(ctx, "Would you still like to continue?", interactive, false) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | if !cont { 79 | log.Fatal(translations.Translator(ctx).TranslateTo("User chose not to continue after reading script", config.Language(ctx))).Send() 80 | } 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // ShowScript uses the built-in pager to display a script at a 87 | // given path, in the given syntax highlighting style. 88 | func ShowScript(path, name, style string) error { 89 | scriptFl, err := os.Open(path) 90 | if err != nil { 91 | return err 92 | } 93 | defer scriptFl.Close() 94 | 95 | str, err := pager.SyntaxHighlightBash(scriptFl, style) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | pgr := pager.New(name, str) 101 | return pgr.Run() 102 | } 103 | 104 | // FlattenPkgs attempts to flatten the a map of slices of packages into a single slice 105 | // of packages by prompting the user if multiple packages match. 106 | func FlattenPkgs(ctx context.Context, found map[string][]db.Package, verb string, interactive bool) []db.Package { 107 | log := loggerctx.From(ctx) 108 | var outPkgs []db.Package 109 | for _, pkgs := range found { 110 | if len(pkgs) > 1 && interactive { 111 | choice, err := PkgPrompt(ctx, pkgs, verb, interactive) 112 | if err != nil { 113 | log.Fatal("Error prompting for choice of package").Send() 114 | } 115 | outPkgs = append(outPkgs, choice) 116 | } else if len(pkgs) == 1 || !interactive { 117 | outPkgs = append(outPkgs, pkgs[0]) 118 | } 119 | } 120 | return outPkgs 121 | } 122 | 123 | // PkgPrompt asks the user to choose between multiple packages. 124 | func PkgPrompt(ctx context.Context, options []db.Package, verb string, interactive bool) (db.Package, error) { 125 | if !interactive { 126 | return options[0], nil 127 | } 128 | 129 | names := make([]string, len(options)) 130 | for i, option := range options { 131 | names[i] = option.Repository + "/" + option.Name + " " + option.Version 132 | } 133 | 134 | prompt := &survey.Select{ 135 | Options: names, 136 | Message: translations.Translator(ctx).TranslateTo("Choose which package to "+verb, config.Language(ctx)), 137 | } 138 | 139 | var choice int 140 | err := survey.AskOne(prompt, &choice) 141 | if err != nil { 142 | return db.Package{}, err 143 | } 144 | 145 | return options[choice], nil 146 | } 147 | 148 | // ChooseOptDepends asks the user to choose between multiple optional dependencies. 149 | // The user may choose multiple items. 150 | func ChooseOptDepends(ctx context.Context, options []string, verb string, interactive bool) ([]string, error) { 151 | if !interactive { 152 | return []string{}, nil 153 | } 154 | 155 | prompt := &survey.MultiSelect{ 156 | Options: options, 157 | Message: translations.Translator(ctx).TranslateTo("Choose which optional package(s) to install", config.Language(ctx)), 158 | } 159 | 160 | var choices []int 161 | err := survey.AskOne(prompt, &choices) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | out := make([]string, len(choices)) 167 | for i, choiceIndex := range choices { 168 | out[i], _, _ = strings.Cut(options[choiceIndex], ": ") 169 | } 170 | 171 | return out, nil 172 | } 173 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package config 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "sync" 25 | 26 | "github.com/pelletier/go-toml/v2" 27 | "lure.sh/lure/internal/types" 28 | "lure.sh/lure/pkg/loggerctx" 29 | ) 30 | 31 | var defaultConfig = &types.Config{ 32 | RootCmd: "sudo", 33 | PagerStyle: "native", 34 | IgnorePkgUpdates: []string{}, 35 | Repos: []types.Repo{ 36 | { 37 | Name: "default", 38 | URL: "https://github.com/lure-sh/lure-repo.git", 39 | }, 40 | }, 41 | } 42 | 43 | var ( 44 | configMtx sync.Mutex 45 | config *types.Config 46 | ) 47 | 48 | // Config returns a LURE configuration struct. 49 | // The first time it's called, it'll load the config from a file. 50 | // Subsequent calls will just return the same value. 51 | func Config(ctx context.Context) *types.Config { 52 | configMtx.Lock() 53 | defer configMtx.Unlock() 54 | log := loggerctx.From(ctx) 55 | 56 | if config == nil { 57 | cfgFl, err := os.Open(GetPaths(ctx).ConfigPath) 58 | if err != nil { 59 | log.Warn("Error opening config file, using defaults").Err(err).Send() 60 | return defaultConfig 61 | } 62 | defer cfgFl.Close() 63 | 64 | // Copy the default configuration into config 65 | defCopy := *defaultConfig 66 | config = &defCopy 67 | config.Repos = nil 68 | 69 | err = toml.NewDecoder(cfgFl).Decode(config) 70 | if err != nil { 71 | log.Warn("Error decoding config file, using defaults").Err(err).Send() 72 | // Set config back to nil so that we try again next time 73 | config = nil 74 | return defaultConfig 75 | } 76 | } 77 | 78 | return config 79 | } 80 | -------------------------------------------------------------------------------- /internal/config/lang.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package config 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "strings" 25 | "sync" 26 | 27 | "lure.sh/lure/pkg/loggerctx" 28 | "golang.org/x/text/language" 29 | ) 30 | 31 | var ( 32 | langMtx sync.Mutex 33 | lang language.Tag 34 | langSet bool 35 | ) 36 | 37 | // Language returns the system language. 38 | // The first time it's called, it'll detect the langauge based on 39 | // the $LANG environment variable. 40 | // Subsequent calls will just return the same value. 41 | func Language(ctx context.Context) language.Tag { 42 | langMtx.Lock() 43 | defer langMtx.Unlock() 44 | log := loggerctx.From(ctx) 45 | if !langSet { 46 | syslang := SystemLang() 47 | tag, err := language.Parse(syslang) 48 | if err != nil { 49 | log.Fatal("Error parsing system language").Err(err).Send() 50 | } 51 | base, _ := tag.Base() 52 | lang = language.Make(base.String()) 53 | langSet = true 54 | } 55 | return lang 56 | } 57 | 58 | // SystemLang returns the system language based on 59 | // the $LANG environment variable. 60 | func SystemLang() string { 61 | lang := os.Getenv("LANG") 62 | lang, _, _ = strings.Cut(lang, ".") 63 | if lang == "" || lang == "C" { 64 | lang = "en" 65 | } 66 | return lang 67 | } 68 | -------------------------------------------------------------------------------- /internal/config/paths.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package config 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "path/filepath" 25 | "sync" 26 | 27 | "github.com/pelletier/go-toml/v2" 28 | "lure.sh/lure/pkg/loggerctx" 29 | ) 30 | 31 | // Paths contains various paths used by LURE 32 | type Paths struct { 33 | ConfigDir string 34 | ConfigPath string 35 | CacheDir string 36 | RepoDir string 37 | PkgsDir string 38 | DBPath string 39 | } 40 | 41 | var ( 42 | pathsMtx sync.Mutex 43 | paths *Paths 44 | ) 45 | 46 | // GetPaths returns a Paths struct. 47 | // The first time it's called, it'll generate the struct 48 | // using information from the system. 49 | // Subsequent calls will return the same value. 50 | func GetPaths(ctx context.Context) *Paths { 51 | pathsMtx.Lock() 52 | defer pathsMtx.Unlock() 53 | 54 | log := loggerctx.From(ctx) 55 | if paths == nil { 56 | paths = &Paths{} 57 | 58 | cfgDir, err := os.UserConfigDir() 59 | if err != nil { 60 | log.Fatal("Unable to detect user config directory").Err(err).Send() 61 | } 62 | 63 | paths.ConfigDir = filepath.Join(cfgDir, "lure") 64 | 65 | err = os.MkdirAll(paths.ConfigDir, 0o755) 66 | if err != nil { 67 | log.Fatal("Unable to create LURE config directory").Err(err).Send() 68 | } 69 | 70 | paths.ConfigPath = filepath.Join(paths.ConfigDir, "lure.toml") 71 | 72 | if _, err := os.Stat(paths.ConfigPath); err != nil { 73 | cfgFl, err := os.Create(paths.ConfigPath) 74 | if err != nil { 75 | log.Fatal("Unable to create LURE config file").Err(err).Send() 76 | } 77 | 78 | err = toml.NewEncoder(cfgFl).Encode(&defaultConfig) 79 | if err != nil { 80 | log.Fatal("Error encoding default configuration").Err(err).Send() 81 | } 82 | 83 | cfgFl.Close() 84 | } 85 | 86 | cacheDir, err := os.UserCacheDir() 87 | if err != nil { 88 | log.Fatal("Unable to detect cache directory").Err(err).Send() 89 | } 90 | 91 | paths.CacheDir = filepath.Join(cacheDir, "lure") 92 | paths.RepoDir = filepath.Join(paths.CacheDir, "repo") 93 | paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs") 94 | 95 | err = os.MkdirAll(paths.RepoDir, 0o755) 96 | if err != nil { 97 | log.Fatal("Unable to create repo cache directory").Err(err).Send() 98 | } 99 | 100 | err = os.MkdirAll(paths.PkgsDir, 0o755) 101 | if err != nil { 102 | log.Fatal("Unable to create package cache directory").Err(err).Send() 103 | } 104 | 105 | paths.DBPath = filepath.Join(paths.CacheDir, "db") 106 | } 107 | return paths 108 | } 109 | -------------------------------------------------------------------------------- /internal/config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // Version contains the version of LURE. If the version 4 | // isn't known, it'll be set to "unknown" 5 | var Version = "unknown" 6 | -------------------------------------------------------------------------------- /internal/cpu/cpu.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package cpu 20 | 21 | import ( 22 | "os" 23 | "runtime" 24 | "strconv" 25 | "strings" 26 | 27 | "golang.org/x/exp/slices" 28 | "golang.org/x/sys/cpu" 29 | ) 30 | 31 | // armVariant checks which variant of ARM lure is running 32 | // on, by using the same detection method as Go itself 33 | func armVariant() string { 34 | armEnv := os.Getenv("LURE_ARM_VARIANT") 35 | // ensure value has "arm" prefix, such as arm5 or arm6 36 | if strings.HasPrefix(armEnv, "arm") { 37 | return armEnv 38 | } 39 | 40 | if cpu.ARM.HasVFPv3 { 41 | return "arm7" 42 | } else if cpu.ARM.HasVFP { 43 | return "arm6" 44 | } else { 45 | return "arm5" 46 | } 47 | } 48 | 49 | // Arch returns the canonical CPU architecture of the system 50 | func Arch() string { 51 | arch := os.Getenv("LURE_ARCH") 52 | if arch == "" { 53 | arch = runtime.GOARCH 54 | } 55 | if arch == "arm" { 56 | arch = armVariant() 57 | } 58 | return arch 59 | } 60 | 61 | func IsCompatibleWith(target string, list []string) bool { 62 | if target == "all" || slices.Contains(list, "all") { 63 | return true 64 | } 65 | 66 | for _, arch := range list { 67 | if strings.HasPrefix(target, "arm") && strings.HasPrefix(arch, "arm") { 68 | targetVer, err := getARMVersion(target) 69 | if err != nil { 70 | return false 71 | } 72 | 73 | archVer, err := getARMVersion(arch) 74 | if err != nil { 75 | return false 76 | } 77 | 78 | if targetVer >= archVer { 79 | return true 80 | } 81 | } 82 | 83 | if target == arch { 84 | return true 85 | } 86 | } 87 | 88 | return false 89 | } 90 | 91 | func CompatibleArches(arch string) ([]string, error) { 92 | if strings.HasPrefix(arch, "arm") { 93 | ver, err := getARMVersion(arch) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | if ver > 5 { 99 | var out []string 100 | for i := ver; i >= 5; i-- { 101 | out = append(out, "arm"+strconv.Itoa(i)) 102 | } 103 | return out, nil 104 | } 105 | } 106 | 107 | return []string{arch}, nil 108 | } 109 | 110 | func getARMVersion(arch string) (int, error) { 111 | // Extract the version number from ARM architecture 112 | version := strings.TrimPrefix(arch, "arm") 113 | if version == "" { 114 | return 5, nil // Default to arm5 if version is not specified 115 | } 116 | return strconv.Atoi(version) 117 | } 118 | -------------------------------------------------------------------------------- /internal/db/db_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package db_test 20 | 21 | import ( 22 | "reflect" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/jmoiron/sqlx" 27 | "lure.sh/lure/internal/db" 28 | ) 29 | 30 | var testPkg = db.Package{ 31 | Name: "test", 32 | Version: "0.0.1", 33 | Release: 1, 34 | Epoch: 2, 35 | Description: db.NewJSON(map[string]string{ 36 | "en": "Test package", 37 | "ru": "Проверочный пакет", 38 | }), 39 | Homepage: db.NewJSON(map[string]string{ 40 | "en": "https://lure.sh/", 41 | }), 42 | Maintainer: db.NewJSON(map[string]string{ 43 | "en": "Elara Musayelyan ", 44 | "ru": "Элара Мусаелян ", 45 | }), 46 | Architectures: db.NewJSON([]string{"arm64", "amd64"}), 47 | Licenses: db.NewJSON([]string{"GPL-3.0-or-later"}), 48 | Provides: db.NewJSON([]string{"test"}), 49 | Conflicts: db.NewJSON([]string{"test"}), 50 | Replaces: db.NewJSON([]string{"test-old"}), 51 | Depends: db.NewJSON(map[string][]string{ 52 | "": {"sudo"}, 53 | }), 54 | BuildDepends: db.NewJSON(map[string][]string{ 55 | "": {"golang"}, 56 | "arch": {"go"}, 57 | }), 58 | Repository: "default", 59 | } 60 | 61 | func TestInit(t *testing.T) { 62 | _, err := db.Open(":memory:") 63 | if err != nil { 64 | t.Fatalf("Expected no error, got %s", err) 65 | } 66 | defer db.Close() 67 | 68 | _, err = db.DB().Exec("SELECT * FROM pkgs") 69 | if err != nil { 70 | t.Fatalf("Expected no error, got %s", err) 71 | } 72 | 73 | ver, ok := db.GetVersion() 74 | if !ok { 75 | t.Errorf("Expected version to be present") 76 | } else if ver != db.CurrentVersion { 77 | t.Errorf("Expected version %d, got %d", db.CurrentVersion, ver) 78 | } 79 | } 80 | 81 | func TestInsertPackage(t *testing.T) { 82 | _, err := db.Open(":memory:") 83 | if err != nil { 84 | t.Fatalf("Expected no error, got %s", err) 85 | } 86 | defer db.Close() 87 | 88 | err = db.InsertPackage(testPkg) 89 | if err != nil { 90 | t.Fatalf("Expected no error, got %s", err) 91 | } 92 | 93 | dbPkg := db.Package{} 94 | err = sqlx.Get(db.DB(), &dbPkg, "SELECT * FROM pkgs WHERE name = 'test' AND repository = 'default'") 95 | if err != nil { 96 | t.Fatalf("Expected no error, got %s", err) 97 | } 98 | 99 | if !reflect.DeepEqual(testPkg, dbPkg) { 100 | t.Errorf("Expected test package to be the same as database package") 101 | } 102 | } 103 | 104 | func TestGetPkgs(t *testing.T) { 105 | _, err := db.Open(":memory:") 106 | if err != nil { 107 | t.Fatalf("Expected no error, got %s", err) 108 | } 109 | defer db.Close() 110 | 111 | x1 := testPkg 112 | x1.Name = "x1" 113 | x2 := testPkg 114 | x2.Name = "x2" 115 | 116 | err = db.InsertPackage(x1) 117 | if err != nil { 118 | t.Errorf("Expected no error, got %s", err) 119 | } 120 | 121 | err = db.InsertPackage(x2) 122 | if err != nil { 123 | t.Errorf("Expected no error, got %s", err) 124 | } 125 | 126 | result, err := db.GetPkgs("name LIKE 'x%'") 127 | if err != nil { 128 | t.Fatalf("Expected no error, got %s", err) 129 | } 130 | 131 | for result.Next() { 132 | var dbPkg db.Package 133 | err = result.StructScan(&dbPkg) 134 | if err != nil { 135 | t.Errorf("Expected no error, got %s", err) 136 | } 137 | 138 | if !strings.HasPrefix(dbPkg.Name, "x") { 139 | t.Errorf("Expected package name to start with 'x', got %s", dbPkg.Name) 140 | } 141 | } 142 | } 143 | 144 | func TestGetPkg(t *testing.T) { 145 | _, err := db.Open(":memory:") 146 | if err != nil { 147 | t.Fatalf("Expected no error, got %s", err) 148 | } 149 | defer db.Close() 150 | 151 | x1 := testPkg 152 | x1.Name = "x1" 153 | x2 := testPkg 154 | x2.Name = "x2" 155 | 156 | err = db.InsertPackage(x1) 157 | if err != nil { 158 | t.Errorf("Expected no error, got %s", err) 159 | } 160 | 161 | err = db.InsertPackage(x2) 162 | if err != nil { 163 | t.Errorf("Expected no error, got %s", err) 164 | } 165 | 166 | pkg, err := db.GetPkg("name LIKE 'x%' ORDER BY name") 167 | if err != nil { 168 | t.Fatalf("Expected no error, got %s", err) 169 | } 170 | 171 | if pkg.Name != "x1" { 172 | t.Errorf("Expected x1 package, got %s", pkg.Name) 173 | } 174 | 175 | if !reflect.DeepEqual(*pkg, x1) { 176 | t.Errorf("Expected x1 to be %v, got %v", x1, *pkg) 177 | } 178 | } 179 | 180 | func TestDeletePkgs(t *testing.T) { 181 | _, err := db.Open(":memory:") 182 | if err != nil { 183 | t.Fatalf("Expected no error, got %s", err) 184 | } 185 | defer db.Close() 186 | 187 | x1 := testPkg 188 | x1.Name = "x1" 189 | x2 := testPkg 190 | x2.Name = "x2" 191 | 192 | err = db.InsertPackage(x1) 193 | if err != nil { 194 | t.Errorf("Expected no error, got %s", err) 195 | } 196 | 197 | err = db.InsertPackage(x2) 198 | if err != nil { 199 | t.Errorf("Expected no error, got %s", err) 200 | } 201 | 202 | err = db.DeletePkgs("name = 'x1'") 203 | if err != nil { 204 | t.Errorf("Expected no error, got %s", err) 205 | } 206 | 207 | var dbPkg db.Package 208 | err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE name LIKE 'x%' ORDER BY name LIMIT 1;") 209 | if err != nil { 210 | t.Errorf("Expected no error, got %s", err) 211 | } 212 | 213 | if dbPkg.Name != "x2" { 214 | t.Errorf("Expected x2 package, got %s", dbPkg.Name) 215 | } 216 | } 217 | 218 | func TestJsonArrayContains(t *testing.T) { 219 | _, err := db.Open(":memory:") 220 | if err != nil { 221 | t.Fatalf("Expected no error, got %s", err) 222 | } 223 | defer db.Close() 224 | 225 | x1 := testPkg 226 | x1.Name = "x1" 227 | x2 := testPkg 228 | x2.Name = "x2" 229 | x2.Provides.Val = append(x2.Provides.Val, "x") 230 | 231 | err = db.InsertPackage(x1) 232 | if err != nil { 233 | t.Errorf("Expected no error, got %s", err) 234 | } 235 | 236 | err = db.InsertPackage(x2) 237 | if err != nil { 238 | t.Errorf("Expected no error, got %s", err) 239 | } 240 | 241 | var dbPkg db.Package 242 | err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE json_array_contains(provides, 'x');") 243 | if err != nil { 244 | t.Fatalf("Expected no error, got %s", err) 245 | } 246 | 247 | if dbPkg.Name != "x2" { 248 | t.Errorf("Expected x2 package, got %s", dbPkg.Name) 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /internal/dl/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package dl 20 | 21 | import ( 22 | "bytes" 23 | "context" 24 | "io" 25 | "mime" 26 | "net/http" 27 | "net/url" 28 | "os" 29 | "path" 30 | "path/filepath" 31 | "strings" 32 | "time" 33 | 34 | "github.com/mholt/archiver/v4" 35 | "github.com/schollz/progressbar/v3" 36 | "lure.sh/lure/internal/shutils/handlers" 37 | ) 38 | 39 | // FileDownloader downloads files using HTTP 40 | type FileDownloader struct{} 41 | 42 | // Name always returns "file" 43 | func (FileDownloader) Name() string { 44 | return "file" 45 | } 46 | 47 | // MatchURL always returns true, as FileDownloader 48 | // is used as a fallback if nothing else matches 49 | func (FileDownloader) MatchURL(string) bool { 50 | return true 51 | } 52 | 53 | // Download downloads a file using HTTP. If the file is 54 | // compressed using a supported format, it will be extracted 55 | func (FileDownloader) Download(opts Options) (Type, string, error) { 56 | u, err := url.Parse(opts.URL) 57 | if err != nil { 58 | return 0, "", err 59 | } 60 | 61 | query := u.Query() 62 | 63 | name := query.Get("~name") 64 | query.Del("~name") 65 | 66 | archive := query.Get("~archive") 67 | query.Del("~archive") 68 | 69 | u.RawQuery = query.Encode() 70 | 71 | var r io.ReadCloser 72 | var size int64 73 | if u.Scheme == "local" { 74 | localFl, err := os.Open(filepath.Join(opts.LocalDir, u.Path)) 75 | if err != nil { 76 | return 0, "", err 77 | } 78 | fi, err := localFl.Stat() 79 | if err != nil { 80 | return 0, "", err 81 | } 82 | size = fi.Size() 83 | if name == "" { 84 | name = fi.Name() 85 | } 86 | r = localFl 87 | } else { 88 | res, err := http.Get(u.String()) 89 | if err != nil { 90 | return 0, "", err 91 | } 92 | size = res.ContentLength 93 | if name == "" { 94 | name = getFilename(res) 95 | } 96 | r = res.Body 97 | } 98 | defer r.Close() 99 | 100 | opts.PostprocDisabled = archive == "false" 101 | 102 | path := filepath.Join(opts.Destination, name) 103 | fl, err := os.Create(path) 104 | if err != nil { 105 | return 0, "", err 106 | } 107 | defer fl.Close() 108 | 109 | var bar io.WriteCloser 110 | if opts.Progress != nil { 111 | bar = progressbar.NewOptions64( 112 | size, 113 | progressbar.OptionSetDescription(name), 114 | progressbar.OptionSetWriter(opts.Progress), 115 | progressbar.OptionShowBytes(true), 116 | progressbar.OptionSetWidth(10), 117 | progressbar.OptionThrottle(65*time.Millisecond), 118 | progressbar.OptionShowCount(), 119 | progressbar.OptionOnCompletion(func() { 120 | _, _ = io.WriteString(opts.Progress, "\n") 121 | }), 122 | progressbar.OptionSpinnerType(14), 123 | progressbar.OptionFullWidth(), 124 | progressbar.OptionSetRenderBlankState(true), 125 | ) 126 | defer bar.Close() 127 | } else { 128 | bar = handlers.NopRWC{} 129 | } 130 | 131 | h, err := opts.NewHash() 132 | if err != nil { 133 | return 0, "", err 134 | } 135 | 136 | var w io.Writer 137 | if opts.Hash != nil { 138 | w = io.MultiWriter(fl, h, bar) 139 | } else { 140 | w = io.MultiWriter(fl, bar) 141 | } 142 | 143 | _, err = io.Copy(w, r) 144 | if err != nil { 145 | return 0, "", err 146 | } 147 | r.Close() 148 | 149 | if opts.Hash != nil { 150 | sum := h.Sum(nil) 151 | if !bytes.Equal(sum, opts.Hash) { 152 | return 0, "", ErrChecksumMismatch 153 | } 154 | } 155 | 156 | if opts.PostprocDisabled { 157 | return TypeFile, name, nil 158 | } 159 | 160 | _, err = fl.Seek(0, io.SeekStart) 161 | if err != nil { 162 | return 0, "", err 163 | } 164 | 165 | format, ar, err := archiver.Identify(name, fl) 166 | if err == archiver.ErrNoMatch { 167 | return TypeFile, name, nil 168 | } else if err != nil { 169 | return 0, "", err 170 | } 171 | 172 | err = extractFile(ar, format, name, opts) 173 | if err != nil { 174 | return 0, "", err 175 | } 176 | 177 | err = os.Remove(path) 178 | return TypeDir, "", err 179 | } 180 | 181 | // extractFile extracts an archive or decompresses a file 182 | func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) { 183 | fname := format.Name() 184 | 185 | switch format := format.(type) { 186 | case archiver.Extractor: 187 | err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error { 188 | fr, err := f.Open() 189 | if err != nil { 190 | return err 191 | } 192 | defer fr.Close() 193 | fi, err := f.Stat() 194 | if err != nil { 195 | return err 196 | } 197 | fm := fi.Mode() 198 | 199 | path := filepath.Join(opts.Destination, f.NameInArchive) 200 | 201 | err = os.MkdirAll(filepath.Dir(path), 0o755) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | if f.IsDir() { 207 | err = os.Mkdir(path, 0o755) 208 | if err != nil { 209 | return err 210 | } 211 | } else { 212 | outFl, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fm.Perm()) 213 | if err != nil { 214 | return err 215 | } 216 | defer outFl.Close() 217 | 218 | _, err = io.Copy(outFl, fr) 219 | return err 220 | } 221 | return nil 222 | }) 223 | if err != nil { 224 | return err 225 | } 226 | case archiver.Decompressor: 227 | rc, err := format.OpenReader(r) 228 | if err != nil { 229 | return err 230 | } 231 | defer rc.Close() 232 | 233 | path := filepath.Join(opts.Destination, name) 234 | path = strings.TrimSuffix(path, fname) 235 | 236 | outFl, err := os.Create(path) 237 | if err != nil { 238 | return err 239 | } 240 | 241 | _, err = io.Copy(outFl, rc) 242 | if err != nil { 243 | return err 244 | } 245 | } 246 | 247 | return nil 248 | } 249 | 250 | // getFilename attempts to parse the Content-Disposition 251 | // HTTP response header and extract a filename. If the 252 | // header does not exist, it will use the last element 253 | // of the path. 254 | func getFilename(res *http.Response) (name string) { 255 | _, params, err := mime.ParseMediaType(res.Header.Get("Content-Disposition")) 256 | if err != nil { 257 | return path.Base(res.Request.URL.Path) 258 | } 259 | if filename, ok := params["filename"]; ok { 260 | return filename 261 | } else { 262 | return path.Base(res.Request.URL.Path) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /internal/dl/git.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package dl 20 | 21 | import ( 22 | "errors" 23 | "net/url" 24 | "path" 25 | "strconv" 26 | "strings" 27 | 28 | "github.com/go-git/go-git/v5" 29 | "github.com/go-git/go-git/v5/config" 30 | "github.com/go-git/go-git/v5/plumbing" 31 | ) 32 | 33 | // GitDownloader downloads Git repositories 34 | type GitDownloader struct{} 35 | 36 | // Name always returns "git" 37 | func (GitDownloader) Name() string { 38 | return "git" 39 | } 40 | 41 | // MatchURL matches any URLs that start with "git+" 42 | func (GitDownloader) MatchURL(u string) bool { 43 | return strings.HasPrefix(u, "git+") 44 | } 45 | 46 | // Download uses git to clone the repository from the specified URL. 47 | // It allows specifying the revision, depth and recursion options 48 | // via query string 49 | func (GitDownloader) Download(opts Options) (Type, string, error) { 50 | u, err := url.Parse(opts.URL) 51 | if err != nil { 52 | return 0, "", err 53 | } 54 | u.Scheme = strings.TrimPrefix(u.Scheme, "git+") 55 | 56 | query := u.Query() 57 | 58 | rev := query.Get("~rev") 59 | query.Del("~rev") 60 | 61 | name := query.Get("~name") 62 | query.Del("~name") 63 | 64 | depthStr := query.Get("~depth") 65 | query.Del("~depth") 66 | 67 | recursive := query.Get("~recursive") 68 | query.Del("~recursive") 69 | 70 | u.RawQuery = query.Encode() 71 | 72 | depth := 0 73 | if depthStr != "" { 74 | depth, err = strconv.Atoi(depthStr) 75 | if err != nil { 76 | return 0, "", err 77 | } 78 | } 79 | 80 | co := &git.CloneOptions{ 81 | URL: u.String(), 82 | Depth: depth, 83 | Progress: opts.Progress, 84 | RecurseSubmodules: git.NoRecurseSubmodules, 85 | } 86 | 87 | if recursive == "true" { 88 | co.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth 89 | } 90 | 91 | r, err := git.PlainClone(opts.Destination, false, co) 92 | if err != nil { 93 | return 0, "", err 94 | } 95 | 96 | err = r.Fetch(&git.FetchOptions{ 97 | RefSpecs: []config.RefSpec{"+refs/*:refs/*"}, 98 | }) 99 | if err != git.NoErrAlreadyUpToDate && err != nil { 100 | return 0, "", err 101 | } 102 | 103 | if rev != "" { 104 | h, err := r.ResolveRevision(plumbing.Revision(rev)) 105 | if err != nil { 106 | return 0, "", err 107 | } 108 | 109 | w, err := r.Worktree() 110 | if err != nil { 111 | return 0, "", err 112 | } 113 | 114 | err = w.Checkout(&git.CheckoutOptions{ 115 | Hash: *h, 116 | }) 117 | if err != nil { 118 | return 0, "", err 119 | } 120 | } 121 | 122 | if name == "" { 123 | name = strings.TrimSuffix(path.Base(u.Path), ".git") 124 | } 125 | 126 | return TypeDir, name, nil 127 | } 128 | 129 | // Update uses git to pull the repository and update it 130 | // to the latest revision. It allows specifying the depth 131 | // and recursion options via query string. It returns 132 | // true if update was successful and false if the 133 | // repository is already up-to-date 134 | func (GitDownloader) Update(opts Options) (bool, error) { 135 | u, err := url.Parse(opts.URL) 136 | if err != nil { 137 | return false, err 138 | } 139 | u.Scheme = strings.TrimPrefix(u.Scheme, "git+") 140 | 141 | query := u.Query() 142 | query.Del("~rev") 143 | 144 | depthStr := query.Get("~depth") 145 | query.Del("~depth") 146 | 147 | recursive := query.Get("~recursive") 148 | query.Del("~recursive") 149 | 150 | u.RawQuery = query.Encode() 151 | 152 | r, err := git.PlainOpen(opts.Destination) 153 | if err != nil { 154 | return false, err 155 | } 156 | 157 | w, err := r.Worktree() 158 | if err != nil { 159 | return false, err 160 | } 161 | 162 | depth := 0 163 | if depthStr != "" { 164 | depth, err = strconv.Atoi(depthStr) 165 | if err != nil { 166 | return false, err 167 | } 168 | } 169 | 170 | po := &git.PullOptions{ 171 | Depth: depth, 172 | Progress: opts.Progress, 173 | RecurseSubmodules: git.NoRecurseSubmodules, 174 | } 175 | 176 | if recursive == "true" { 177 | po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth 178 | } 179 | 180 | m, err := getManifest(opts.Destination) 181 | manifestOK := err == nil 182 | 183 | err = w.Pull(po) 184 | if errors.Is(err, git.NoErrAlreadyUpToDate) { 185 | return false, nil 186 | } else if err != nil { 187 | return false, err 188 | } 189 | 190 | if manifestOK { 191 | err = writeManifest(opts.Destination, m) 192 | if err != nil { 193 | return true, err 194 | } 195 | } 196 | 197 | return true, nil 198 | } 199 | -------------------------------------------------------------------------------- /internal/dl/torrent.go: -------------------------------------------------------------------------------- 1 | package dl 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | urlMatchRegex = regexp.MustCompile(`(magnet|torrent\+https?):.*`) 15 | ErrAria2NotFound = errors.New("aria2 must be installed for torrent functionality") 16 | ErrDestinationEmpty = errors.New("the destination directory is empty") 17 | ) 18 | 19 | type TorrentDownloader struct{} 20 | 21 | // Name always returns "file" 22 | func (TorrentDownloader) Name() string { 23 | return "torrent" 24 | } 25 | 26 | // MatchURL returns true if the URL is a magnet link 27 | // or an http(s) link with a "torrent+" prefix 28 | func (TorrentDownloader) MatchURL(u string) bool { 29 | return urlMatchRegex.MatchString(u) 30 | } 31 | 32 | // Download downloads a file over the BitTorrent protocol. 33 | func (TorrentDownloader) Download(opts Options) (Type, string, error) { 34 | aria2Path, err := exec.LookPath("aria2c") 35 | if err != nil { 36 | return 0, "", ErrAria2NotFound 37 | } 38 | 39 | opts.URL = strings.TrimPrefix(opts.URL, "torrent+") 40 | 41 | cmd := exec.Command(aria2Path, "--summary-interval=0", "--log-level=warn", "--seed-time=0", "--dir="+opts.Destination, opts.URL) 42 | cmd.Stdout = os.Stdout 43 | cmd.Stderr = os.Stderr 44 | err = cmd.Run() 45 | if err != nil { 46 | return 0, "", fmt.Errorf("aria2c returned an error: %w", err) 47 | } 48 | 49 | err = removeTorrentFiles(opts.Destination) 50 | if err != nil { 51 | return 0, "", err 52 | } 53 | 54 | return determineType(opts.Destination) 55 | } 56 | 57 | func removeTorrentFiles(path string) error { 58 | filePaths, err := filepath.Glob(filepath.Join(path, "*.torrent")) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | for _, filePath := range filePaths { 64 | err = os.Remove(filePath) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func determineType(path string) (Type, string, error) { 74 | files, err := os.ReadDir(path) 75 | if err != nil { 76 | return 0, "", err 77 | } 78 | 79 | if len(files) > 1 { 80 | return TypeDir, "", nil 81 | } else if len(files) == 1 { 82 | if files[0].IsDir() { 83 | return TypeDir, files[0].Name(), nil 84 | } else { 85 | return TypeFile, files[0].Name(), nil 86 | } 87 | } 88 | 89 | return 0, "", ErrDestinationEmpty 90 | } 91 | -------------------------------------------------------------------------------- /internal/dlcache/dlcache.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package dlcache 20 | 21 | import ( 22 | "context" 23 | "crypto/sha1" 24 | "encoding/hex" 25 | "io" 26 | "os" 27 | "path/filepath" 28 | 29 | "lure.sh/lure/internal/config" 30 | ) 31 | 32 | // BasePath returns the base path of the download cache 33 | func BasePath(ctx context.Context) string { 34 | return filepath.Join(config.GetPaths(ctx).CacheDir, "dl") 35 | } 36 | 37 | // New creates a new directory with the given ID in the cache. 38 | // If a directory with the same ID already exists, 39 | // it will be deleted before creating a new one. 40 | func New(ctx context.Context, id string) (string, error) { 41 | h, err := hashID(id) 42 | if err != nil { 43 | return "", err 44 | } 45 | itemPath := filepath.Join(BasePath(ctx), h) 46 | 47 | fi, err := os.Stat(itemPath) 48 | if err == nil || (fi != nil && !fi.IsDir()) { 49 | err = os.RemoveAll(itemPath) 50 | if err != nil { 51 | return "", err 52 | } 53 | } 54 | 55 | err = os.MkdirAll(itemPath, 0o755) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | return itemPath, nil 61 | } 62 | 63 | // Get checks if an entry with the given ID 64 | // already exists in the cache, and if so, 65 | // returns the directory and true. If it 66 | // does not exist, it returns an empty string 67 | // and false. 68 | func Get(ctx context.Context, id string) (string, bool) { 69 | h, err := hashID(id) 70 | if err != nil { 71 | return "", false 72 | } 73 | itemPath := filepath.Join(BasePath(ctx), h) 74 | 75 | _, err = os.Stat(itemPath) 76 | if err != nil { 77 | return "", false 78 | } 79 | 80 | return itemPath, true 81 | } 82 | 83 | // hashID hashes the input ID with SHA1 84 | // and returns the hex string of the hashed 85 | // ID. 86 | func hashID(id string) (string, error) { 87 | h := sha1.New() 88 | _, err := io.WriteString(h, id) 89 | if err != nil { 90 | return "", err 91 | } 92 | return hex.EncodeToString(h.Sum(nil)), nil 93 | } 94 | -------------------------------------------------------------------------------- /internal/dlcache/dlcache_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package dlcache_test 20 | 21 | import ( 22 | "context" 23 | "crypto/sha1" 24 | "encoding/hex" 25 | "io" 26 | "os" 27 | "path/filepath" 28 | "testing" 29 | 30 | "lure.sh/lure/internal/config" 31 | "lure.sh/lure/internal/dlcache" 32 | ) 33 | 34 | func init() { 35 | dir, err := os.MkdirTemp("/tmp", "lure-dlcache-test.*") 36 | if err != nil { 37 | panic(err) 38 | } 39 | config.GetPaths(context.Background()).RepoDir = dir 40 | } 41 | 42 | func TestNew(t *testing.T) { 43 | const id = "https://example.com" 44 | dir, err := dlcache.New(id) 45 | if err != nil { 46 | t.Errorf("Expected no error, got %s", err) 47 | } 48 | 49 | exp := filepath.Join(dlcache.BasePath(), sha1sum(id)) 50 | if dir != exp { 51 | t.Errorf("Expected %s, got %s", exp, dir) 52 | } 53 | 54 | fi, err := os.Stat(dir) 55 | if err != nil { 56 | t.Errorf("stat: expected no error, got %s", err) 57 | } 58 | 59 | if !fi.IsDir() { 60 | t.Errorf("Expected cache item to be a directory") 61 | } 62 | 63 | dir2, ok := dlcache.Get(id) 64 | if !ok { 65 | t.Errorf("Expected Get() to return valid value") 66 | } 67 | if dir2 != dir { 68 | t.Errorf("Expected %s from Get(), got %s", dir, dir2) 69 | } 70 | } 71 | 72 | func sha1sum(id string) string { 73 | h := sha1.New() 74 | _, _ = io.WriteString(h, id) 75 | return hex.EncodeToString(h.Sum(nil)) 76 | } 77 | -------------------------------------------------------------------------------- /internal/osutils/move.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package osutils 20 | 21 | import ( 22 | "io" 23 | "os" 24 | "path/filepath" 25 | ) 26 | 27 | // Move attempts to use os.Rename and if that fails (such as for a cross-device move), 28 | // it instead copies the source to the destination and then removes the source. 29 | func Move(sourcePath, destPath string) error { 30 | // Try to rename the source to the destination 31 | err := os.Rename(sourcePath, destPath) 32 | if err == nil { 33 | return nil // Successful move 34 | } 35 | 36 | // Rename failed, so copy the source to the destination 37 | err = copyDirOrFile(sourcePath, destPath) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | // Copy successful, remove the original source 43 | err = os.RemoveAll(sourcePath) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func copyDirOrFile(sourcePath, destPath string) error { 52 | sourceInfo, err := os.Stat(sourcePath) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if sourceInfo.IsDir() { 58 | return copyDir(sourcePath, destPath, sourceInfo) 59 | } else if sourceInfo.Mode().IsRegular() { 60 | return copyFile(sourcePath, destPath, sourceInfo) 61 | } else { 62 | // ignore non-regular files 63 | return nil 64 | } 65 | } 66 | 67 | func copyDir(sourcePath, destPath string, sourceInfo os.FileInfo) error { 68 | err := os.MkdirAll(destPath, sourceInfo.Mode()) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | entries, err := os.ReadDir(sourcePath) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | for _, entry := range entries { 79 | sourceEntry := filepath.Join(sourcePath, entry.Name()) 80 | destEntry := filepath.Join(destPath, entry.Name()) 81 | 82 | err = copyDirOrFile(sourceEntry, destEntry) 83 | if err != nil { 84 | return err 85 | } 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func copyFile(sourcePath, destPath string, sourceInfo os.FileInfo) error { 92 | sourceFile, err := os.Open(sourcePath) 93 | if err != nil { 94 | return err 95 | } 96 | defer sourceFile.Close() 97 | 98 | destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, sourceInfo.Mode()) 99 | if err != nil { 100 | return err 101 | } 102 | defer destFile.Close() 103 | 104 | _, err = io.Copy(destFile, sourceFile) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /internal/overrides/overrides.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package overrides 20 | 21 | import ( 22 | "reflect" 23 | "strings" 24 | 25 | "lure.sh/lure/internal/cpu" 26 | "lure.sh/lure/internal/db" 27 | "lure.sh/lure/pkg/distro" 28 | "golang.org/x/exp/slices" 29 | "golang.org/x/text/language" 30 | ) 31 | 32 | type Opts struct { 33 | Name string 34 | Overrides bool 35 | LikeDistros bool 36 | Languages []string 37 | LanguageTags []language.Tag 38 | } 39 | 40 | var DefaultOpts = &Opts{ 41 | Overrides: true, 42 | LikeDistros: true, 43 | Languages: []string{"en"}, 44 | } 45 | 46 | // Resolve generates a slice of possible override names in the order that they should be checked 47 | func Resolve(info *distro.OSRelease, opts *Opts) ([]string, error) { 48 | if opts == nil { 49 | opts = DefaultOpts 50 | } 51 | 52 | if !opts.Overrides { 53 | return []string{opts.Name}, nil 54 | } 55 | 56 | langs, err := parseLangs(opts.Languages, opts.LanguageTags) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | architectures, err := cpu.CompatibleArches(cpu.Arch()) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | distros := []string{info.ID} 67 | if opts.LikeDistros { 68 | distros = append(distros, info.Like...) 69 | } 70 | 71 | var out []string 72 | for _, lang := range langs { 73 | for _, distro := range distros { 74 | for _, arch := range architectures { 75 | out = append(out, opts.Name+"_"+arch+"_"+distro+"_"+lang) 76 | } 77 | 78 | out = append(out, opts.Name+"_"+distro+"_"+lang) 79 | } 80 | 81 | for _, arch := range architectures { 82 | out = append(out, opts.Name+"_"+arch+"_"+lang) 83 | } 84 | 85 | out = append(out, opts.Name+"_"+lang) 86 | } 87 | 88 | for _, distro := range distros { 89 | for _, arch := range architectures { 90 | out = append(out, opts.Name+"_"+arch+"_"+distro) 91 | } 92 | 93 | out = append(out, opts.Name+"_"+distro) 94 | } 95 | 96 | for _, arch := range architectures { 97 | out = append(out, opts.Name+"_"+arch) 98 | } 99 | 100 | out = append(out, opts.Name) 101 | 102 | for index, item := range out { 103 | out[index] = strings.TrimPrefix(strings.ReplaceAll(item, "-", "_"), "_") 104 | } 105 | 106 | return out, nil 107 | } 108 | 109 | func (o *Opts) WithName(name string) *Opts { 110 | out := &Opts{} 111 | *out = *o 112 | 113 | out.Name = name 114 | return out 115 | } 116 | 117 | func (o *Opts) WithOverrides(v bool) *Opts { 118 | out := &Opts{} 119 | *out = *o 120 | 121 | out.Overrides = v 122 | return out 123 | } 124 | 125 | func (o *Opts) WithLikeDistros(v bool) *Opts { 126 | out := &Opts{} 127 | *out = *o 128 | 129 | out.LikeDistros = v 130 | return out 131 | } 132 | 133 | func (o *Opts) WithLanguages(langs []string) *Opts { 134 | out := &Opts{} 135 | *out = *o 136 | 137 | out.Languages = langs 138 | return out 139 | } 140 | 141 | func (o *Opts) WithLanguageTags(langs []string) *Opts { 142 | out := &Opts{} 143 | *out = *o 144 | 145 | out.Languages = langs 146 | return out 147 | } 148 | 149 | // ResolvedPackage is a LURE package after its overrides 150 | // have been resolved 151 | type ResolvedPackage struct { 152 | Name string `sh:"name"` 153 | Version string `sh:"version"` 154 | Release int `sh:"release"` 155 | Epoch uint `sh:"epoch"` 156 | Description string `db:"description"` 157 | Homepage string `db:"homepage"` 158 | Maintainer string `db:"maintainer"` 159 | Architectures []string `sh:"architectures"` 160 | Licenses []string `sh:"license"` 161 | Provides []string `sh:"provides"` 162 | Conflicts []string `sh:"conflicts"` 163 | Replaces []string `sh:"replaces"` 164 | Depends []string `sh:"deps"` 165 | BuildDepends []string `sh:"build_deps"` 166 | OptDepends []string `sh:"opt_deps"` 167 | } 168 | 169 | func ResolvePackage(pkg *db.Package, overrides []string) *ResolvedPackage { 170 | out := &ResolvedPackage{} 171 | outVal := reflect.ValueOf(out).Elem() 172 | pkgVal := reflect.ValueOf(pkg).Elem() 173 | 174 | for i := 0; i < outVal.NumField(); i++ { 175 | fieldVal := outVal.Field(i) 176 | fieldType := fieldVal.Type() 177 | pkgFieldVal := pkgVal.FieldByName(outVal.Type().Field(i).Name) 178 | pkgFieldType := pkgFieldVal.Type() 179 | 180 | if strings.HasPrefix(pkgFieldType.String(), "db.JSON") { 181 | pkgFieldVal = pkgFieldVal.FieldByName("Val") 182 | pkgFieldType = pkgFieldVal.Type() 183 | } 184 | 185 | if pkgFieldType.AssignableTo(fieldType) { 186 | fieldVal.Set(pkgFieldVal) 187 | continue 188 | } 189 | 190 | if pkgFieldVal.Kind() == reflect.Map && pkgFieldType.Elem().AssignableTo(fieldType) { 191 | for _, override := range overrides { 192 | overrideVal := pkgFieldVal.MapIndex(reflect.ValueOf(override)) 193 | if !overrideVal.IsValid() { 194 | continue 195 | } 196 | 197 | fieldVal.Set(overrideVal) 198 | break 199 | } 200 | } 201 | } 202 | 203 | return out 204 | } 205 | 206 | func parseLangs(langs []string, tags []language.Tag) ([]string, error) { 207 | out := make([]string, len(tags)+len(langs)) 208 | for i, tag := range tags { 209 | base, _ := tag.Base() 210 | out[i] = base.String() 211 | } 212 | for i, lang := range langs { 213 | tag, err := language.Parse(lang) 214 | if err != nil { 215 | return nil, err 216 | } 217 | base, _ := tag.Base() 218 | out[len(tags)+i] = base.String() 219 | } 220 | slices.Sort(out) 221 | out = slices.Compact(out) 222 | return out, nil 223 | } 224 | -------------------------------------------------------------------------------- /internal/overrides/overrides_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package overrides_test 20 | 21 | import ( 22 | "os" 23 | "reflect" 24 | "testing" 25 | 26 | "lure.sh/lure/internal/overrides" 27 | "lure.sh/lure/pkg/distro" 28 | "golang.org/x/text/language" 29 | ) 30 | 31 | var info = &distro.OSRelease{ 32 | ID: "centos", 33 | Like: []string{"rhel", "fedora"}, 34 | } 35 | 36 | func TestResolve(t *testing.T) { 37 | names, err := overrides.Resolve(info, nil) 38 | if err != nil { 39 | t.Fatalf("Expected no error, got %s", err) 40 | } 41 | 42 | expected := []string{ 43 | "amd64_centos_en", 44 | "centos_en", 45 | "amd64_rhel_en", 46 | "rhel_en", 47 | "amd64_fedora_en", 48 | "fedora_en", 49 | "amd64_en", 50 | "en", 51 | "amd64_centos", 52 | "centos", 53 | "amd64_rhel", 54 | "rhel", 55 | "amd64_fedora", 56 | "fedora", 57 | "amd64", 58 | "", 59 | } 60 | 61 | if !reflect.DeepEqual(names, expected) { 62 | t.Errorf("expected %v, got %v", expected, names) 63 | } 64 | } 65 | 66 | func TestResolveName(t *testing.T) { 67 | names, err := overrides.Resolve(info, &overrides.Opts{ 68 | Name: "deps", 69 | Overrides: true, 70 | LikeDistros: true, 71 | }) 72 | if err != nil { 73 | t.Fatalf("Expected no error, got %s", err) 74 | } 75 | 76 | expected := []string{ 77 | "deps_amd64_centos", 78 | "deps_centos", 79 | "deps_amd64_rhel", 80 | "deps_rhel", 81 | "deps_amd64_fedora", 82 | "deps_fedora", 83 | "deps_amd64", 84 | "deps", 85 | } 86 | 87 | if !reflect.DeepEqual(names, expected) { 88 | t.Errorf("expected %v, got %v", expected, names) 89 | } 90 | } 91 | 92 | func TestResolveArch(t *testing.T) { 93 | os.Setenv("LURE_ARCH", "arm7") 94 | defer os.Setenv("LURE_ARCH", "") 95 | 96 | names, err := overrides.Resolve(info, &overrides.Opts{ 97 | Name: "deps", 98 | Overrides: true, 99 | LikeDistros: true, 100 | }) 101 | if err != nil { 102 | t.Fatalf("Expected no error, got %s", err) 103 | } 104 | 105 | expected := []string{ 106 | "deps_arm7_centos", 107 | "deps_arm6_centos", 108 | "deps_arm5_centos", 109 | "deps_centos", 110 | "deps_arm7_rhel", 111 | "deps_arm6_rhel", 112 | "deps_arm5_rhel", 113 | "deps_rhel", 114 | "deps_arm7_fedora", 115 | "deps_arm6_fedora", 116 | "deps_arm5_fedora", 117 | "deps_fedora", 118 | "deps_arm7", 119 | "deps_arm6", 120 | "deps_arm5", 121 | "deps", 122 | } 123 | 124 | if !reflect.DeepEqual(names, expected) { 125 | t.Errorf("expected %v, got %v", expected, names) 126 | } 127 | } 128 | 129 | func TestResolveNoLikeDistros(t *testing.T) { 130 | names, err := overrides.Resolve(info, &overrides.Opts{ 131 | Overrides: true, 132 | LikeDistros: false, 133 | }) 134 | if err != nil { 135 | t.Fatalf("Expected no error, got %s", err) 136 | } 137 | 138 | expected := []string{ 139 | "amd64_centos", 140 | "centos", 141 | "amd64", 142 | "", 143 | } 144 | 145 | if !reflect.DeepEqual(names, expected) { 146 | t.Errorf("expected %v, got %v", expected, names) 147 | } 148 | } 149 | 150 | func TestResolveNoOverrides(t *testing.T) { 151 | names, err := overrides.Resolve(info, &overrides.Opts{ 152 | Name: "deps", 153 | Overrides: false, 154 | LikeDistros: false, 155 | }) 156 | if err != nil { 157 | t.Fatalf("Expected no error, got %s", err) 158 | } 159 | 160 | expected := []string{"deps"} 161 | 162 | if !reflect.DeepEqual(names, expected) { 163 | t.Errorf("expected %v, got %v", expected, names) 164 | } 165 | } 166 | 167 | func TestResolveLangs(t *testing.T) { 168 | names, err := overrides.Resolve(info, &overrides.Opts{ 169 | Overrides: true, 170 | Languages: []string{"ru_RU", "en", "en_US"}, 171 | LanguageTags: []language.Tag{language.BritishEnglish}, 172 | }) 173 | if err != nil { 174 | t.Fatalf("Expected no error, got %s", err) 175 | } 176 | 177 | expected := []string{ 178 | "amd64_centos_en", 179 | "centos_en", 180 | "amd64_en", 181 | "en", 182 | "amd64_centos_ru", 183 | "centos_ru", 184 | "amd64_ru", 185 | "ru", 186 | "amd64_centos", 187 | "centos", 188 | "amd64", 189 | "", 190 | } 191 | 192 | if !reflect.DeepEqual(names, expected) { 193 | t.Errorf("expected %v, got %v", expected, names) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /internal/pager/highlighting.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package pager 20 | 21 | import ( 22 | "bytes" 23 | "io" 24 | 25 | "github.com/alecthomas/chroma/v2/quick" 26 | ) 27 | 28 | func SyntaxHighlightBash(r io.Reader, style string) (string, error) { 29 | data, err := io.ReadAll(r) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | w := &bytes.Buffer{} 35 | err = quick.Highlight(w, string(data), "bash", "terminal", style) 36 | return w.String(), err 37 | } 38 | -------------------------------------------------------------------------------- /internal/pager/pager.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package pager 20 | 21 | import ( 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/charmbracelet/bubbles/viewport" 26 | tea "github.com/charmbracelet/bubbletea" 27 | "github.com/charmbracelet/lipgloss" 28 | "github.com/muesli/reflow/wordwrap" 29 | ) 30 | 31 | var ( 32 | titleStyle lipgloss.Style 33 | infoStyle lipgloss.Style 34 | ) 35 | 36 | func init() { 37 | b1 := lipgloss.RoundedBorder() 38 | b1.Right = "\u251C" 39 | titleStyle = lipgloss.NewStyle().BorderStyle(b1).Padding(0, 1) 40 | 41 | b2 := lipgloss.RoundedBorder() 42 | b2.Left = "\u2524" 43 | infoStyle = titleStyle.Copy().BorderStyle(b2) 44 | } 45 | 46 | type Pager struct { 47 | model pagerModel 48 | } 49 | 50 | func New(name, content string) *Pager { 51 | return &Pager{ 52 | model: pagerModel{ 53 | name: name, 54 | content: content, 55 | }, 56 | } 57 | } 58 | 59 | func (p *Pager) Run() error { 60 | prog := tea.NewProgram( 61 | p.model, 62 | tea.WithMouseCellMotion(), 63 | tea.WithAltScreen(), 64 | ) 65 | 66 | _, err := prog.Run() 67 | return err 68 | } 69 | 70 | type pagerModel struct { 71 | name string 72 | content string 73 | ready bool 74 | viewport viewport.Model 75 | } 76 | 77 | func (pm pagerModel) Init() tea.Cmd { 78 | return nil 79 | } 80 | 81 | func (pm pagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 82 | var ( 83 | cmd tea.Cmd 84 | cmds []tea.Cmd 85 | ) 86 | 87 | switch msg := msg.(type) { 88 | case tea.KeyMsg: 89 | k := msg.String() 90 | if k == "ctrl+c" || k == "q" || k == "esc" { 91 | return pm, tea.Quit 92 | } 93 | case tea.WindowSizeMsg: 94 | headerHeight := lipgloss.Height(pm.headerView()) 95 | footerHeight := lipgloss.Height(pm.footerView()) 96 | verticalMarginHeight := headerHeight + footerHeight 97 | 98 | if !pm.ready { 99 | pm.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) 100 | pm.viewport.HighPerformanceRendering = true 101 | pm.viewport.YPosition = headerHeight + 1 102 | pm.viewport.SetContent(wordwrap.String(pm.content, msg.Width)) 103 | pm.ready = true 104 | } else { 105 | pm.viewport.Width = msg.Width 106 | pm.viewport.Height = msg.Height - verticalMarginHeight 107 | } 108 | 109 | cmds = append(cmds, viewport.Sync(pm.viewport)) 110 | } 111 | 112 | // Handle keyboard and mouse events in the viewport 113 | pm.viewport, cmd = pm.viewport.Update(msg) 114 | cmds = append(cmds, cmd) 115 | 116 | return pm, tea.Batch(cmds...) 117 | } 118 | 119 | func (pm pagerModel) View() string { 120 | if !pm.ready { 121 | return "\n Initializing..." 122 | } 123 | return fmt.Sprintf("%s\n%s\n%s", pm.headerView(), pm.viewport.View(), pm.footerView()) 124 | } 125 | 126 | func (pm pagerModel) headerView() string { 127 | title := titleStyle.Render(pm.name) 128 | line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(title))) 129 | return lipgloss.JoinHorizontal(lipgloss.Center, title, line) 130 | } 131 | 132 | func (pm pagerModel) footerView() string { 133 | info := infoStyle.Render(fmt.Sprintf("%3.f%%", pm.viewport.ScrollPercent()*100)) 134 | line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(info))) 135 | return lipgloss.JoinHorizontal(lipgloss.Center, line, info) 136 | } 137 | 138 | func max(a, b int) int { 139 | if a > b { 140 | return a 141 | } 142 | return b 143 | } 144 | -------------------------------------------------------------------------------- /internal/shutils/decoder/decoder.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package decoder 20 | 21 | import ( 22 | "context" 23 | "errors" 24 | "reflect" 25 | "strings" 26 | 27 | "github.com/mitchellh/mapstructure" 28 | "lure.sh/lure/internal/overrides" 29 | "lure.sh/lure/pkg/distro" 30 | "golang.org/x/exp/slices" 31 | "mvdan.cc/sh/v3/expand" 32 | "mvdan.cc/sh/v3/interp" 33 | "mvdan.cc/sh/v3/syntax" 34 | ) 35 | 36 | var ErrNotPointerToStruct = errors.New("val must be a pointer to a struct") 37 | 38 | type VarNotFoundError struct { 39 | name string 40 | } 41 | 42 | func (nfe VarNotFoundError) Error() string { 43 | return "required variable '" + nfe.name + "' could not be found" 44 | } 45 | 46 | type InvalidTypeError struct { 47 | name string 48 | vartype string 49 | exptype string 50 | } 51 | 52 | func (ite InvalidTypeError) Error() string { 53 | return "variable '" + ite.name + "' is of type " + ite.vartype + ", but " + ite.exptype + " is expected" 54 | } 55 | 56 | // Decoder provides methods for decoding variable values 57 | type Decoder struct { 58 | info *distro.OSRelease 59 | Runner *interp.Runner 60 | // Enable distro overrides (true by default) 61 | Overrides bool 62 | // Enable using like distros for overrides 63 | LikeDistros bool 64 | } 65 | 66 | // New creates a new variable decoder 67 | func New(info *distro.OSRelease, runner *interp.Runner) *Decoder { 68 | return &Decoder{info, runner, true, len(info.Like) > 0} 69 | } 70 | 71 | // DecodeVar decodes a variable to val using reflection. 72 | // Structs should use the "sh" struct tag. 73 | func (d *Decoder) DecodeVar(name string, val any) error { 74 | variable := d.getVar(name) 75 | if variable == nil { 76 | return VarNotFoundError{name} 77 | } 78 | 79 | dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 80 | WeaklyTypedInput: true, 81 | DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) { 82 | if strings.Contains(to.Type().String(), "db.JSON") { 83 | valType := to.FieldByName("Val").Type() 84 | if !from.Type().AssignableTo(valType) { 85 | return nil, InvalidTypeError{name, from.Type().String(), valType.String()} 86 | } 87 | 88 | to.FieldByName("Val").Set(from) 89 | return to, nil 90 | } 91 | return from.Interface(), nil 92 | }), 93 | Result: val, 94 | TagName: "sh", 95 | }) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | switch variable.Kind { 101 | case expand.Indexed: 102 | return dec.Decode(variable.List) 103 | case expand.Associative: 104 | return dec.Decode(variable.Map) 105 | default: 106 | return dec.Decode(variable.Str) 107 | } 108 | } 109 | 110 | // DecodeVars decodes all variables to val using reflection. 111 | // Structs should use the "sh" struct tag. 112 | func (d *Decoder) DecodeVars(val any) error { 113 | valKind := reflect.TypeOf(val).Kind() 114 | if valKind != reflect.Pointer { 115 | return ErrNotPointerToStruct 116 | } else { 117 | elemKind := reflect.TypeOf(val).Elem().Kind() 118 | if elemKind != reflect.Struct { 119 | return ErrNotPointerToStruct 120 | } 121 | } 122 | 123 | rVal := reflect.ValueOf(val).Elem() 124 | 125 | for i := 0; i < rVal.NumField(); i++ { 126 | field := rVal.Field(i) 127 | fieldType := rVal.Type().Field(i) 128 | 129 | if !fieldType.IsExported() { 130 | continue 131 | } 132 | 133 | name := fieldType.Name 134 | tag := fieldType.Tag.Get("sh") 135 | required := false 136 | if tag != "" { 137 | if strings.Contains(tag, ",") { 138 | splitTag := strings.Split(tag, ",") 139 | name = splitTag[0] 140 | 141 | if len(splitTag) > 1 { 142 | if slices.Contains(splitTag, "required") { 143 | required = true 144 | } 145 | } 146 | } else { 147 | name = tag 148 | } 149 | } 150 | 151 | newVal := reflect.New(field.Type()) 152 | err := d.DecodeVar(name, newVal.Interface()) 153 | if _, ok := err.(VarNotFoundError); ok && !required { 154 | continue 155 | } else if err != nil { 156 | return err 157 | } 158 | 159 | field.Set(newVal.Elem()) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | type ScriptFunc func(ctx context.Context, opts ...interp.RunnerOption) error 166 | 167 | // GetFunc returns a function corresponding to a bash function 168 | // with the given name 169 | func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) { 170 | fn := d.getFunc(name) 171 | if fn == nil { 172 | return nil, false 173 | } 174 | 175 | return func(ctx context.Context, opts ...interp.RunnerOption) error { 176 | sub := d.Runner.Subshell() 177 | for _, opt := range opts { 178 | opt(sub) 179 | } 180 | return sub.Run(ctx, fn) 181 | }, true 182 | } 183 | 184 | func (d *Decoder) getFunc(name string) *syntax.Stmt { 185 | names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) 186 | if err != nil { 187 | return nil 188 | } 189 | 190 | for _, fnName := range names { 191 | fn, ok := d.Runner.Funcs[fnName] 192 | if ok { 193 | return fn 194 | } 195 | } 196 | return nil 197 | } 198 | 199 | // getVar gets a variable based on its name, taking into account 200 | // override variables and nameref variables. 201 | func (d *Decoder) getVar(name string) *expand.Variable { 202 | names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) 203 | if err != nil { 204 | return nil 205 | } 206 | 207 | for _, varName := range names { 208 | val, ok := d.Runner.Vars[varName] 209 | if ok { 210 | // Resolve nameref variables 211 | _, resolved := val.Resolve(expand.FuncEnviron(func(s string) string { 212 | if val, ok := d.Runner.Vars[s]; ok { 213 | return val.String() 214 | } 215 | return "" 216 | })) 217 | val = resolved 218 | 219 | return &val 220 | } 221 | } 222 | return nil 223 | } 224 | -------------------------------------------------------------------------------- /internal/shutils/decoder/decoder_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package decoder_test 20 | 21 | import ( 22 | "bytes" 23 | "context" 24 | "errors" 25 | "os" 26 | "reflect" 27 | "strings" 28 | "testing" 29 | 30 | "lure.sh/lure/internal/shutils/decoder" 31 | "lure.sh/lure/pkg/distro" 32 | "mvdan.cc/sh/v3/interp" 33 | "mvdan.cc/sh/v3/syntax" 34 | ) 35 | 36 | type BuildVars struct { 37 | Name string `sh:"name,required"` 38 | Version string `sh:"version,required"` 39 | Release int `sh:"release,required"` 40 | Epoch uint `sh:"epoch"` 41 | Description string `sh:"desc"` 42 | Homepage string `sh:"homepage"` 43 | Maintainer string `sh:"maintainer"` 44 | Architectures []string `sh:"architectures"` 45 | Licenses []string `sh:"license"` 46 | Provides []string `sh:"provides"` 47 | Conflicts []string `sh:"conflicts"` 48 | Depends []string `sh:"deps"` 49 | BuildDepends []string `sh:"build_deps"` 50 | Replaces []string `sh:"replaces"` 51 | } 52 | 53 | const testScript = ` 54 | name='test' 55 | version='0.0.1' 56 | release=1 57 | epoch=2 58 | desc="Test package" 59 | homepage='https://lure.arsenm.dev' 60 | maintainer='Arsen Musayelyan ' 61 | architectures=('arm64' 'amd64') 62 | license=('GPL-3.0-or-later') 63 | provides=('test') 64 | conflicts=('test') 65 | replaces=('test-old') 66 | replaces_test_os=('test-legacy') 67 | 68 | deps=('sudo') 69 | 70 | build_deps=('golang') 71 | build_deps_arch=('go') 72 | 73 | test() { 74 | echo "Test" 75 | } 76 | 77 | package() { 78 | install-binary test 79 | } 80 | ` 81 | 82 | var osRelease = &distro.OSRelease{ 83 | ID: "test_os", 84 | Like: []string{"arch"}, 85 | } 86 | 87 | func TestDecodeVars(t *testing.T) { 88 | ctx := context.Background() 89 | 90 | fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") 91 | if err != nil { 92 | t.Fatalf("Expected no error, got %s", err) 93 | } 94 | 95 | runner, err := interp.New() 96 | if err != nil { 97 | t.Fatalf("Expected no error, got %s", err) 98 | } 99 | 100 | err = runner.Run(ctx, fl) 101 | if err != nil { 102 | t.Fatalf("Expected no error, got %s", err) 103 | } 104 | 105 | dec := decoder.New(osRelease, runner) 106 | 107 | var bv BuildVars 108 | err = dec.DecodeVars(&bv) 109 | if err != nil { 110 | t.Fatalf("Expected no error, got %s", err) 111 | } 112 | 113 | expected := BuildVars{ 114 | Name: "test", 115 | Version: "0.0.1", 116 | Release: 1, 117 | Epoch: 2, 118 | Description: "Test package", 119 | Homepage: "https://lure.arsenm.dev", 120 | Maintainer: "Arsen Musayelyan ", 121 | Architectures: []string{"arm64", "amd64"}, 122 | Licenses: []string{"GPL-3.0-or-later"}, 123 | Provides: []string{"test"}, 124 | Conflicts: []string{"test"}, 125 | Replaces: []string{"test-legacy"}, 126 | Depends: []string{"sudo"}, 127 | BuildDepends: []string{"go"}, 128 | } 129 | 130 | if !reflect.DeepEqual(bv, expected) { 131 | t.Errorf("Expected %v, got %v", expected, bv) 132 | } 133 | } 134 | 135 | func TestDecodeVarsMissing(t *testing.T) { 136 | ctx := context.Background() 137 | 138 | const testScript = ` 139 | name='test' 140 | epoch=2 141 | desc="Test package" 142 | homepage='https://lure.arsenm.dev' 143 | maintainer='Arsen Musayelyan ' 144 | architectures=('arm64' 'amd64') 145 | license=('GPL-3.0-or-later') 146 | provides=('test') 147 | conflicts=('test') 148 | replaces=('test-old') 149 | replaces_test_os=('test-legacy') 150 | 151 | deps=('sudo') 152 | 153 | build_deps=('golang') 154 | build_deps_arch=('go') 155 | 156 | test() { 157 | echo "Test" 158 | } 159 | 160 | package() { 161 | install-binary test 162 | } 163 | ` 164 | 165 | fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") 166 | if err != nil { 167 | t.Fatalf("Expected no error, got %s", err) 168 | } 169 | 170 | runner, err := interp.New() 171 | if err != nil { 172 | t.Fatalf("Expected no error, got %s", err) 173 | } 174 | 175 | err = runner.Run(ctx, fl) 176 | if err != nil { 177 | t.Fatalf("Expected no error, got %s", err) 178 | } 179 | 180 | dec := decoder.New(osRelease, runner) 181 | 182 | var bv BuildVars 183 | err = dec.DecodeVars(&bv) 184 | 185 | var notFoundErr decoder.VarNotFoundError 186 | if !errors.As(err, ¬FoundErr) { 187 | t.Fatalf("Expected VarNotFoundError, got %T %v", err, err) 188 | } 189 | } 190 | 191 | func TestGetFunc(t *testing.T) { 192 | ctx := context.Background() 193 | 194 | fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") 195 | if err != nil { 196 | t.Fatalf("Expected no error, got %s", err) 197 | } 198 | 199 | runner, err := interp.New() 200 | if err != nil { 201 | t.Fatalf("Expected no error, got %s", err) 202 | } 203 | 204 | err = runner.Run(ctx, fl) 205 | if err != nil { 206 | t.Fatalf("Expected no error, got %s", err) 207 | } 208 | 209 | dec := decoder.New(osRelease, runner) 210 | fn, ok := dec.GetFunc("test") 211 | if !ok { 212 | t.Fatalf("Expected test() function to exist") 213 | } 214 | 215 | buf := &bytes.Buffer{} 216 | err = fn(ctx, interp.StdIO(os.Stdin, buf, buf)) 217 | if err != nil { 218 | t.Fatalf("Expected no error, got %s", err) 219 | } 220 | 221 | if buf.String() != "Test\n" { 222 | t.Fatalf(`Expected "Test\n", got %#v`, buf.String()) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /internal/shutils/handlers/exec.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package handlers 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | "time" 25 | 26 | "mvdan.cc/sh/v3/interp" 27 | ) 28 | 29 | func InsufficientArgsError(cmd string, exp, got int) error { 30 | argsWord := "arguments" 31 | if exp == 1 { 32 | argsWord = "argument" 33 | } 34 | 35 | return fmt.Errorf("%s: command requires at least %d %s, got %d", cmd, exp, argsWord, got) 36 | } 37 | 38 | type ExecFunc func(hc interp.HandlerContext, name string, args []string) error 39 | 40 | type ExecFuncs map[string]ExecFunc 41 | 42 | // ExecHandler returns a new ExecHandlerFunc that falls back to fallback 43 | // if the command cannot be found in the map. If fallback is nil, the default 44 | // handler is used. 45 | func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc { 46 | return func(ctx context.Context, args []string) error { 47 | name := args[0] 48 | 49 | if fn, ok := ef[name]; ok { 50 | hctx := interp.HandlerCtx(ctx) 51 | if len(args) > 1 { 52 | return fn(hctx, args[0], args[1:]) 53 | } else { 54 | return fn(hctx, args[0], nil) 55 | } 56 | } 57 | 58 | if fallback == nil { 59 | fallback = interp.DefaultExecHandler(2 * time.Second) 60 | } 61 | return fallback(ctx, args) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/shutils/handlers/exec_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package handlers_test 20 | 21 | import ( 22 | "context" 23 | "strings" 24 | "testing" 25 | 26 | "lure.sh/lure/internal/shutils/handlers" 27 | "lure.sh/lure/internal/shutils/decoder" 28 | "lure.sh/lure/pkg/distro" 29 | "mvdan.cc/sh/v3/interp" 30 | "mvdan.cc/sh/v3/syntax" 31 | ) 32 | 33 | const testScript = ` 34 | name='test' 35 | version='0.0.1' 36 | release=1 37 | epoch=2 38 | desc="Test package" 39 | homepage='https://lure.sh' 40 | maintainer='Elara Musayelyan ' 41 | architectures=('arm64' 'amd64') 42 | license=('GPL-3.0-or-later') 43 | provides=('test') 44 | conflicts=('test') 45 | replaces=('test-old') 46 | replaces_test_os=('test-legacy') 47 | 48 | deps=('sudo') 49 | 50 | build_deps=('golang') 51 | build_deps_arch=('go') 52 | 53 | test() { 54 | test-cmd "Hello, World" 55 | test-fb 56 | } 57 | 58 | package() { 59 | install-binary test 60 | } 61 | ` 62 | 63 | var osRelease = &distro.OSRelease{ 64 | ID: "test_os", 65 | Like: []string{"arch"}, 66 | } 67 | 68 | func TestExecFuncs(t *testing.T) { 69 | ctx := context.Background() 70 | 71 | fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") 72 | if err != nil { 73 | t.Fatalf("Expected no error, got %s", err) 74 | } 75 | 76 | runner, err := interp.New() 77 | if err != nil { 78 | t.Fatalf("Expected no error, got %s", err) 79 | } 80 | 81 | err = runner.Run(ctx, fl) 82 | if err != nil { 83 | t.Fatalf("Expected no error, got %s", err) 84 | } 85 | 86 | dec := decoder.New(osRelease, runner) 87 | fn, ok := dec.GetFunc("test") 88 | if !ok { 89 | t.Fatalf("Expected test() function to exist") 90 | } 91 | 92 | eh := shutils.ExecFuncs{ 93 | "test-cmd": func(hc interp.HandlerContext, name string, args []string) error { 94 | if name != "test-cmd" { 95 | t.Errorf("Expected name to be 'test-cmd', got '%s'", name) 96 | } 97 | 98 | if len(args) < 1 { 99 | t.Fatalf("Expected at least one argument, got %d", len(args)) 100 | } 101 | 102 | if args[0] != "Hello, World" { 103 | t.Errorf("Expected first argument to be 'Hello, World', got '%s'", args[0]) 104 | } 105 | 106 | return nil 107 | }, 108 | } 109 | 110 | fbInvoked := false 111 | fbHandler := func(context.Context, []string) error { 112 | fbInvoked = true 113 | return nil 114 | } 115 | 116 | err = fn(ctx, interp.ExecHandler(eh.ExecHandler(fbHandler))) 117 | if err != nil { 118 | t.Errorf("Expected no error, got %s", err) 119 | } 120 | 121 | if !fbInvoked { 122 | t.Errorf("Expected fallback handler to be invoked") 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /internal/shutils/handlers/fakeroot.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | "syscall" 11 | "time" 12 | 13 | "lure.sh/fakeroot" 14 | "mvdan.cc/sh/v3/expand" 15 | "mvdan.cc/sh/v3/interp" 16 | ) 17 | 18 | // FakerootExecHandler was extracted from github.com/mvdan/sh/interp/handler.go 19 | // and modified to run commands in a fakeroot environent. 20 | func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc { 21 | return func(ctx context.Context, args []string) error { 22 | hc := interp.HandlerCtx(ctx) 23 | path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) 24 | if err != nil { 25 | fmt.Fprintln(hc.Stderr, err) 26 | return interp.NewExitStatus(127) 27 | } 28 | cmd := &exec.Cmd{ 29 | Path: path, 30 | Args: args, 31 | Env: execEnv(hc.Env), 32 | Dir: hc.Dir, 33 | Stdin: hc.Stdin, 34 | Stdout: hc.Stdout, 35 | Stderr: hc.Stderr, 36 | } 37 | 38 | err = fakeroot.Apply(cmd) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = cmd.Start() 44 | if err == nil { 45 | if done := ctx.Done(); done != nil { 46 | go func() { 47 | <-done 48 | 49 | if killTimeout <= 0 || runtime.GOOS == "windows" { 50 | _ = cmd.Process.Signal(os.Kill) 51 | return 52 | } 53 | 54 | // TODO: don't temporarily leak this goroutine 55 | // if the program stops itself with the 56 | // interrupt. 57 | go func() { 58 | time.Sleep(killTimeout) 59 | _ = cmd.Process.Signal(os.Kill) 60 | }() 61 | _ = cmd.Process.Signal(os.Interrupt) 62 | }() 63 | } 64 | 65 | err = cmd.Wait() 66 | } 67 | 68 | switch x := err.(type) { 69 | case *exec.ExitError: 70 | // started, but errored - default to 1 if OS 71 | // doesn't have exit statuses 72 | if status, ok := x.Sys().(syscall.WaitStatus); ok { 73 | if status.Signaled() { 74 | if ctx.Err() != nil { 75 | return ctx.Err() 76 | } 77 | return interp.NewExitStatus(uint8(128 + status.Signal())) 78 | } 79 | return interp.NewExitStatus(uint8(status.ExitStatus())) 80 | } 81 | return interp.NewExitStatus(1) 82 | case *exec.Error: 83 | // did not start 84 | fmt.Fprintf(hc.Stderr, "%v\n", err) 85 | return interp.NewExitStatus(127) 86 | default: 87 | return err 88 | } 89 | } 90 | } 91 | 92 | // execEnv was extracted from github.com/mvdan/sh/interp/vars.go 93 | func execEnv(env expand.Environ) []string { 94 | list := make([]string, 0, 64) 95 | env.Each(func(name string, vr expand.Variable) bool { 96 | if !vr.IsSet() { 97 | // If a variable is set globally but unset in the 98 | // runner, we need to ensure it's not part of the final 99 | // list. Seems like zeroing the element is enough. 100 | // This is a linear search, but this scenario should be 101 | // rare, and the number of variables shouldn't be large. 102 | for i, kv := range list { 103 | if strings.HasPrefix(kv, name+"=") { 104 | list[i] = "" 105 | } 106 | } 107 | } 108 | if vr.Exported && vr.Kind == expand.String { 109 | list = append(list, name+"="+vr.String()) 110 | } 111 | return true 112 | }) 113 | return list 114 | } 115 | -------------------------------------------------------------------------------- /internal/shutils/handlers/nop.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package handlers 20 | 21 | import ( 22 | "context" 23 | "io" 24 | "os" 25 | ) 26 | 27 | func NopReadDir(context.Context, string) ([]os.FileInfo, error) { 28 | return nil, os.ErrNotExist 29 | } 30 | 31 | func NopStat(context.Context, string, bool) (os.FileInfo, error) { 32 | return nil, os.ErrNotExist 33 | } 34 | 35 | func NopExec(context.Context, []string) error { 36 | return nil 37 | } 38 | 39 | func NopOpen(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) { 40 | return NopRWC{}, nil 41 | } 42 | 43 | type NopRWC struct{} 44 | 45 | func (NopRWC) Read([]byte) (int, error) { 46 | return 0, io.EOF 47 | } 48 | 49 | func (NopRWC) Write(b []byte) (int, error) { 50 | return len(b), nil 51 | } 52 | 53 | func (NopRWC) Close() error { 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/shutils/handlers/nop_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package handlers_test 20 | 21 | import ( 22 | "bytes" 23 | "context" 24 | "os" 25 | "strings" 26 | "testing" 27 | 28 | "lure.sh/lure/internal/shutils/handlers" 29 | "mvdan.cc/sh/v3/interp" 30 | "mvdan.cc/sh/v3/syntax" 31 | ) 32 | 33 | func TestNopExec(t *testing.T) { 34 | ctx := context.Background() 35 | 36 | fl, err := syntax.NewParser().Parse(strings.NewReader(`/bin/echo test`), "lure.sh") 37 | if err != nil { 38 | t.Fatalf("Expected no error, got %s", err) 39 | } 40 | 41 | buf := &bytes.Buffer{} 42 | runner, err := interp.New( 43 | interp.ExecHandler(handlers.NopExec), 44 | interp.StdIO(os.Stdin, buf, buf), 45 | ) 46 | if err != nil { 47 | t.Fatalf("Expected no error, got %s", err) 48 | } 49 | 50 | err = runner.Run(ctx, fl) 51 | if err != nil { 52 | t.Fatalf("Expected no error, got %s", err) 53 | } 54 | 55 | if buf.String() != "" { 56 | t.Fatalf("Expected empty string, got %#v", buf.String()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/shutils/handlers/restricted.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package handlers 20 | 21 | import ( 22 | "context" 23 | "io" 24 | "io/fs" 25 | "path/filepath" 26 | "strings" 27 | "time" 28 | 29 | "golang.org/x/exp/slices" 30 | "mvdan.cc/sh/v3/interp" 31 | ) 32 | 33 | func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc { 34 | return func(ctx context.Context, s string) ([]fs.FileInfo, error) { 35 | path := filepath.Clean(s) 36 | for _, allowedPrefix := range allowedPrefixes { 37 | if strings.HasPrefix(path, allowedPrefix) { 38 | return interp.DefaultReadDirHandler()(ctx, s) 39 | } 40 | } 41 | 42 | return nil, fs.ErrNotExist 43 | } 44 | } 45 | 46 | func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc { 47 | return func(ctx context.Context, s string, b bool) (fs.FileInfo, error) { 48 | path := filepath.Clean(s) 49 | for _, allowedPrefix := range allowedPrefixes { 50 | if strings.HasPrefix(path, allowedPrefix) { 51 | return interp.DefaultStatHandler()(ctx, s, b) 52 | } 53 | } 54 | 55 | return nil, fs.ErrNotExist 56 | } 57 | } 58 | 59 | func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc { 60 | return func(ctx context.Context, s string, i int, fm fs.FileMode) (io.ReadWriteCloser, error) { 61 | path := filepath.Clean(s) 62 | for _, allowedPrefix := range allowedPrefixes { 63 | if strings.HasPrefix(path, allowedPrefix) { 64 | return interp.DefaultOpenHandler()(ctx, s, i, fm) 65 | } 66 | } 67 | 68 | return NopRWC{}, nil 69 | } 70 | } 71 | 72 | func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc { 73 | return func(ctx context.Context, args []string) error { 74 | if slices.Contains(allowedCmds, args[0]) { 75 | return interp.DefaultExecHandler(2*time.Second)(ctx, args) 76 | } 77 | 78 | return nil 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /internal/translations/files/lure.en.toml: -------------------------------------------------------------------------------- 1 | [[translation]] 2 | id = 1228660974 3 | value = 'Pulling repository' 4 | 5 | [[translation]] 6 | id = 2779805870 7 | value = 'Repository up to date' 8 | 9 | [[translation]] 10 | id = 1433222829 11 | value = 'Would you like to view the build script for' 12 | 13 | [[translation]] 14 | id = 2470847050 15 | value = 'Failed to prompt user to view build script' 16 | 17 | [[translation]] 18 | id = 855659503 19 | value = 'Would you still like to continue?' 20 | 21 | [[translation]] 22 | id = 1997041569 23 | value = 'User chose not to continue after reading script' 24 | 25 | [[translation]] 26 | id = 2347700990 27 | value = 'Building package' 28 | 29 | [[translation]] 30 | id = 2105058868 31 | value = 'Downloading sources' 32 | 33 | [[translation]] 34 | id = 1884485082 35 | value = 'Downloading source' 36 | 37 | [[translation]] 38 | id = 1519177982 39 | value = 'Error building package' 40 | 41 | [[translation]] 42 | id = 2125220917 43 | value = 'Choose which package(s) to install' 44 | 45 | [[translation]] 46 | id = 812531604 47 | value = 'Error prompting for choice of package' 48 | 49 | [[translation]] 50 | id = 1040982801 51 | value = 'Updating version' 52 | 53 | [[translation]] 54 | id = 1014897988 55 | value = 'Remove build dependencies?' 56 | 57 | [[translation]] 58 | id = 2205430948 59 | value = 'Installing build dependencies' 60 | 61 | [[translation]] 62 | id = 2522710805 63 | value = 'Installing dependencies' 64 | 65 | [[translation]] 66 | id = 3602138206 67 | value = 'Error installing package' 68 | 69 | [[translation]] 70 | id = 2235794125 71 | value = 'Would you like to remove build dependencies?' 72 | 73 | [[translation]] 74 | id = 2562049386 75 | value = "Your system's CPU architecture doesn't match this package. Do you want to build anyway?" 76 | 77 | [[translation]] 78 | id = 4006393493 79 | value = 'The checksums array must be the same length as sources' 80 | 81 | [[translation]] 82 | id = 3759891273 83 | value = 'The package() function is required' 84 | 85 | [[translation]] 86 | id = 1057080231 87 | value = 'Executing package()' 88 | 89 | [[translation]] 90 | id = 2687735200 91 | value = 'Executing prepare()' 92 | 93 | [[translation]] 94 | id = 535572372 95 | value = 'Executing version()' 96 | 97 | [[translation]] 98 | id = 436644691 99 | value = 'Executing build()' 100 | 101 | [[translation]] 102 | id = 1393316459 103 | value = 'This package is already installed' 104 | 105 | [[translation]] 106 | id = 1267660189 107 | value = 'Source can be updated, updating if required' 108 | 109 | [[translation]] 110 | id = 21753247 111 | value = 'Source found in cache, linked to destination' 112 | 113 | [[translation]] 114 | id = 257354570 115 | value = 'Compressing package' 116 | 117 | [[translation]] 118 | id = 2952487371 119 | value = 'Building package metadata' 120 | 121 | [[translation]] 122 | id = 3121791194 123 | value = 'Running LURE as root is forbidden as it may cause catastrophic damage to your system' 124 | 125 | [[translation]] 126 | id = 1256604213 127 | value = 'Waiting for torrent metadata' 128 | 129 | [[translation]] 130 | id = 432261354 131 | value = 'Downloading torrent file' 132 | 133 | [[translation]] 134 | id = 1579384326 135 | value = 'name' 136 | 137 | [[translation]] 138 | id = 3206337475 139 | value = 'version' 140 | 141 | [[translation]] 142 | id = 1810056261 143 | value = 'new' 144 | 145 | [[translation]] 146 | id = 1602912115 147 | value = 'source' 148 | 149 | [[translation]] 150 | id = 2363381545 151 | value = 'type' 152 | 153 | [[translation]] 154 | id = 3419504365 155 | value = 'downloader' 156 | -------------------------------------------------------------------------------- /internal/translations/files/lure.ru.toml: -------------------------------------------------------------------------------- 1 | [[translation]] 2 | id = 1228660974 3 | value = 'Скачивание репозитория' 4 | 5 | [[translation]] 6 | id = 2779805870 7 | value = 'Репозиторий уже обновлен' 8 | 9 | [[translation]] 10 | id = 1433222829 11 | value = 'Показать скрипт для пакета' 12 | 13 | [[translation]] 14 | id = 2470847050 15 | value = 'Не удалось предложить просмотреть скрипт' 16 | 17 | [[translation]] 18 | id = 855659503 19 | value = 'Продолжить?' 20 | 21 | [[translation]] 22 | id = 1997041569 23 | value = 'Пользователь решил не продолжать после просмотра скрипта' 24 | 25 | [[translation]] 26 | id = 2347700990 27 | value = 'Сборка пакета' 28 | 29 | [[translation]] 30 | id = 2105058868 31 | value = 'Скачивание файлов' 32 | 33 | [[translation]] 34 | id = 1884485082 35 | value = 'Скачивание источника' 36 | 37 | [[translation]] 38 | id = 1519177982 39 | value = 'Ошибка при сборке пакета' 40 | 41 | [[translation]] 42 | id = 2125220917 43 | value = 'Выберите, какие пакеты установить' 44 | 45 | [[translation]] 46 | id = 812531604 47 | value = 'Ошибка при запросе выбора пакета' 48 | 49 | [[translation]] 50 | id = 1040982801 51 | value = 'Обновление версии' 52 | 53 | [[translation]] 54 | id = 2235794125 55 | value = 'Удалить зависимости сборки?' 56 | 57 | [[translation]] 58 | id = 2205430948 59 | value = 'Установка зависимостей сборки' 60 | 61 | [[translation]] 62 | id = 2522710805 63 | value = 'Установка зависимостей' 64 | 65 | [[translation]] 66 | id = 3602138206 67 | value = 'Ошибка при установке пакета' 68 | 69 | [[translation]] 70 | id = 1057080231 71 | value = 'Вызов функции package()' 72 | 73 | [[translation]] 74 | id = 2687735200 75 | value = 'Вызов функции prepare()' 76 | 77 | [[translation]] 78 | id = 535572372 79 | value = 'Вызов функции version()' 80 | 81 | [[translation]] 82 | id = 436644691 83 | value = 'Вызов функции build()' 84 | 85 | [[translation]] 86 | id = 2562049386 87 | value = "Архитектура процессора вашей системы не соответствует этому пакету. Продолжать несмотря на это?" 88 | 89 | [[translation]] 90 | id = 3759891273 91 | value = 'Функция package() необходима' 92 | 93 | [[translation]] 94 | id = 4006393493 95 | value = 'Массив checksums должен быть той же длины, что и sources' 96 | 97 | [[translation]] 98 | id = 1393316459 99 | value = 'Этот пакет уже установлен' 100 | 101 | [[translation]] 102 | id = 1267660189 103 | value = 'Источник может быть обновлен, если требуется, обновляем' 104 | 105 | [[translation]] 106 | id = 21753247 107 | value = 'Источник найден в кэше' 108 | 109 | [[translation]] 110 | id = 257354570 111 | value = 'Сжатие пакета' 112 | 113 | [[translation]] 114 | id = 2952487371 115 | value = 'Создание метаданных пакета' 116 | 117 | [[translation]] 118 | id = 3121791194 119 | value = 'Запуск LURE от имени root запрещен, так как это может привести к катастрофическому повреждению вашей системы' 120 | 121 | [[translation]] 122 | id = 1256604213 123 | value = 'Ожидание метаданных торрента' 124 | 125 | [[translation]] 126 | id = 432261354 127 | value = 'Скачивание торрент-файла' 128 | 129 | [[translation]] 130 | id = 1579384326 131 | value = 'название' 132 | 133 | [[translation]] 134 | id = 3206337475 135 | value = 'версия' 136 | 137 | [[translation]] 138 | id = 1810056261 139 | value = 'новая' 140 | 141 | [[translation]] 142 | id = 1602912115 143 | value = 'источник' 144 | 145 | [[translation]] 146 | id = 2363381545 147 | value = 'вид' 148 | 149 | [[translation]] 150 | id = 3419504365 151 | value = 'протокол-скачивание' -------------------------------------------------------------------------------- /internal/translations/translations.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package translations 20 | 21 | import ( 22 | "context" 23 | "embed" 24 | "sync" 25 | 26 | "go.elara.ws/logger" 27 | "lure.sh/lure/pkg/loggerctx" 28 | "go.elara.ws/translate" 29 | "golang.org/x/text/language" 30 | ) 31 | 32 | //go:embed files 33 | var translationFS embed.FS 34 | 35 | var ( 36 | mu sync.Mutex 37 | translator *translate.Translator 38 | ) 39 | 40 | func Translator(ctx context.Context) *translate.Translator { 41 | mu.Lock() 42 | defer mu.Unlock() 43 | log := loggerctx.From(ctx) 44 | if translator == nil { 45 | t, err := translate.NewFromFS(translationFS) 46 | if err != nil { 47 | log.Fatal("Error creating new translator").Err(err).Send() 48 | } 49 | translator = &t 50 | } 51 | return translator 52 | } 53 | 54 | func NewLogger(ctx context.Context, l logger.Logger, lang language.Tag) *translate.TranslatedLogger { 55 | return translate.NewLogger(l, *Translator(ctx), lang) 56 | } 57 | -------------------------------------------------------------------------------- /internal/types/build.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package types 20 | 21 | import "lure.sh/lure/pkg/manager" 22 | 23 | type BuildOpts struct { 24 | Script string 25 | Manager manager.Manager 26 | Clean bool 27 | Interactive bool 28 | } 29 | 30 | // BuildVars represents the script variables required 31 | // to build a package 32 | type BuildVars struct { 33 | Name string `sh:"name,required"` 34 | Version string `sh:"version,required"` 35 | Release int `sh:"release,required"` 36 | Epoch uint `sh:"epoch"` 37 | Description string `sh:"desc"` 38 | Homepage string `sh:"homepage"` 39 | Maintainer string `sh:"maintainer"` 40 | Architectures []string `sh:"architectures"` 41 | Licenses []string `sh:"license"` 42 | Provides []string `sh:"provides"` 43 | Conflicts []string `sh:"conflicts"` 44 | Depends []string `sh:"deps"` 45 | BuildDepends []string `sh:"build_deps"` 46 | OptDepends []string `sh:"opt_deps"` 47 | Replaces []string `sh:"replaces"` 48 | Sources []string `sh:"sources"` 49 | Checksums []string `sh:"checksums"` 50 | Backup []string `sh:"backup"` 51 | Scripts Scripts `sh:"scripts"` 52 | } 53 | 54 | type Scripts struct { 55 | PreInstall string `sh:"preinstall"` 56 | PostInstall string `sh:"postinstall"` 57 | PreRemove string `sh:"preremove"` 58 | PostRemove string `sh:"postremove"` 59 | PreUpgrade string `sh:"preupgrade"` 60 | PostUpgrade string `sh:"postupgrade"` 61 | PreTrans string `sh:"pretrans"` 62 | PostTrans string `sh:"posttrans"` 63 | } 64 | 65 | type Directories struct { 66 | BaseDir string 67 | SrcDir string 68 | PkgDir string 69 | ScriptDir string 70 | } 71 | -------------------------------------------------------------------------------- /internal/types/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package types 20 | 21 | // Config represents the LURE configuration file 22 | type Config struct { 23 | RootCmd string `toml:"rootCmd"` 24 | PagerStyle string `toml:"pagerStyle"` 25 | IgnorePkgUpdates []string `toml:"ignorePkgUpdates"` 26 | Repos []Repo `toml:"repo"` 27 | Unsafe Unsafe `toml:"unsafe"` 28 | } 29 | 30 | // Repo represents a LURE repo within a configuration file 31 | type Repo struct { 32 | Name string `toml:"name"` 33 | URL string `toml:"url"` 34 | } 35 | 36 | type Unsafe struct { 37 | AllowRunAsRoot bool `toml:"allowRunAsRoot"` 38 | } 39 | -------------------------------------------------------------------------------- /internal/types/repo.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package types 20 | 21 | // RepoConfig represents a LURE repo's lure-repo.toml file. 22 | type RepoConfig struct { 23 | Repo struct { 24 | MinVersion string `toml:"minVersion"` 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | 24 | "github.com/urfave/cli/v2" 25 | "lure.sh/lure/internal/config" 26 | "lure.sh/lure/internal/db" 27 | "lure.sh/lure/pkg/loggerctx" 28 | "lure.sh/lure/pkg/manager" 29 | "lure.sh/lure/pkg/repos" 30 | "golang.org/x/exp/slices" 31 | ) 32 | 33 | var listCmd = &cli.Command{ 34 | Name: "list", 35 | Usage: "List LURE repo packages", 36 | Aliases: []string{"ls"}, 37 | Flags: []cli.Flag{ 38 | &cli.BoolFlag{ 39 | Name: "installed", 40 | Aliases: []string{"I"}, 41 | }, 42 | }, 43 | Action: func(c *cli.Context) error { 44 | ctx := c.Context 45 | log := loggerctx.From(ctx) 46 | 47 | err := repos.Pull(ctx, config.Config(ctx).Repos) 48 | if err != nil { 49 | log.Fatal("Error pulling repositories").Err(err).Send() 50 | } 51 | 52 | where := "true" 53 | args := []any(nil) 54 | if c.NArg() > 0 { 55 | where = "name LIKE ? OR json_array_contains(provides, ?)" 56 | args = []any{c.Args().First(), c.Args().First()} 57 | } 58 | 59 | result, err := db.GetPkgs(ctx, where, args...) 60 | if err != nil { 61 | log.Fatal("Error getting packages").Err(err).Send() 62 | } 63 | defer result.Close() 64 | 65 | var installed map[string]string 66 | if c.Bool("installed") { 67 | mgr := manager.Detect() 68 | if mgr == nil { 69 | log.Fatal("Unable to detect a supported package manager on the system").Send() 70 | } 71 | 72 | installed, err = mgr.ListInstalled(&manager.Opts{AsRoot: false}) 73 | if err != nil { 74 | log.Fatal("Error listing installed packages").Err(err).Send() 75 | } 76 | } 77 | 78 | for result.Next() { 79 | var pkg db.Package 80 | err := result.StructScan(&pkg) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | if slices.Contains(config.Config(ctx).IgnorePkgUpdates, pkg.Name) { 86 | continue 87 | } 88 | 89 | version := pkg.Version 90 | if c.Bool("installed") { 91 | instVersion, ok := installed[pkg.Name] 92 | if !ok { 93 | continue 94 | } else { 95 | version = instVersion 96 | } 97 | } 98 | 99 | fmt.Printf("%s/%s %s\n", pkg.Repository, pkg.Name, version) 100 | } 101 | 102 | if err != nil { 103 | log.Fatal("Error iterating over packages").Err(err).Send() 104 | } 105 | 106 | return nil 107 | }, 108 | } 109 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "os/signal" 25 | "strings" 26 | "syscall" 27 | 28 | "github.com/mattn/go-isatty" 29 | "github.com/urfave/cli/v2" 30 | "go.elara.ws/logger" 31 | "lure.sh/lure/internal/config" 32 | "lure.sh/lure/internal/db" 33 | "lure.sh/lure/internal/translations" 34 | "lure.sh/lure/pkg/loggerctx" 35 | "lure.sh/lure/pkg/manager" 36 | ) 37 | 38 | var app = &cli.App{ 39 | Name: "lure", 40 | Usage: "Linux User REpository", 41 | Flags: []cli.Flag{ 42 | &cli.StringFlag{ 43 | Name: "pm-args", 44 | Aliases: []string{"P"}, 45 | Usage: "Arguments to be passed on to the package manager", 46 | }, 47 | &cli.BoolFlag{ 48 | Name: "interactive", 49 | Aliases: []string{"i"}, 50 | Value: isatty.IsTerminal(os.Stdin.Fd()), 51 | Usage: "Enable interactive questions and prompts", 52 | }, 53 | }, 54 | Commands: []*cli.Command{ 55 | installCmd, 56 | removeCmd, 57 | upgradeCmd, 58 | infoCmd, 59 | listCmd, 60 | buildCmd, 61 | addrepoCmd, 62 | removerepoCmd, 63 | refreshCmd, 64 | fixCmd, 65 | genCmd, 66 | helperCmd, 67 | versionCmd, 68 | }, 69 | Before: func(c *cli.Context) error { 70 | ctx := c.Context 71 | log := loggerctx.From(ctx) 72 | 73 | cmd := c.Args().First() 74 | if cmd != "helper" && !config.Config(ctx).Unsafe.AllowRunAsRoot && os.Geteuid() == 0 { 75 | log.Fatal("Running LURE as root is forbidden as it may cause catastrophic damage to your system").Send() 76 | } 77 | 78 | if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" { 79 | args := strings.Split(trimmed, " ") 80 | manager.Args = append(manager.Args, args...) 81 | } 82 | 83 | return nil 84 | }, 85 | After: func(ctx *cli.Context) error { 86 | return db.Close() 87 | }, 88 | EnableBashCompletion: true, 89 | } 90 | 91 | var versionCmd = &cli.Command{ 92 | Name: "version", 93 | Usage: "Print the current LURE version and exit", 94 | Action: func(ctx *cli.Context) error { 95 | println(config.Version) 96 | return nil 97 | }, 98 | } 99 | 100 | func main() { 101 | ctx := context.Background() 102 | log := translations.NewLogger(ctx, logger.NewCLI(os.Stderr), config.Language(ctx)) 103 | ctx = loggerctx.With(ctx, log) 104 | 105 | // Set the root command to the one set in the LURE config 106 | manager.DefaultRootCmd = config.Config(ctx).RootCmd 107 | 108 | ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) 109 | defer cancel() 110 | 111 | err := app.RunContext(ctx, os.Args) 112 | if err != nil { 113 | log.Error("Error while running app").Err(err).Send() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pkg/build/install.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package build 20 | 21 | import ( 22 | "context" 23 | "path/filepath" 24 | 25 | "lure.sh/lure/internal/config" 26 | "lure.sh/lure/internal/db" 27 | "lure.sh/lure/internal/types" 28 | "lure.sh/lure/pkg/loggerctx" 29 | ) 30 | 31 | // InstallPkgs installs native packages via the package manager, 32 | // then builds and installs the LURE packages 33 | func InstallPkgs(ctx context.Context, lurePkgs []db.Package, nativePkgs []string, opts types.BuildOpts) { 34 | log := loggerctx.From(ctx) 35 | 36 | if len(nativePkgs) > 0 { 37 | err := opts.Manager.Install(nil, nativePkgs...) 38 | if err != nil { 39 | log.Fatal("Error installing native packages").Err(err).Send() 40 | } 41 | } 42 | 43 | InstallScripts(ctx, GetScriptPaths(ctx, lurePkgs), opts) 44 | } 45 | 46 | // GetScriptPaths returns a slice of script paths corresponding to the 47 | // given packages 48 | func GetScriptPaths(ctx context.Context, pkgs []db.Package) []string { 49 | var scripts []string 50 | for _, pkg := range pkgs { 51 | scriptPath := filepath.Join(config.GetPaths(ctx).RepoDir, pkg.Repository, pkg.Name, "lure.sh") 52 | scripts = append(scripts, scriptPath) 53 | } 54 | return scripts 55 | } 56 | 57 | // InstallScripts builds and installs the given LURE build scripts 58 | func InstallScripts(ctx context.Context, scripts []string, opts types.BuildOpts) { 59 | log := loggerctx.From(ctx) 60 | for _, script := range scripts { 61 | opts.Script = script 62 | builtPkgs, _, err := BuildPackage(ctx, opts) 63 | if err != nil { 64 | log.Fatal("Error building package").Err(err).Send() 65 | } 66 | 67 | err = opts.Manager.InstallLocal(nil, builtPkgs...) 68 | if err != nil { 69 | log.Fatal("Error installing package").Err(err).Send() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/distro/osrelease.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package distro 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "strings" 25 | 26 | "lure.sh/lure/internal/shutils/handlers" 27 | "mvdan.cc/sh/v3/expand" 28 | "mvdan.cc/sh/v3/interp" 29 | "mvdan.cc/sh/v3/syntax" 30 | ) 31 | 32 | // OSRelease contains information from an os-release file 33 | type OSRelease struct { 34 | Name string 35 | PrettyName string 36 | ID string 37 | Like []string 38 | VersionID string 39 | ANSIColor string 40 | HomeURL string 41 | DocumentationURL string 42 | SupportURL string 43 | BugReportURL string 44 | Logo string 45 | } 46 | 47 | var parsed *OSRelease 48 | 49 | // OSReleaseName returns a struct parsed from the system's os-release 50 | // file. It checks /etc/os-release as well as /usr/lib/os-release. 51 | // The first time it's called, it'll parse the os-release file. 52 | // Subsequent calls will return the same value. 53 | func ParseOSRelease(ctx context.Context) (*OSRelease, error) { 54 | if parsed != nil { 55 | return parsed, nil 56 | } 57 | 58 | fl, err := os.Open("/usr/lib/os-release") 59 | if err != nil { 60 | fl, err = os.Open("/etc/os-release") 61 | if err != nil { 62 | return nil, err 63 | } 64 | } 65 | 66 | file, err := syntax.NewParser().Parse(fl, "/usr/lib/os-release") 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | fl.Close() 72 | 73 | // Create new shell interpreter with nop open, exec, readdir, and stat handlers 74 | // as well as no environment variables in order to prevent vulnerabilities 75 | // caused by changing the os-release file. 76 | runner, err := interp.New( 77 | interp.OpenHandler(handlers.NopOpen), 78 | interp.ExecHandler(handlers.NopExec), 79 | interp.ReadDirHandler(handlers.NopReadDir), 80 | interp.StatHandler(handlers.NopStat), 81 | interp.Env(expand.ListEnviron()), 82 | ) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | err = runner.Run(ctx, file) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | out := &OSRelease{ 93 | Name: runner.Vars["NAME"].Str, 94 | PrettyName: runner.Vars["PRETTY_NAME"].Str, 95 | ID: runner.Vars["ID"].Str, 96 | VersionID: runner.Vars["VERSION_ID"].Str, 97 | ANSIColor: runner.Vars["ANSI_COLOR"].Str, 98 | HomeURL: runner.Vars["HOME_URL"].Str, 99 | DocumentationURL: runner.Vars["DOCUMENTATION_URL"].Str, 100 | SupportURL: runner.Vars["SUPPORT_URL"].Str, 101 | BugReportURL: runner.Vars["BUG_REPORT_URL"].Str, 102 | Logo: runner.Vars["LOGO"].Str, 103 | } 104 | 105 | distroUpdated := false 106 | if distID, ok := os.LookupEnv("LURE_DISTRO"); ok { 107 | out.ID = distID 108 | } 109 | 110 | if distLike, ok := os.LookupEnv("LURE_DISTRO_LIKE"); ok { 111 | out.Like = strings.Split(distLike, " ") 112 | } else if runner.Vars["ID_LIKE"].IsSet() && !distroUpdated { 113 | out.Like = strings.Split(runner.Vars["ID_LIKE"].Str, " ") 114 | } 115 | 116 | parsed = out 117 | return out, nil 118 | } 119 | -------------------------------------------------------------------------------- /pkg/gen/funcs.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | "strings" 5 | "text/template" 6 | ) 7 | 8 | var funcs = template.FuncMap{ 9 | "tolower": strings.ToLower, 10 | "firstchar": func(s string) string { 11 | return s[:1] 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /pkg/gen/pip.go: -------------------------------------------------------------------------------- 1 | package gen 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "text/template" 11 | ) 12 | 13 | //go:embed tmpls/pip.tmpl.sh 14 | var pipTmpl string 15 | 16 | type PipOptions struct { 17 | Name string 18 | Version string 19 | Description string 20 | } 21 | 22 | type pypiAPIResponse struct { 23 | Info pypiInfo `json:"info"` 24 | URLs []pypiURL `json:"urls"` 25 | } 26 | 27 | func (res pypiAPIResponse) SourceURL() (pypiURL, error) { 28 | for _, url := range res.URLs { 29 | if url.PackageType == "sdist" { 30 | return url, nil 31 | } 32 | } 33 | return pypiURL{}, errors.New("package doesn't have a source distribution") 34 | } 35 | 36 | type pypiInfo struct { 37 | Name string `json:"name"` 38 | Version string `json:"version"` 39 | Summary string `json:"summary"` 40 | Homepage string `json:"home_page"` 41 | License string `json:"license"` 42 | } 43 | 44 | type pypiURL struct { 45 | Digests map[string]string `json:"digests"` 46 | Filename string `json:"filename"` 47 | PackageType string `json:"packagetype"` 48 | } 49 | 50 | func Pip(w io.Writer, opts PipOptions) error { 51 | tmpl, err := template.New("pip"). 52 | Funcs(funcs). 53 | Parse(pipTmpl) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | url := fmt.Sprintf( 59 | "https://pypi.org/pypi/%s/%s/json", 60 | opts.Name, 61 | opts.Version, 62 | ) 63 | 64 | res, err := http.Get(url) 65 | if err != nil { 66 | return err 67 | } 68 | defer res.Body.Close() 69 | if res.StatusCode != 200 { 70 | return fmt.Errorf("pypi: %s", res.Status) 71 | } 72 | 73 | var resp pypiAPIResponse 74 | err = json.NewDecoder(res.Body).Decode(&resp) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | if opts.Description != "" { 80 | resp.Info.Summary = opts.Description 81 | } 82 | 83 | return tmpl.Execute(w, resp) 84 | } 85 | -------------------------------------------------------------------------------- /pkg/gen/tmpls/pip.tmpl.sh: -------------------------------------------------------------------------------- 1 | name='{{.Info.Name | tolower}}' 2 | version='{{.Info.Version}}' 3 | release='1' 4 | desc='{{.Info.Summary}}' 5 | homepage='{{.Info.Homepage}}' 6 | maintainer='Example ' 7 | architectures=('all') 8 | license=('{{if .Info.License | ne ""}}{{.Info.License}}{{else}}custom:Unknown{{end}}') 9 | provides=('{{.Info.Name | tolower}}') 10 | conflicts=('{{.Info.Name | tolower}}') 11 | 12 | deps=("python3") 13 | deps_arch=("python") 14 | deps_alpine=("python3") 15 | 16 | build_deps=("python3" "python3-setuptools") 17 | build_deps_arch=("python" "python-setuptools") 18 | build_deps_alpine=("python3" "py3-setuptools") 19 | 20 | sources=("https://files.pythonhosted.org/packages/source/{{.SourceURL.Filename | firstchar}}/{{.Info.Name}}/{{.SourceURL.Filename}}") 21 | checksums=('blake2b-256:{{.SourceURL.Digests.blake2b_256}}') 22 | 23 | build() { 24 | cd "$srcdir/{{.Info.Name}}-${version}" 25 | python3 setup.py build 26 | } 27 | 28 | package() { 29 | cd "$srcdir/{{.Info.Name}}-${version}" 30 | python3 setup.py install --root="${pkgdir}/" --optimize=1 || return 1 31 | } 32 | -------------------------------------------------------------------------------- /pkg/loggerctx/log.go: -------------------------------------------------------------------------------- 1 | package loggerctx 2 | 3 | import ( 4 | "context" 5 | 6 | "go.elara.ws/logger" 7 | ) 8 | 9 | // loggerCtxKey is used as the context key for loggers 10 | type loggerCtxKey struct{} 11 | 12 | // With returns a copy of ctx containing log 13 | func With(ctx context.Context, log logger.Logger) context.Context { 14 | return context.WithValue(ctx, loggerCtxKey{}, log) 15 | } 16 | 17 | // From attempts to get a logger from ctx. If ctx doesn't 18 | // contain a logger, it returns a nop logger. 19 | func From(ctx context.Context) logger.Logger { 20 | if val := ctx.Value(loggerCtxKey{}); val != nil { 21 | if log, ok := val.(logger.Logger); ok && log != nil { 22 | return log 23 | } else { 24 | return logger.NewNop() 25 | } 26 | } else { 27 | return logger.NewNop() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /pkg/manager/apk.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package manager 20 | 21 | import ( 22 | "bufio" 23 | "fmt" 24 | "os/exec" 25 | "strings" 26 | ) 27 | 28 | // APK represents the APK package manager 29 | type APK struct { 30 | rootCmd string 31 | } 32 | 33 | func (*APK) Exists() bool { 34 | _, err := exec.LookPath("apk") 35 | return err == nil 36 | } 37 | 38 | func (*APK) Name() string { 39 | return "apk" 40 | } 41 | 42 | func (*APK) Format() string { 43 | return "apk" 44 | } 45 | 46 | func (a *APK) SetRootCmd(s string) { 47 | a.rootCmd = s 48 | } 49 | 50 | func (a *APK) Sync(opts *Opts) error { 51 | opts = ensureOpts(opts) 52 | cmd := a.getCmd(opts, "apk", "update") 53 | setCmdEnv(cmd) 54 | err := cmd.Run() 55 | if err != nil { 56 | return fmt.Errorf("apk: sync: %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func (a *APK) Install(opts *Opts, pkgs ...string) error { 62 | opts = ensureOpts(opts) 63 | cmd := a.getCmd(opts, "apk", "add") 64 | cmd.Args = append(cmd.Args, pkgs...) 65 | setCmdEnv(cmd) 66 | err := cmd.Run() 67 | if err != nil { 68 | return fmt.Errorf("apk: install: %w", err) 69 | } 70 | return nil 71 | } 72 | 73 | func (a *APK) InstallLocal(opts *Opts, pkgs ...string) error { 74 | opts = ensureOpts(opts) 75 | cmd := a.getCmd(opts, "apk", "add", "--allow-untrusted") 76 | cmd.Args = append(cmd.Args, pkgs...) 77 | setCmdEnv(cmd) 78 | err := cmd.Run() 79 | if err != nil { 80 | return fmt.Errorf("apk: installlocal: %w", err) 81 | } 82 | return nil 83 | } 84 | 85 | func (a *APK) Remove(opts *Opts, pkgs ...string) error { 86 | opts = ensureOpts(opts) 87 | cmd := a.getCmd(opts, "apk", "del") 88 | cmd.Args = append(cmd.Args, pkgs...) 89 | setCmdEnv(cmd) 90 | err := cmd.Run() 91 | if err != nil { 92 | return fmt.Errorf("apk: remove: %w", err) 93 | } 94 | return nil 95 | } 96 | 97 | func (a *APK) Upgrade(opts *Opts, pkgs ...string) error { 98 | opts = ensureOpts(opts) 99 | cmd := a.getCmd(opts, "apk", "upgrade") 100 | cmd.Args = append(cmd.Args, pkgs...) 101 | setCmdEnv(cmd) 102 | err := cmd.Run() 103 | if err != nil { 104 | return fmt.Errorf("apk: upgrade: %w", err) 105 | } 106 | return nil 107 | } 108 | 109 | func (a *APK) UpgradeAll(opts *Opts) error { 110 | opts = ensureOpts(opts) 111 | return a.Upgrade(opts) 112 | } 113 | 114 | func (a *APK) ListInstalled(opts *Opts) (map[string]string, error) { 115 | out := map[string]string{} 116 | cmd := exec.Command("apk", "list", "-I") 117 | 118 | stdout, err := cmd.StdoutPipe() 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | err = cmd.Start() 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | scanner := bufio.NewScanner(stdout) 129 | for scanner.Scan() { 130 | name, info, ok := strings.Cut(scanner.Text(), "-") 131 | if !ok { 132 | continue 133 | } 134 | 135 | version, _, ok := strings.Cut(info, " ") 136 | if !ok { 137 | continue 138 | } 139 | 140 | out[name] = version 141 | } 142 | 143 | err = scanner.Err() 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | return out, nil 149 | } 150 | 151 | func (a *APK) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { 152 | var cmd *exec.Cmd 153 | if opts.AsRoot { 154 | cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd) 155 | cmd.Args = append(cmd.Args, opts.Args...) 156 | cmd.Args = append(cmd.Args, args...) 157 | } else { 158 | cmd = exec.Command(mgrCmd, args...) 159 | } 160 | 161 | if !opts.NoConfirm { 162 | cmd.Args = append(cmd.Args, "-i") 163 | } 164 | 165 | return cmd 166 | } 167 | -------------------------------------------------------------------------------- /pkg/manager/apt.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package manager 20 | 21 | import ( 22 | "bufio" 23 | "fmt" 24 | "os/exec" 25 | "strings" 26 | ) 27 | 28 | // APT represents the APT package manager 29 | type APT struct { 30 | rootCmd string 31 | } 32 | 33 | func (*APT) Exists() bool { 34 | _, err := exec.LookPath("apt") 35 | return err == nil 36 | } 37 | 38 | func (*APT) Name() string { 39 | return "apt" 40 | } 41 | 42 | func (*APT) Format() string { 43 | return "deb" 44 | } 45 | 46 | func (a *APT) SetRootCmd(s string) { 47 | a.rootCmd = s 48 | } 49 | 50 | func (a *APT) Sync(opts *Opts) error { 51 | opts = ensureOpts(opts) 52 | cmd := a.getCmd(opts, "apt", "update") 53 | setCmdEnv(cmd) 54 | err := cmd.Run() 55 | if err != nil { 56 | return fmt.Errorf("apt: sync: %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func (a *APT) Install(opts *Opts, pkgs ...string) error { 62 | opts = ensureOpts(opts) 63 | cmd := a.getCmd(opts, "apt", "install") 64 | cmd.Args = append(cmd.Args, pkgs...) 65 | setCmdEnv(cmd) 66 | err := cmd.Run() 67 | if err != nil { 68 | return fmt.Errorf("apt: install: %w", err) 69 | } 70 | return nil 71 | } 72 | 73 | func (a *APT) InstallLocal(opts *Opts, pkgs ...string) error { 74 | opts = ensureOpts(opts) 75 | return a.Install(opts, pkgs...) 76 | } 77 | 78 | func (a *APT) Remove(opts *Opts, pkgs ...string) error { 79 | opts = ensureOpts(opts) 80 | cmd := a.getCmd(opts, "apt", "remove") 81 | cmd.Args = append(cmd.Args, pkgs...) 82 | setCmdEnv(cmd) 83 | err := cmd.Run() 84 | if err != nil { 85 | return fmt.Errorf("apt: remove: %w", err) 86 | } 87 | return nil 88 | } 89 | 90 | func (a *APT) Upgrade(opts *Opts, pkgs ...string) error { 91 | opts = ensureOpts(opts) 92 | return a.Install(opts, pkgs...) 93 | } 94 | 95 | func (a *APT) UpgradeAll(opts *Opts) error { 96 | opts = ensureOpts(opts) 97 | cmd := a.getCmd(opts, "apt", "upgrade") 98 | setCmdEnv(cmd) 99 | err := cmd.Run() 100 | if err != nil { 101 | return fmt.Errorf("apt: upgradeall: %w", err) 102 | } 103 | return nil 104 | } 105 | 106 | func (a *APT) ListInstalled(opts *Opts) (map[string]string, error) { 107 | out := map[string]string{} 108 | cmd := exec.Command("dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W") 109 | 110 | stdout, err := cmd.StdoutPipe() 111 | if err != nil { 112 | return nil, err 113 | } 114 | 115 | err = cmd.Start() 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | scanner := bufio.NewScanner(stdout) 121 | for scanner.Scan() { 122 | name, version, ok := strings.Cut(scanner.Text(), "\u200b") 123 | if !ok { 124 | continue 125 | } 126 | out[name] = version 127 | } 128 | 129 | err = scanner.Err() 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | return out, nil 135 | } 136 | 137 | func (a *APT) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { 138 | var cmd *exec.Cmd 139 | if opts.AsRoot { 140 | cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd) 141 | cmd.Args = append(cmd.Args, opts.Args...) 142 | cmd.Args = append(cmd.Args, args...) 143 | } else { 144 | cmd = exec.Command(mgrCmd, args...) 145 | } 146 | 147 | if opts.NoConfirm { 148 | cmd.Args = append(cmd.Args, "-y") 149 | } 150 | 151 | return cmd 152 | } 153 | -------------------------------------------------------------------------------- /pkg/manager/dnf.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package manager 20 | 21 | import ( 22 | "bufio" 23 | "fmt" 24 | "os/exec" 25 | "strings" 26 | ) 27 | 28 | // DNF represents the DNF package manager 29 | type DNF struct { 30 | rootCmd string 31 | } 32 | 33 | func (*DNF) Exists() bool { 34 | _, err := exec.LookPath("dnf") 35 | return err == nil 36 | } 37 | 38 | func (*DNF) Name() string { 39 | return "dnf" 40 | } 41 | 42 | func (*DNF) Format() string { 43 | return "rpm" 44 | } 45 | 46 | func (d *DNF) SetRootCmd(s string) { 47 | d.rootCmd = s 48 | } 49 | 50 | func (d *DNF) Sync(opts *Opts) error { 51 | opts = ensureOpts(opts) 52 | cmd := d.getCmd(opts, "dnf", "upgrade") 53 | setCmdEnv(cmd) 54 | err := cmd.Run() 55 | if err != nil { 56 | return fmt.Errorf("dnf: sync: %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func (d *DNF) Install(opts *Opts, pkgs ...string) error { 62 | opts = ensureOpts(opts) 63 | cmd := d.getCmd(opts, "dnf", "install", "--allowerasing") 64 | cmd.Args = append(cmd.Args, pkgs...) 65 | setCmdEnv(cmd) 66 | err := cmd.Run() 67 | if err != nil { 68 | return fmt.Errorf("dnf: install: %w", err) 69 | } 70 | return nil 71 | } 72 | 73 | func (d *DNF) InstallLocal(opts *Opts, pkgs ...string) error { 74 | opts = ensureOpts(opts) 75 | return d.Install(opts, pkgs...) 76 | } 77 | 78 | func (d *DNF) Remove(opts *Opts, pkgs ...string) error { 79 | opts = ensureOpts(opts) 80 | cmd := d.getCmd(opts, "dnf", "remove") 81 | cmd.Args = append(cmd.Args, pkgs...) 82 | setCmdEnv(cmd) 83 | err := cmd.Run() 84 | if err != nil { 85 | return fmt.Errorf("dnf: remove: %w", err) 86 | } 87 | return nil 88 | } 89 | 90 | func (d *DNF) Upgrade(opts *Opts, pkgs ...string) error { 91 | opts = ensureOpts(opts) 92 | cmd := d.getCmd(opts, "dnf", "upgrade") 93 | cmd.Args = append(cmd.Args, pkgs...) 94 | setCmdEnv(cmd) 95 | err := cmd.Run() 96 | if err != nil { 97 | return fmt.Errorf("dnf: upgrade: %w", err) 98 | } 99 | return nil 100 | } 101 | 102 | func (d *DNF) UpgradeAll(opts *Opts) error { 103 | opts = ensureOpts(opts) 104 | cmd := d.getCmd(opts, "dnf", "upgrade") 105 | setCmdEnv(cmd) 106 | err := cmd.Run() 107 | if err != nil { 108 | return fmt.Errorf("dnf: upgradeall: %w", err) 109 | } 110 | return nil 111 | } 112 | 113 | func (d *DNF) ListInstalled(opts *Opts) (map[string]string, error) { 114 | out := map[string]string{} 115 | cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n") 116 | 117 | stdout, err := cmd.StdoutPipe() 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | err = cmd.Start() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | scanner := bufio.NewScanner(stdout) 128 | for scanner.Scan() { 129 | name, version, ok := strings.Cut(scanner.Text(), "\u200b") 130 | if !ok { 131 | continue 132 | } 133 | version = strings.TrimPrefix(version, "0:") 134 | out[name] = version 135 | } 136 | 137 | err = scanner.Err() 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return out, nil 143 | } 144 | 145 | func (d *DNF) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { 146 | var cmd *exec.Cmd 147 | if opts.AsRoot { 148 | cmd = exec.Command(getRootCmd(d.rootCmd), mgrCmd) 149 | cmd.Args = append(cmd.Args, opts.Args...) 150 | cmd.Args = append(cmd.Args, args...) 151 | } else { 152 | cmd = exec.Command(mgrCmd, args...) 153 | } 154 | 155 | if opts.NoConfirm { 156 | cmd.Args = append(cmd.Args, "-y") 157 | } 158 | 159 | return cmd 160 | } 161 | -------------------------------------------------------------------------------- /pkg/manager/managers.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package manager 20 | 21 | import ( 22 | "os" 23 | "os/exec" 24 | ) 25 | 26 | var Args []string 27 | 28 | type Opts struct { 29 | AsRoot bool 30 | NoConfirm bool 31 | Args []string 32 | } 33 | 34 | var DefaultOpts = &Opts{ 35 | AsRoot: true, 36 | NoConfirm: false, 37 | } 38 | 39 | // DefaultRootCmd is the command used for privilege elevation by default 40 | var DefaultRootCmd = "sudo" 41 | 42 | var managers = []Manager{ 43 | &Pacman{}, 44 | &APT{}, 45 | &DNF{}, 46 | &YUM{}, 47 | &APK{}, 48 | &Zypper{}, 49 | } 50 | 51 | // Register registers a new package manager 52 | func Register(m Manager) { 53 | managers = append(managers, m) 54 | } 55 | 56 | // Manager represents a system package manager 57 | type Manager interface { 58 | // Name returns the name of the manager. 59 | Name() string 60 | // Format returns the packaging format of the manager. 61 | // Examples: rpm, deb, apk 62 | Format() string 63 | // Returns true if the package manager exists on the system. 64 | Exists() bool 65 | // Sets the command used to elevate privileges. Defaults to DefaultRootCmd. 66 | SetRootCmd(string) 67 | // Sync fetches repositories without installing anything 68 | Sync(*Opts) error 69 | // Install installs packages 70 | Install(*Opts, ...string) error 71 | // Remove uninstalls packages 72 | Remove(*Opts, ...string) error 73 | // Upgrade upgrades packages 74 | Upgrade(*Opts, ...string) error 75 | // InstallLocal installs packages from local files rather than repos 76 | InstallLocal(*Opts, ...string) error 77 | // UpgradeAll upgrades all packages 78 | UpgradeAll(*Opts) error 79 | // ListInstalled returns all installed packages mapped to their versions 80 | ListInstalled(*Opts) (map[string]string, error) 81 | } 82 | 83 | // Detect returns the package manager detected on the system 84 | func Detect() Manager { 85 | for _, mgr := range managers { 86 | if mgr.Exists() { 87 | return mgr 88 | } 89 | } 90 | return nil 91 | } 92 | 93 | // Get returns the package manager with the given name 94 | func Get(name string) Manager { 95 | for _, mgr := range managers { 96 | if mgr.Name() == name { 97 | return mgr 98 | } 99 | } 100 | return nil 101 | } 102 | 103 | // getRootCmd returns rootCmd if it's not empty, otherwise returns DefaultRootCmd 104 | func getRootCmd(rootCmd string) string { 105 | if rootCmd != "" { 106 | return rootCmd 107 | } 108 | return DefaultRootCmd 109 | } 110 | 111 | func setCmdEnv(cmd *exec.Cmd) { 112 | cmd.Env = os.Environ() 113 | cmd.Stdin = os.Stdin 114 | cmd.Stdout = os.Stdout 115 | cmd.Stderr = os.Stderr 116 | } 117 | 118 | func ensureOpts(opts *Opts) *Opts { 119 | if opts == nil { 120 | opts = DefaultOpts 121 | } 122 | opts.Args = append(opts.Args, Args...) 123 | return opts 124 | } 125 | -------------------------------------------------------------------------------- /pkg/manager/pacman.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package manager 20 | 21 | import ( 22 | "bufio" 23 | "fmt" 24 | "os/exec" 25 | "strings" 26 | ) 27 | 28 | // Pacman represents the Pacman package manager 29 | type Pacman struct { 30 | rootCmd string 31 | } 32 | 33 | func (*Pacman) Exists() bool { 34 | _, err := exec.LookPath("pacman") 35 | return err == nil 36 | } 37 | 38 | func (*Pacman) Name() string { 39 | return "pacman" 40 | } 41 | 42 | func (*Pacman) Format() string { 43 | return "archlinux" 44 | } 45 | 46 | func (p *Pacman) SetRootCmd(s string) { 47 | p.rootCmd = s 48 | } 49 | 50 | func (p *Pacman) Sync(opts *Opts) error { 51 | opts = ensureOpts(opts) 52 | cmd := p.getCmd(opts, "pacman", "-Sy") 53 | setCmdEnv(cmd) 54 | err := cmd.Run() 55 | if err != nil { 56 | return fmt.Errorf("pacman: sync: %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func (p *Pacman) Install(opts *Opts, pkgs ...string) error { 62 | opts = ensureOpts(opts) 63 | cmd := p.getCmd(opts, "pacman", "-S", "--needed") 64 | cmd.Args = append(cmd.Args, pkgs...) 65 | setCmdEnv(cmd) 66 | err := cmd.Run() 67 | if err != nil { 68 | return fmt.Errorf("pacman: install: %w", err) 69 | } 70 | return nil 71 | } 72 | 73 | func (p *Pacman) InstallLocal(opts *Opts, pkgs ...string) error { 74 | opts = ensureOpts(opts) 75 | cmd := p.getCmd(opts, "pacman", "-U", "--needed") 76 | cmd.Args = append(cmd.Args, pkgs...) 77 | setCmdEnv(cmd) 78 | err := cmd.Run() 79 | if err != nil { 80 | return fmt.Errorf("pacman: installlocal: %w", err) 81 | } 82 | return nil 83 | } 84 | 85 | func (p *Pacman) Remove(opts *Opts, pkgs ...string) error { 86 | opts = ensureOpts(opts) 87 | cmd := p.getCmd(opts, "pacman", "-R") 88 | cmd.Args = append(cmd.Args, pkgs...) 89 | setCmdEnv(cmd) 90 | err := cmd.Run() 91 | if err != nil { 92 | return fmt.Errorf("pacman: remove: %w", err) 93 | } 94 | return nil 95 | } 96 | 97 | func (p *Pacman) Upgrade(opts *Opts, pkgs ...string) error { 98 | opts = ensureOpts(opts) 99 | return p.Install(opts, pkgs...) 100 | } 101 | 102 | func (p *Pacman) UpgradeAll(opts *Opts) error { 103 | opts = ensureOpts(opts) 104 | cmd := p.getCmd(opts, "pacman", "-Su") 105 | setCmdEnv(cmd) 106 | err := cmd.Run() 107 | if err != nil { 108 | return fmt.Errorf("pacman: upgradeall: %w", err) 109 | } 110 | return nil 111 | } 112 | 113 | func (p *Pacman) ListInstalled(opts *Opts) (map[string]string, error) { 114 | out := map[string]string{} 115 | cmd := exec.Command("pacman", "-Q") 116 | 117 | stdout, err := cmd.StdoutPipe() 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | err = cmd.Start() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | scanner := bufio.NewScanner(stdout) 128 | for scanner.Scan() { 129 | name, version, ok := strings.Cut(scanner.Text(), " ") 130 | if !ok { 131 | continue 132 | } 133 | out[name] = version 134 | } 135 | 136 | err = scanner.Err() 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | return out, nil 142 | } 143 | 144 | func (p *Pacman) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { 145 | var cmd *exec.Cmd 146 | if opts.AsRoot { 147 | cmd = exec.Command(getRootCmd(p.rootCmd), mgrCmd) 148 | cmd.Args = append(cmd.Args, opts.Args...) 149 | cmd.Args = append(cmd.Args, args...) 150 | } else { 151 | cmd = exec.Command(mgrCmd, args...) 152 | } 153 | 154 | if opts.NoConfirm { 155 | cmd.Args = append(cmd.Args, "--noconfirm") 156 | } 157 | 158 | return cmd 159 | } 160 | -------------------------------------------------------------------------------- /pkg/manager/yum.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package manager 20 | 21 | import ( 22 | "bufio" 23 | "fmt" 24 | "os/exec" 25 | "strings" 26 | ) 27 | 28 | // YUM represents the YUM package manager 29 | type YUM struct { 30 | rootCmd string 31 | } 32 | 33 | func (*YUM) Exists() bool { 34 | _, err := exec.LookPath("yum") 35 | return err == nil 36 | } 37 | 38 | func (*YUM) Name() string { 39 | return "yum" 40 | } 41 | 42 | func (*YUM) Format() string { 43 | return "rpm" 44 | } 45 | 46 | func (y *YUM) SetRootCmd(s string) { 47 | y.rootCmd = s 48 | } 49 | 50 | func (y *YUM) Sync(opts *Opts) error { 51 | opts = ensureOpts(opts) 52 | cmd := y.getCmd(opts, "yum", "upgrade") 53 | setCmdEnv(cmd) 54 | err := cmd.Run() 55 | if err != nil { 56 | return fmt.Errorf("yum: sync: %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func (y *YUM) Install(opts *Opts, pkgs ...string) error { 62 | opts = ensureOpts(opts) 63 | cmd := y.getCmd(opts, "yum", "install", "--allowerasing") 64 | cmd.Args = append(cmd.Args, pkgs...) 65 | setCmdEnv(cmd) 66 | err := cmd.Run() 67 | if err != nil { 68 | return fmt.Errorf("yum: install: %w", err) 69 | } 70 | return nil 71 | } 72 | 73 | func (y *YUM) InstallLocal(opts *Opts, pkgs ...string) error { 74 | opts = ensureOpts(opts) 75 | return y.Install(opts, pkgs...) 76 | } 77 | 78 | func (y *YUM) Remove(opts *Opts, pkgs ...string) error { 79 | opts = ensureOpts(opts) 80 | cmd := y.getCmd(opts, "yum", "remove") 81 | cmd.Args = append(cmd.Args, pkgs...) 82 | setCmdEnv(cmd) 83 | err := cmd.Run() 84 | if err != nil { 85 | return fmt.Errorf("yum: remove: %w", err) 86 | } 87 | return nil 88 | } 89 | 90 | func (y *YUM) Upgrade(opts *Opts, pkgs ...string) error { 91 | opts = ensureOpts(opts) 92 | cmd := y.getCmd(opts, "yum", "upgrade") 93 | cmd.Args = append(cmd.Args, pkgs...) 94 | setCmdEnv(cmd) 95 | err := cmd.Run() 96 | if err != nil { 97 | return fmt.Errorf("yum: upgrade: %w", err) 98 | } 99 | return nil 100 | } 101 | 102 | func (y *YUM) UpgradeAll(opts *Opts) error { 103 | opts = ensureOpts(opts) 104 | cmd := y.getCmd(opts, "yum", "upgrade") 105 | setCmdEnv(cmd) 106 | err := cmd.Run() 107 | if err != nil { 108 | return fmt.Errorf("yum: upgradeall: %w", err) 109 | } 110 | return nil 111 | } 112 | 113 | func (y *YUM) ListInstalled(opts *Opts) (map[string]string, error) { 114 | out := map[string]string{} 115 | cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n") 116 | 117 | stdout, err := cmd.StdoutPipe() 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | err = cmd.Start() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | scanner := bufio.NewScanner(stdout) 128 | for scanner.Scan() { 129 | name, version, ok := strings.Cut(scanner.Text(), "\u200b") 130 | if !ok { 131 | continue 132 | } 133 | version = strings.TrimPrefix(version, "0:") 134 | out[name] = version 135 | } 136 | 137 | err = scanner.Err() 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return out, nil 143 | } 144 | 145 | func (y *YUM) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { 146 | var cmd *exec.Cmd 147 | if opts.AsRoot { 148 | cmd = exec.Command(getRootCmd(y.rootCmd), mgrCmd) 149 | cmd.Args = append(cmd.Args, opts.Args...) 150 | cmd.Args = append(cmd.Args, args...) 151 | } else { 152 | cmd = exec.Command(mgrCmd, args...) 153 | } 154 | 155 | if opts.NoConfirm { 156 | cmd.Args = append(cmd.Args, "-y") 157 | } 158 | 159 | return cmd 160 | } 161 | -------------------------------------------------------------------------------- /pkg/manager/zypper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package manager 20 | 21 | import ( 22 | "bufio" 23 | "fmt" 24 | "os/exec" 25 | "strings" 26 | ) 27 | 28 | // Zypper represents the Zypper package manager 29 | type Zypper struct { 30 | rootCmd string 31 | } 32 | 33 | func (*Zypper) Exists() bool { 34 | _, err := exec.LookPath("zypper") 35 | return err == nil 36 | } 37 | 38 | func (*Zypper) Name() string { 39 | return "zypper" 40 | } 41 | 42 | func (*Zypper) Format() string { 43 | return "rpm" 44 | } 45 | 46 | func (z *Zypper) SetRootCmd(s string) { 47 | z.rootCmd = s 48 | } 49 | 50 | func (z *Zypper) Sync(opts *Opts) error { 51 | opts = ensureOpts(opts) 52 | cmd := z.getCmd(opts, "zypper", "refresh") 53 | setCmdEnv(cmd) 54 | err := cmd.Run() 55 | if err != nil { 56 | return fmt.Errorf("zypper: sync: %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func (z *Zypper) Install(opts *Opts, pkgs ...string) error { 62 | opts = ensureOpts(opts) 63 | cmd := z.getCmd(opts, "zypper", "install", "-y") 64 | cmd.Args = append(cmd.Args, pkgs...) 65 | setCmdEnv(cmd) 66 | err := cmd.Run() 67 | if err != nil { 68 | return fmt.Errorf("zypper: install: %w", err) 69 | } 70 | return nil 71 | } 72 | 73 | func (z *Zypper) InstallLocal(opts *Opts, pkgs ...string) error { 74 | opts = ensureOpts(opts) 75 | return z.Install(opts, pkgs...) 76 | } 77 | 78 | func (z *Zypper) Remove(opts *Opts, pkgs ...string) error { 79 | opts = ensureOpts(opts) 80 | cmd := z.getCmd(opts, "zypper", "remove", "-y") 81 | cmd.Args = append(cmd.Args, pkgs...) 82 | setCmdEnv(cmd) 83 | err := cmd.Run() 84 | if err != nil { 85 | return fmt.Errorf("zypper: remove: %w", err) 86 | } 87 | return nil 88 | } 89 | 90 | func (z *Zypper) Upgrade(opts *Opts, pkgs ...string) error { 91 | opts = ensureOpts(opts) 92 | cmd := z.getCmd(opts, "zypper", "update", "-y") 93 | cmd.Args = append(cmd.Args, pkgs...) 94 | setCmdEnv(cmd) 95 | err := cmd.Run() 96 | if err != nil { 97 | return fmt.Errorf("zypper: upgrade: %w", err) 98 | } 99 | return nil 100 | } 101 | 102 | func (z *Zypper) UpgradeAll(opts *Opts) error { 103 | opts = ensureOpts(opts) 104 | cmd := z.getCmd(opts, "zypper", "update", "-y") 105 | setCmdEnv(cmd) 106 | err := cmd.Run() 107 | if err != nil { 108 | return fmt.Errorf("zypper: upgradeall: %w", err) 109 | } 110 | return nil 111 | } 112 | 113 | func (z *Zypper) ListInstalled(opts *Opts) (map[string]string, error) { 114 | out := map[string]string{} 115 | cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n") 116 | 117 | stdout, err := cmd.StdoutPipe() 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | err = cmd.Start() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | scanner := bufio.NewScanner(stdout) 128 | for scanner.Scan() { 129 | name, version, ok := strings.Cut(scanner.Text(), "\u200b") 130 | if !ok { 131 | continue 132 | } 133 | version = strings.TrimPrefix(version, "0:") 134 | out[name] = version 135 | } 136 | 137 | err = scanner.Err() 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return out, nil 143 | } 144 | 145 | func (z *Zypper) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { 146 | var cmd *exec.Cmd 147 | if opts.AsRoot { 148 | cmd = exec.Command(getRootCmd(z.rootCmd), mgrCmd) 149 | cmd.Args = append(cmd.Args, opts.Args...) 150 | cmd.Args = append(cmd.Args, args...) 151 | } else { 152 | cmd = exec.Command(mgrCmd, args...) 153 | } 154 | 155 | if opts.NoConfirm { 156 | cmd.Args = append(cmd.Args, "-y") 157 | } 158 | 159 | return cmd 160 | } 161 | -------------------------------------------------------------------------------- /pkg/repos/find.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package repos 20 | 21 | import ( 22 | "context" 23 | 24 | "lure.sh/lure/internal/db" 25 | ) 26 | 27 | // FindPkgs looks for packages matching the inputs inside the database. 28 | // It returns a map that maps the package name input to any packages found for it. 29 | // It also returns a slice that contains the names of all packages that were not found. 30 | func FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) { 31 | found := map[string][]db.Package{} 32 | notFound := []string(nil) 33 | 34 | for _, pkgName := range pkgs { 35 | if pkgName == "" { 36 | continue 37 | } 38 | 39 | result, err := db.GetPkgs(ctx, "json_array_contains(provides, ?)", pkgName) 40 | if err != nil { 41 | return nil, nil, err 42 | } 43 | 44 | added := 0 45 | for result.Next() { 46 | var pkg db.Package 47 | err = result.StructScan(&pkg) 48 | if err != nil { 49 | return nil, nil, err 50 | } 51 | 52 | added++ 53 | found[pkgName] = append(found[pkgName], pkg) 54 | } 55 | result.Close() 56 | 57 | if added == 0 { 58 | result, err := db.GetPkgs(ctx, "name LIKE ?", pkgName) 59 | if err != nil { 60 | return nil, nil, err 61 | } 62 | 63 | for result.Next() { 64 | var pkg db.Package 65 | err = result.StructScan(&pkg) 66 | if err != nil { 67 | return nil, nil, err 68 | } 69 | 70 | added++ 71 | found[pkgName] = append(found[pkgName], pkg) 72 | } 73 | 74 | result.Close() 75 | } 76 | 77 | if added == 0 { 78 | notFound = append(notFound, pkgName) 79 | } 80 | } 81 | 82 | return found, notFound, nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/repos/find_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package repos_test 20 | 21 | import ( 22 | "context" 23 | "reflect" 24 | "strings" 25 | "testing" 26 | 27 | "lure.sh/lure/internal/db" 28 | "lure.sh/lure/internal/types" 29 | "lure.sh/lure/pkg/repos" 30 | ) 31 | 32 | func TestFindPkgs(t *testing.T) { 33 | _, err := db.Open(":memory:") 34 | if err != nil { 35 | t.Fatalf("Expected no error, got %s", err) 36 | } 37 | defer db.Close() 38 | 39 | setCfgDirs(t) 40 | defer removeCacheDir(t) 41 | 42 | ctx := context.Background() 43 | 44 | err = repos.Pull(ctx, []types.Repo{ 45 | { 46 | Name: "default", 47 | URL: "https://github.com/Arsen6331/lure-repo.git", 48 | }, 49 | }) 50 | if err != nil { 51 | t.Fatalf("Expected no error, got %s", err) 52 | } 53 | 54 | found, notFound, err := repos.FindPkgs([]string{"itd", "nonexistentpackage1", "nonexistentpackage2"}) 55 | if err != nil { 56 | t.Fatalf("Expected no error, got %s", err) 57 | } 58 | 59 | if !reflect.DeepEqual(notFound, []string{"nonexistentpackage1", "nonexistentpackage2"}) { 60 | t.Errorf("Expected 'nonexistentpackage{1,2} not to be found") 61 | } 62 | 63 | if len(found) != 1 { 64 | t.Errorf("Expected 1 package found, got %d", len(found)) 65 | } 66 | 67 | itdPkgs, ok := found["itd"] 68 | if !ok { 69 | t.Fatalf("Expected 'itd' packages to be found") 70 | } 71 | 72 | if len(itdPkgs) < 2 { 73 | t.Errorf("Expected two 'itd' packages to be found") 74 | } 75 | 76 | for i, pkg := range itdPkgs { 77 | if !strings.HasPrefix(pkg.Name, "itd") { 78 | t.Errorf("Expected package name of all found packages to start with 'itd', got %s on element %d", pkg.Name, i) 79 | } 80 | } 81 | } 82 | 83 | func TestFindPkgsEmpty(t *testing.T) { 84 | _, err := db.Open(":memory:") 85 | if err != nil { 86 | t.Fatalf("Expected no error, got %s", err) 87 | } 88 | defer db.Close() 89 | 90 | setCfgDirs(t) 91 | defer removeCacheDir(t) 92 | 93 | err = db.InsertPackage(db.Package{ 94 | Name: "test1", 95 | Repository: "default", 96 | Version: "0.0.1", 97 | Release: 1, 98 | Description: db.NewJSON(map[string]string{ 99 | "en": "Test package 1", 100 | "ru": "Проверочный пакет 1", 101 | }), 102 | Provides: db.NewJSON([]string{""}), 103 | }) 104 | if err != nil { 105 | t.Fatalf("Expected no error, got %s", err) 106 | } 107 | 108 | err = db.InsertPackage(db.Package{ 109 | Name: "test2", 110 | Repository: "default", 111 | Version: "0.0.1", 112 | Release: 1, 113 | Description: db.NewJSON(map[string]string{ 114 | "en": "Test package 2", 115 | "ru": "Проверочный пакет 2", 116 | }), 117 | Provides: db.NewJSON([]string{"test"}), 118 | }) 119 | if err != nil { 120 | t.Fatalf("Expected no error, got %s", err) 121 | } 122 | 123 | found, notFound, err := repos.FindPkgs([]string{"test", ""}) 124 | if err != nil { 125 | t.Fatalf("Expected no error, got %s", err) 126 | } 127 | 128 | if len(notFound) != 0 { 129 | t.Errorf("Expected all packages to be found") 130 | } 131 | 132 | if len(found) != 1 { 133 | t.Errorf("Expected 1 package found, got %d", len(found)) 134 | } 135 | 136 | testPkgs, ok := found["test"] 137 | if !ok { 138 | t.Fatalf("Expected 'test' packages to be found") 139 | } 140 | 141 | if len(testPkgs) != 1 { 142 | t.Errorf("Expected one 'test' package to be found, got %d", len(testPkgs)) 143 | } 144 | 145 | if testPkgs[0].Name != "test2" { 146 | t.Errorf("Expected 'test2' package, got '%s'", testPkgs[0].Name) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /pkg/repos/pull_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package repos_test 20 | 21 | import ( 22 | "context" 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | 27 | "lure.sh/lure/internal/config" 28 | "lure.sh/lure/internal/db" 29 | "lure.sh/lure/internal/types" 30 | "lure.sh/lure/pkg/repos" 31 | ) 32 | 33 | func setCfgDirs(t *testing.T) { 34 | t.Helper() 35 | 36 | paths := config.GetPaths() 37 | 38 | var err error 39 | paths.CacheDir, err = os.MkdirTemp("/tmp", "lure-pull-test.*") 40 | if err != nil { 41 | t.Fatalf("Expected no error, got %s", err) 42 | } 43 | 44 | paths.RepoDir = filepath.Join(paths.CacheDir, "repo") 45 | paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs") 46 | 47 | err = os.MkdirAll(paths.RepoDir, 0o755) 48 | if err != nil { 49 | t.Fatalf("Expected no error, got %s", err) 50 | } 51 | 52 | err = os.MkdirAll(paths.PkgsDir, 0o755) 53 | if err != nil { 54 | t.Fatalf("Expected no error, got %s", err) 55 | } 56 | 57 | paths.DBPath = filepath.Join(paths.CacheDir, "db") 58 | } 59 | 60 | func removeCacheDir(t *testing.T) { 61 | t.Helper() 62 | 63 | err := os.RemoveAll(config.GetPaths().CacheDir) 64 | if err != nil { 65 | t.Fatalf("Expected no error, got %s", err) 66 | } 67 | } 68 | 69 | func TestPull(t *testing.T) { 70 | _, err := db.Open(":memory:") 71 | if err != nil { 72 | t.Fatalf("Expected no error, got %s", err) 73 | } 74 | defer db.Close() 75 | 76 | setCfgDirs(t) 77 | defer removeCacheDir(t) 78 | 79 | ctx := context.Background() 80 | 81 | err = repos.Pull(ctx, []types.Repo{ 82 | { 83 | Name: "default", 84 | URL: "https://github.com/Arsen6331/lure-repo.git", 85 | }, 86 | }) 87 | if err != nil { 88 | t.Fatalf("Expected no error, got %s", err) 89 | } 90 | 91 | result, err := db.GetPkgs("name LIKE 'itd%'") 92 | if err != nil { 93 | t.Fatalf("Expected no error, got %s", err) 94 | } 95 | 96 | var pkgAmt int 97 | for result.Next() { 98 | var dbPkg db.Package 99 | err = result.StructScan(&dbPkg) 100 | if err != nil { 101 | t.Errorf("Expected no error, got %s", err) 102 | } 103 | pkgAmt++ 104 | } 105 | 106 | if pkgAmt < 2 { 107 | t.Errorf("Expected 2 packages to match, got %d", pkgAmt) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pkg/search/search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "io/fs" 8 | "os" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | 13 | "lure.sh/lure/internal/config" 14 | "lure.sh/lure/internal/db" 15 | ) 16 | 17 | // Filter represents search filters. 18 | type Filter int 19 | 20 | // Filters 21 | const ( 22 | FilterNone Filter = iota 23 | FilterInRepo 24 | FilterSupportsArch 25 | ) 26 | 27 | // SoryBy represents a value that packages can be sorted by. 28 | type SortBy int 29 | 30 | // Sort values 31 | const ( 32 | SortByNone = iota 33 | SortByName 34 | SortByRepo 35 | SortByVersion 36 | ) 37 | 38 | // Package represents a package from LURE's database 39 | type Package struct { 40 | Name string 41 | Version string 42 | Release int 43 | Epoch uint 44 | Description map[string]string 45 | Homepage map[string]string 46 | Maintainer map[string]string 47 | Architectures []string 48 | Licenses []string 49 | Provides []string 50 | Conflicts []string 51 | Replaces []string 52 | Depends map[string][]string 53 | BuildDepends map[string][]string 54 | OptDepends map[string][]string 55 | Repository string 56 | } 57 | 58 | func convertPkg(p db.Package) Package { 59 | return Package{ 60 | Name: p.Name, 61 | Version: p.Version, 62 | Release: p.Release, 63 | Epoch: p.Epoch, 64 | Description: p.Description.Val, 65 | Homepage: p.Homepage.Val, 66 | Maintainer: p.Maintainer.Val, 67 | Architectures: p.Architectures.Val, 68 | Licenses: p.Licenses.Val, 69 | Provides: p.Provides.Val, 70 | Conflicts: p.Conflicts.Val, 71 | Replaces: p.Replaces.Val, 72 | Depends: p.Depends.Val, 73 | BuildDepends: p.BuildDepends.Val, 74 | OptDepends: p.OptDepends.Val, 75 | Repository: p.Repository, 76 | } 77 | } 78 | 79 | // Options contains the options for a search. 80 | type Options struct { 81 | Filter Filter 82 | FilterValue string 83 | SortBy SortBy 84 | Limit int64 85 | Query string 86 | } 87 | 88 | // Search searches for packages in the database based on the given options. 89 | func Search(ctx context.Context, opts Options) ([]Package, error) { 90 | query := "(name LIKE ? OR description LIKE ? OR json_array_contains(provides, ?))" 91 | args := []any{"%" + opts.Query + "%", "%" + opts.Query + "%", opts.Query} 92 | 93 | if opts.Filter != FilterNone { 94 | switch opts.Filter { 95 | case FilterInRepo: 96 | query += " AND repository = ?" 97 | case FilterSupportsArch: 98 | query += " AND json_array_contains(architectures, ?)" 99 | } 100 | args = append(args, opts.FilterValue) 101 | } 102 | 103 | if opts.SortBy != SortByNone { 104 | switch opts.SortBy { 105 | case SortByName: 106 | query += " ORDER BY name" 107 | case SortByRepo: 108 | query += " ORDER BY repository" 109 | case SortByVersion: 110 | query += " ORDER BY version" 111 | } 112 | } 113 | 114 | if opts.Limit != 0 { 115 | query += " LIMIT " + strconv.FormatInt(opts.Limit, 10) 116 | } 117 | 118 | result, err := db.GetPkgs(ctx, query, args...) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | var out []Package 124 | for result.Next() { 125 | pkg := db.Package{} 126 | err = result.StructScan(&pkg) 127 | if err != nil { 128 | return nil, err 129 | } 130 | out = append(out, convertPkg(pkg)) 131 | } 132 | 133 | return out, err 134 | } 135 | 136 | // GetPkg gets a single package from the database and returns it. 137 | func GetPkg(ctx context.Context, repo, name string) (Package, error) { 138 | pkg, err := db.GetPkg(ctx, "name = ? AND repository = ?", name, repo) 139 | return convertPkg(*pkg), err 140 | } 141 | 142 | var ( 143 | // ErrInvalidArgument is an error returned by GetScript when one of its arguments 144 | // contain invalid characters 145 | ErrInvalidArgument = errors.New("name and repository must not contain . or /") 146 | 147 | // ErrScriptNotFound is returned by GetScript if it can't find the script requested 148 | // by the user. 149 | ErrScriptNotFound = errors.New("requested script not found") 150 | ) 151 | 152 | // GetScript returns a reader containing the build script for a given package. 153 | func GetScript(ctx context.Context, repo, name string) (io.ReadCloser, error) { 154 | if strings.Contains(name, "./") || strings.ContainsAny(repo, "./") { 155 | return nil, ErrInvalidArgument 156 | } 157 | 158 | scriptPath := filepath.Join(config.GetPaths(ctx).RepoDir, repo, name, "lure.sh") 159 | fl, err := os.Open(scriptPath) 160 | if errors.Is(err, fs.ErrNotExist) { 161 | return nil, ErrScriptNotFound 162 | } else if err != nil { 163 | return nil, err 164 | } 165 | 166 | return fl, nil 167 | } 168 | -------------------------------------------------------------------------------- /repo.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/pelletier/go-toml/v2" 26 | "github.com/urfave/cli/v2" 27 | "lure.sh/lure/internal/config" 28 | "lure.sh/lure/internal/db" 29 | "lure.sh/lure/internal/types" 30 | "lure.sh/lure/pkg/loggerctx" 31 | "lure.sh/lure/pkg/repos" 32 | "golang.org/x/exp/slices" 33 | ) 34 | 35 | var addrepoCmd = &cli.Command{ 36 | Name: "addrepo", 37 | Usage: "Add a new repository", 38 | Aliases: []string{"ar"}, 39 | Flags: []cli.Flag{ 40 | &cli.StringFlag{ 41 | Name: "name", 42 | Aliases: []string{"n"}, 43 | Required: true, 44 | Usage: "Name of the new repo", 45 | }, 46 | &cli.StringFlag{ 47 | Name: "url", 48 | Aliases: []string{"u"}, 49 | Required: true, 50 | Usage: "URL of the new repo", 51 | }, 52 | }, 53 | Action: func(c *cli.Context) error { 54 | ctx := c.Context 55 | log := loggerctx.From(ctx) 56 | 57 | name := c.String("name") 58 | repoURL := c.String("url") 59 | 60 | cfg := config.Config(ctx) 61 | 62 | for _, repo := range cfg.Repos { 63 | if repo.URL == repoURL { 64 | log.Fatal("Repo already exists").Str("name", repo.Name).Send() 65 | } 66 | } 67 | 68 | cfg.Repos = append(cfg.Repos, types.Repo{ 69 | Name: name, 70 | URL: repoURL, 71 | }) 72 | 73 | cfgFl, err := os.Create(config.GetPaths(ctx).ConfigPath) 74 | if err != nil { 75 | log.Fatal("Error opening config file").Err(err).Send() 76 | } 77 | 78 | err = toml.NewEncoder(cfgFl).Encode(cfg) 79 | if err != nil { 80 | log.Fatal("Error encoding config").Err(err).Send() 81 | } 82 | 83 | err = repos.Pull(ctx, cfg.Repos) 84 | if err != nil { 85 | log.Fatal("Error pulling repos").Err(err).Send() 86 | } 87 | 88 | return nil 89 | }, 90 | } 91 | 92 | var removerepoCmd = &cli.Command{ 93 | Name: "removerepo", 94 | Usage: "Remove an existing repository", 95 | Aliases: []string{"rr"}, 96 | Flags: []cli.Flag{ 97 | &cli.StringFlag{ 98 | Name: "name", 99 | Aliases: []string{"n"}, 100 | Required: true, 101 | Usage: "Name of the repo to be deleted", 102 | }, 103 | }, 104 | Action: func(c *cli.Context) error { 105 | ctx := c.Context 106 | log := loggerctx.From(ctx) 107 | 108 | name := c.String("name") 109 | cfg := config.Config(ctx) 110 | 111 | found := false 112 | index := 0 113 | for i, repo := range cfg.Repos { 114 | if repo.Name == name { 115 | index = i 116 | found = true 117 | } 118 | } 119 | if !found { 120 | log.Fatal("Repo does not exist").Str("name", name).Send() 121 | } 122 | 123 | cfg.Repos = slices.Delete(cfg.Repos, index, index+1) 124 | 125 | cfgFl, err := os.Create(config.GetPaths(ctx).ConfigPath) 126 | if err != nil { 127 | log.Fatal("Error opening config file").Err(err).Send() 128 | } 129 | 130 | err = toml.NewEncoder(cfgFl).Encode(&cfg) 131 | if err != nil { 132 | log.Fatal("Error encoding config").Err(err).Send() 133 | } 134 | 135 | err = os.RemoveAll(filepath.Join(config.GetPaths(ctx).RepoDir, name)) 136 | if err != nil { 137 | log.Fatal("Error removing repo directory").Err(err).Send() 138 | } 139 | 140 | err = db.DeletePkgs(ctx, "repository = ?", name) 141 | if err != nil { 142 | log.Fatal("Error removing packages from database").Err(err).Send() 143 | } 144 | 145 | return nil 146 | }, 147 | } 148 | 149 | var refreshCmd = &cli.Command{ 150 | Name: "refresh", 151 | Usage: "Pull all repositories that have changed", 152 | Aliases: []string{"ref"}, 153 | Action: func(c *cli.Context) error { 154 | ctx := c.Context 155 | log := loggerctx.From(ctx) 156 | err := repos.Pull(ctx, config.Config(ctx).Repos) 157 | if err != nil { 158 | log.Fatal("Error pulling repos").Err(err).Send() 159 | } 160 | return nil 161 | }, 162 | } 163 | -------------------------------------------------------------------------------- /scripts/completion/bash: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | : ${PROG:=$(basename ${BASH_SOURCE})} 4 | 5 | _cli_bash_autocomplete() { 6 | if [[ "${COMP_WORDS[0]}" != "source" ]]; then 7 | local cur opts base 8 | COMPREPLY=() 9 | cur="${COMP_WORDS[COMP_CWORD]}" 10 | if [[ "$cur" == "-"* ]]; then 11 | opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) 12 | else 13 | opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) 14 | fi 15 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 16 | return 0 17 | fi 18 | } 19 | 20 | complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG 21 | unset PROG -------------------------------------------------------------------------------- /scripts/completion/zsh: -------------------------------------------------------------------------------- 1 | #compdef lure 2 | 3 | _cli_zsh_autocomplete() { 4 | local -a opts 5 | local cur 6 | cur=${words[-1]} 7 | if [[ "$cur" == "-"* ]]; then 8 | opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") 9 | else 10 | opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") 11 | fi 12 | 13 | if [[ "${opts[1]}" != "" ]]; then 14 | _describe 'values' opts 15 | else 16 | _files 17 | fi 18 | } 19 | 20 | compdef _cli_zsh_autocomplete lure 21 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # LURE - Linux User REpository 4 | # Copyright (C) 2023 Elara Musayelyan 5 | # 6 | # This program is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program. If not, see . 18 | 19 | info() { 20 | echo $'\x1b[32m[INFO]\x1b[0m' $@ 21 | } 22 | 23 | warn() { 24 | echo $'\x1b[31m[WARN]\x1b[0m' $@ 25 | } 26 | 27 | error() { 28 | echo $'\x1b[31;1m[ERR]\x1b[0m' $@ 29 | exit 1 30 | } 31 | 32 | installPkg() { 33 | rootCmd="" 34 | if command -v doas &>/dev/null; then 35 | rootCmd="doas" 36 | elif command -v sudo &>/dev/null; then 37 | rootCmd="sudo" 38 | else 39 | warn "No privilege elevation command (e.g. sudo, doas) detected" 40 | fi 41 | 42 | case $1 in 43 | pacman) $rootCmd pacman --noconfirm -U ${@:2} ;; 44 | apk) $rootCmd apk add --allow-untrusted ${@:2} ;; 45 | zypper) $rootCmd zypper --no-gpg-checks install ${@:2} ;; 46 | *) $rootCmd $1 install -y ${@:2} ;; 47 | esac 48 | } 49 | 50 | if ! command -v curl &>/dev/null; then 51 | error "This script requires the curl command. Please install it and run again." 52 | fi 53 | 54 | pkgFormat="" 55 | pkgMgr="" 56 | if command -v pacman &>/dev/null; then 57 | info "Detected pacman" 58 | pkgFormat="pkg.tar.zst" 59 | pkgMgr="pacman" 60 | elif command -v apt &>/dev/null; then 61 | info "Detected apt" 62 | pkgFormat="deb" 63 | pkgMgr="apt" 64 | elif command -v dnf &>/dev/null; then 65 | info "Detected dnf" 66 | pkgFormat="rpm" 67 | pkgMgr="dnf" 68 | elif command -v yum &>/dev/null; then 69 | info "Detected yum" 70 | pkgFormat="rpm" 71 | pkgMgr="yum" 72 | elif command -v zypper &>/dev/null; then 73 | info "Detected zypper" 74 | pkgFormat="rpm" 75 | pkgMgr="zypper" 76 | elif command -v apk &>/dev/null; then 77 | info "Detected apk" 78 | pkgFormat="apk" 79 | pkgMgr="apk" 80 | else 81 | error "No supported package manager detected!" 82 | fi 83 | 84 | latestVersion=$(curl -sI 'https://gitea.elara.ws/lure/lure/releases/latest' | grep -io 'location: .*' | rev | cut -d '/' -f1 | rev | tr -d '[:space:]') 85 | info "Found latest LURE version:" $latestVersion 86 | 87 | fname="$(mktemp -u -p /tmp "lure.XXXXXXXXXX").${pkgFormat}" 88 | url="https://gitea.elara.ws/lure/lure/releases/download/${latestVersion}/linux-user-repository-${latestVersion#v}-linux-$(uname -m).${pkgFormat}" 89 | 90 | info "Downloading LURE package" 91 | curl -L $url -o $fname 92 | 93 | info "Installing LURE package" 94 | installPkg $pkgMgr $fname 95 | 96 | info "Cleaning up" 97 | rm $fname 98 | 99 | info "Done!" 100 | -------------------------------------------------------------------------------- /upgrade.go: -------------------------------------------------------------------------------- 1 | /* 2 | * LURE - Linux User REpository 3 | * Copyright (C) 2023 Elara Musayelyan 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "context" 23 | "fmt" 24 | 25 | "github.com/urfave/cli/v2" 26 | "lure.sh/lure/internal/config" 27 | "lure.sh/lure/internal/db" 28 | "lure.sh/lure/internal/types" 29 | "lure.sh/lure/pkg/build" 30 | "lure.sh/lure/pkg/distro" 31 | "lure.sh/lure/pkg/loggerctx" 32 | "lure.sh/lure/pkg/manager" 33 | "lure.sh/lure/pkg/repos" 34 | "go.elara.ws/vercmp" 35 | "golang.org/x/exp/maps" 36 | "golang.org/x/exp/slices" 37 | ) 38 | 39 | var upgradeCmd = &cli.Command{ 40 | Name: "upgrade", 41 | Usage: "Upgrade all installed packages", 42 | Aliases: []string{"up"}, 43 | Flags: []cli.Flag{ 44 | &cli.BoolFlag{ 45 | Name: "clean", 46 | Aliases: []string{"c"}, 47 | Usage: "Build package from scratch even if there's an already built package available", 48 | }, 49 | }, 50 | Action: func(c *cli.Context) error { 51 | ctx := c.Context 52 | log := loggerctx.From(ctx) 53 | 54 | info, err := distro.ParseOSRelease(ctx) 55 | if err != nil { 56 | log.Fatal("Error parsing os-release file").Err(err).Send() 57 | } 58 | 59 | mgr := manager.Detect() 60 | if mgr == nil { 61 | log.Fatal("Unable to detect a supported package manager on the system").Send() 62 | } 63 | 64 | err = repos.Pull(ctx, config.Config(ctx).Repos) 65 | if err != nil { 66 | log.Fatal("Error pulling repos").Err(err).Send() 67 | } 68 | 69 | updates, err := checkForUpdates(ctx, mgr, info) 70 | if err != nil { 71 | log.Fatal("Error checking for updates").Err(err).Send() 72 | } 73 | 74 | if len(updates) > 0 { 75 | build.InstallPkgs(ctx, updates, nil, types.BuildOpts{ 76 | Manager: mgr, 77 | Clean: c.Bool("clean"), 78 | Interactive: c.Bool("interactive"), 79 | }) 80 | } else { 81 | log.Info("There is nothing to do.").Send() 82 | } 83 | 84 | return nil 85 | }, 86 | } 87 | 88 | func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]db.Package, error) { 89 | installed, err := mgr.ListInstalled(nil) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | pkgNames := maps.Keys(installed) 95 | found, _, err := repos.FindPkgs(ctx, pkgNames) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | var out []db.Package 101 | for pkgName, pkgs := range found { 102 | if slices.Contains(config.Config(ctx).IgnorePkgUpdates, pkgName) { 103 | continue 104 | } 105 | 106 | if len(pkgs) > 1 { 107 | // Puts the element with the highest version first 108 | slices.SortFunc(pkgs, func(a, b db.Package) int { 109 | return vercmp.Compare(a.Version, b.Version) 110 | }) 111 | } 112 | 113 | // First element is the package we want to install 114 | pkg := pkgs[0] 115 | 116 | repoVer := pkg.Version 117 | if pkg.Release != 0 && pkg.Epoch == 0 { 118 | repoVer = fmt.Sprintf("%s-%d", pkg.Version, pkg.Release) 119 | } else if pkg.Release != 0 && pkg.Epoch != 0 { 120 | repoVer = fmt.Sprintf("%d:%s-%d", pkg.Epoch, pkg.Version, pkg.Release) 121 | } 122 | 123 | c := vercmp.Compare(repoVer, installed[pkgName]) 124 | if c == 0 || c == -1 { 125 | continue 126 | } else if c == 1 { 127 | out = append(out, pkg) 128 | } 129 | } 130 | return out, nil 131 | } 132 | --------------------------------------------------------------------------------