58 Commits

Author SHA1 Message Date
75a60070ba Add documentation about Like distros 2022-10-01 21:00:57 -07:00
a02a009b63 Disable like distros if LURE_DISTRO is set 2022-10-01 20:56:08 -07:00
74adb915fc Add option to disable like distros in decoder 2022-10-01 20:53:26 -07:00
bdca0a5ffc Move cross-packaging instructions to usage docs 2022-10-01 18:20:54 -07:00
674cfe6b0d Ask user for confirmation if CPU arch doesn't match 2022-10-01 14:39:26 -07:00
bb50b55ac5 Fix apk remove command 2022-10-01 01:47:07 -07:00
29016fcdb7 Run gofmt 2022-10-01 01:30:44 -07:00
c09574e659 Add options to package managers 2022-10-01 01:30:13 -07:00
1f39f5edf1 Only set epoch if not equal to zero 2022-10-01 00:47:50 -07:00
8661721ccc Run gofmt 2022-09-30 20:09:09 -07:00
93d5ad9a53 Factor in ID_LIKE variable and ARM variants when checking for overrides 2022-09-30 20:07:44 -07:00
77c3ea7d56 Return EOF if Nop ReadWriteCloser is used 2022-09-30 16:25:40 -07:00
fa76d95c04 Remove error when nop exec handler is executed 2022-09-30 16:22:27 -07:00
6012f0f505 Move reading build vars to separate function 2022-09-30 16:05:50 -07:00
35046566a1 If build dependencies exist, prompt to remove after build 2022-09-30 15:52:46 -07:00
46f79e4d26 Remove patches from planned features 2022-09-30 15:46:12 -07:00
7fa6dcf183 Set minimum Go version in module to 1.18 2022-09-30 13:58:50 -07:00
a5c2ac60d9 Document functions in build-scripts.md 2022-09-30 13:51:20 -07:00
7d6d22cf69 Require package() function to be present 2022-09-30 13:50:17 -07:00
28809828c2 Merge branch 'master' of gitea 2022-09-30 13:46:43 -07:00
3194648096 Merge pull request #9 from vaporup/patch-4 2022-09-30 13:39:53 -07:00
Sven Wick
86706e88ad Update configuration.md 2022-09-30 22:37:43 +02:00
3ac715d447 Add prepare() function 2022-09-30 13:22:10 -07:00
3cf1e7f513 Add docs for configuration 2022-09-30 13:10:27 -07:00
c18d1440c3 Add usage docs 2022-09-29 21:46:13 -07:00
87c2a8bf0d Fix build deps path in docs 2022-09-29 21:22:42 -07:00
ef1ea04760 Fix path to build scripts docs 2022-09-29 14:37:23 -07:00
6c9f841f11 Add documentation 2022-09-29 14:36:06 -07:00
bfb4431763 Add conflicts, replaces, provides to nfpm.Overridables 2022-09-29 14:35:56 -07:00
b5bc721840 Fix AUR badge 2022-09-29 20:18:45 +00:00
337e25c34f Add Maintainer field to script variables 2022-09-29 01:31:33 -07:00
6238542507 Merge pull request #5 from vaporup/patch-2 2022-09-28 18:17:46 -07:00
Sven Wick
98ffff404a Update README.md 2022-09-29 03:16:16 +02:00
dbe4cf98fa Remove completed planned feature from README 2022-09-28 17:11:47 -07:00
38edfe9283 Update installation instructions 2022-09-28 17:10:35 -07:00
332e36fb3d Add Makefile 2022-09-28 17:10:01 -07:00
61fe25bcee Add goreleaser config 2022-09-28 16:47:37 -07:00
46e03437ef Add more planned features 2022-09-28 15:33:09 -07:00
1ea10112fd Take epoch into account when building packages 2022-09-28 15:26:22 -07:00
bc00dde3b2 Run gofmt 2022-09-28 15:23:09 -07:00
Sven Wick
c3aba08681 Merge branch 'Arsen6331:master' into patch-2 2022-09-28 22:43:04 +02:00
ae9e294bf5 Use all as architecture if architectures array contains it 2022-09-28 13:03:44 -07:00
Sven Wick
57a75aa54e Update README.md 2022-09-28 21:45:00 +02:00
Sven Wick
438eef3213 Update README.md
Add doc for cross-packaging
2022-09-28 21:43:36 +02:00
4307278698 Allow overriding package format and distro while building 2022-09-28 10:08:28 -07:00
af83d71c6b Merge pull request #2 from vaporup/patch-1 2022-09-28 09:50:42 -07:00
Sven Wick
887184660c Update README.md
Mention nfpm in README
2022-09-28 17:07:14 +02:00
00c5ea3b5e Add list command 2022-09-28 01:35:17 -07:00
16bb936db7 Remove redundant check on depth 2022-09-27 20:00:37 -07:00
ab1826f523 Fix ~depth parameter for git downloads 2022-09-27 19:57:22 -07:00
74cc88580e Run gofmt 2022-09-27 19:14:04 -07:00
7a6a04d805 Add Planned Features section 2022-09-27 18:00:43 -07:00
f00fc25734 Allocate memory for output slice in pkgPrompt() using make() 2022-09-26 19:11:30 -07:00
095630ab91 Use nop handlers when parsing for update check 2022-09-26 18:59:46 -07:00
2f7c56f7eb Use nop handlers when parsing for info 2022-09-26 14:59:58 -07:00
7f7701ad03 Add info command 2022-09-26 14:42:17 -07:00
503328bf11 Add scripts to package 2022-09-26 14:38:14 -07:00
cd6990fe45 Remove unneeded WriteFunc() function 2022-09-26 14:07:13 -07:00
28 changed files with 1233 additions and 225 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
/lure
/dist/

69
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,69 @@
before:
hooks:
- go mod tidy
builds:
- id: lure
env:
- CGO_ENABLED=0
binary: lure
goos:
- linux
goarch:
- 386
- amd64
- arm
- arm64
- riscv64
archives:
- replacements:
386: i386
amd64: x86_64
arm64: aarch64
nfpms:
- id: lure
file_name_template: '{{.PackageName}}-{{.Version}}-{{.Os}}-{{.Arch}}'
description: "Linux User REpository"
replacements:
386: i386
amd64: x86_64
arm64: aarch64
homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
maintainer: 'Arsen Musyaelyan <arsen@arsenm.dev>'
license: GPLv3
formats:
- apk
- deb
- rpm
aurs:
- name: lure-bin
homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
description: "Linux User REpository"
maintainers:
- 'Arsen Musyaelyan <arsen@arsenm.dev>'
license: GPLv3
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://aur@aur.archlinux.org/lure-bin.git'
provides:
- lure
conflicts:
- lure
depends:
- sudo
- pacman
package: |-
# binaries
install -Dm755 "./lure" "${pkgdir}/usr/bin/lure"
release:
gitea:
owner: Arsen6331
name: lure
gitea_urls:
api: 'https://gitea.arsenm.dev/api/v1/'
download: 'https://gitea.arsenm.dev'
skip_tls_verify: false
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc

13
Makefile Normal file
View File

@@ -0,0 +1,13 @@
lure:
go build
clean:
rm -f lure
install: lure
sudo install -Dm755 lure /usr/local/bin/lure
uninstall:
rm -f /usr/local/bin/lure
.PHONY: install clean uninstall

View File

@@ -1,6 +1,7 @@
# LURE (Linux User REpository)
[![Go Report Card](https://goreportcard.com/badge/go.arsenm.dev/lure)](https://goreportcard.com/report/go.arsenm.dev/lure)
[![lure-bin AUR package](https://img.shields.io/aur/version/lure-bin?label=lure-bin&logo=archlinux)](https://aur.archlinux.org/packages/lure-bin/)
LURE is intended to bring the AUR to all distros. It is currently in an ***alpha*** state and may not be stable. It can download a repository, build packages in it using a bash script similar to [PKGBUILD](https://wiki.archlinux.org/title/PKGBUILD), and then install them using your system package manager.
@@ -10,9 +11,17 @@ LURE is written in pure Go and has zero dependencies after it's built. The only
## Installation
Binary releases are not provided currently. They will be provided once I have time to set up a CI pipeline.
Distro packages and binary archives are provided at the latest Gitea release: https://gitea.arsenm.dev/Arsen6331/lure/releases/latest
To install LURE, you'll need Go 1.18 or newer. Once installed, clone this repo and run `go build` inside, and then run `sudo install -Dm755 lure /usr/local/bin`.
LURE is also available on the AUR as [lure-bin](https://aur.archlinux.org/packages/lure-bin)
### Building from source
To build LURE from source, you'll need Go 1.18 or newer. Once Go is installed, clone this repo and run:
```shell
sudo make install
```
---
@@ -26,26 +35,9 @@ This means it's really easy to deploy LURE on any distro that it has support for
---
## Distro Overrides
## Documentation
Allowing LURE to run on different distros provides some challenges. For example, some distros use different names for their packages. This is solved using distro overrides. Any variable or function used in a LURE build script may be overridden based on distro and CPU architecture. The way you do this is by appending the distro and/or architecture to the end of the name. For example, [ITD](https://gitea.arsenm.dev/Arsen6331/itd) depends on the `pactl` command as well as DBus and BlueZ. These are named somewhat differently on different distros. For ITD, I use the following for the dependencies:
```bash
deps=('dbus' 'bluez' 'pulseaudio-utils')
deps_arch=('dbus' 'bluez' 'libpulse')
deps_opensuse=('dbus-1' 'bluez' 'pulseaudio-utils')
```
Appending `arch` and `opensuse` to the end causes LURE to use the appropriate array based on the distro. If on Arch Linux, it will use `deps_arch`. If on OpenSUSE, it will use `deps_opensuse`, and if on anything else, it will use `deps`.
Names are checked in the following order:
- $name_$architecture_$distro
- $name_$distro
- $name_$architecture
- $name
Distro detection is performed by reading the `/usr/lib/os-release` and `/etc/os-release` files.
The documentation for LURE is in the [docs](docs) directory in this repo.
---
@@ -62,5 +54,12 @@ As mentioned before, LURE has zero dependencies after it's built. All functional
- Bash: https://github.com/mvdan/sh
- Git: https://github.com/go-git/go-git
- Archiver: https://github.com/mholt/archiver
- nfpm: https://github.com/goreleaser/nfpm
---
## Planned Features
- Automated install script
- Automated docker-based testing tool
- Web interface for repos

204
build.go
View File

@@ -28,18 +28,20 @@ import (
"strconv"
"strings"
"github.com/AlecAivazis/survey/v2"
_ "github.com/goreleaser/nfpm/v2/apk"
_ "github.com/goreleaser/nfpm/v2/arch"
_ "github.com/goreleaser/nfpm/v2/deb"
_ "github.com/goreleaser/nfpm/v2/rpm"
"github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
"golang.org/x/sys/cpu"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"go.arsenm.dev/lure/distro"
"go.arsenm.dev/lure/download"
"go.arsenm.dev/lure/internal/cpu"
"go.arsenm.dev/lure/internal/shutils"
"go.arsenm.dev/lure/internal/shutils/decoder"
"go.arsenm.dev/lure/manager"
"mvdan.cc/sh/v3/expand"
@@ -56,6 +58,7 @@ type BuildVars struct {
Epoch uint `sh:"epoch"`
Description string `sh:"desc"`
Homepage string `sh:"homepage"`
Maintainer string `sh:"maintainer"`
Architectures []string `sh:"architectures"`
Licenses []string `sh:"license"`
Provides []string `sh:"provides"`
@@ -66,6 +69,18 @@ type BuildVars struct {
Sources []string `sh:"sources"`
Checksums []string `sh:"checksums"`
Backup []string `sh:"backup"`
Scripts Scripts `sh:"scripts"`
}
type Scripts struct {
PreInstall string `sh:"preinstall"`
PostInstall string `sh:"postinstall"`
PreRemove string `sh:"preremove"`
PostRemove string `sh:"postinstall"`
PreUpgrade string `sh:"preupgrade"`
PostUpgrade string `sh:"postupgrade"`
PreTrans string `sh:"pretrans"`
PostTrans string `sh:"posttrans"`
}
func buildCmd(c *cli.Context) error {
@@ -92,6 +107,12 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
return nil, nil, err
}
var distroChanged bool
if distID, ok := os.LookupEnv("LURE_DISTRO"); ok {
info.ID = distID
distroChanged = true
}
fl, err := os.Open(script)
if err != nil {
return nil, nil, err
@@ -121,12 +142,32 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
dec := decoder.New(info, runner)
// If distro was changed, the list of like distros
// no longer applies, so disable its use
if distroChanged {
dec.LikeDistros = false
}
var vars BuildVars
err = dec.DecodeVars(&vars)
if err != nil {
return nil, nil, err
}
if !archMatches(vars.Architectures) {
var buildAnyway bool
survey.AskOne(
&survey.Confirm{
Message: "Your system's CPU architecture doesn't match this package. Do you want to build anyway?",
Default: true,
},
&buildAnyway,
)
if !buildAnyway {
os.Exit(1)
}
}
log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
baseDir := filepath.Join(cacheDir, "pkgs", vars.Name)
@@ -182,7 +223,17 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
return nil, nil, err
}
fn, ok := dec.GetFunc("build")
fn, ok := dec.GetFunc("prepare")
if ok {
log.Info("Executing prepare()").Send()
err = fn(ctx, srcdir)
if err != nil {
return nil, nil, err
}
}
fn, ok = dec.GetFunc("build")
if ok {
log.Info("Executing build()").Send()
@@ -200,6 +251,8 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
if err != nil {
return nil, nil, err
}
} else {
log.Fatal("The package() function is required").Send()
}
uniq(
@@ -216,13 +269,27 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
Release: strconv.Itoa(vars.Release),
Homepage: vars.Homepage,
License: strings.Join(vars.Licenses, ", "),
Maintainer: vars.Maintainer,
Overridables: nfpm.Overridables{
Depends: append(repoDeps, builtNames...),
Conflicts: vars.Conflicts,
Replaces: vars.Replaces,
Provides: vars.Provides,
Depends: append(repoDeps, builtNames...),
},
}
if vars.Epoch != 0 {
pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10)
}
setScripts(&vars, pkgInfo, filepath.Dir(script))
if slices.Contains(vars.Architectures, "all") {
pkgInfo.Arch = "all"
}
if pkgInfo.Arch == "arm" {
pkgInfo.Arch = checkARMVariant()
pkgInfo.Arch = cpu.ARMVariant()
}
contents := []*files.Content{}
@@ -293,7 +360,12 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
pkgInfo.Overridables.Contents = contents
packager, err := nfpm.Get(mgr.Format())
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok {
pkgFormat = format
}
packager, err := nfpm.Get(pkgFormat)
if err != nil {
return nil, nil, err
}
@@ -314,6 +386,29 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
return nil, nil, err
}
if len(vars.BuildDepends) > 0 {
var removeBuildDeps bool
err = survey.AskOne(&survey.Confirm{
Message: "Would you like to remove build dependencies?",
}, &removeBuildDeps)
if err != nil {
return nil, nil, err
}
if removeBuildDeps {
err = mgr.Remove(
&manager.Opts{
AsRoot: true,
NoConfirm: true,
},
vars.BuildDepends...,
)
if err != nil {
return nil, nil, err
}
}
}
uniq(&pkgPaths, &pkgNames)
return pkgPaths, pkgNames, nil
@@ -375,22 +470,95 @@ func setDirVars(ctx context.Context, runner *interp.Runner, srcdir, pkgdir strin
return runner.Run(ctx, fl)
}
// checkARMVariant checks which variant of ARM lure is running
// on, by using the same detection method as Go itself
func checkARMVariant() string {
armEnv := os.Getenv("LURE_ARM_VARIANT")
// ensure value has "arm" prefix, such as arm5 or arm6
if strings.HasPrefix(armEnv, "arm") {
return armEnv
func setScripts(vars *BuildVars, info *nfpm.Info, scriptDir string) {
if vars.Scripts.PreInstall != "" {
info.Scripts.PreInstall = filepath.Join(scriptDir, vars.Scripts.PreInstall)
}
if cpu.ARM.HasVFPv3 {
return "arm7"
} else if cpu.ARM.HasVFP {
return "arm6"
} else {
return "arm5"
if vars.Scripts.PostInstall != "" {
info.Scripts.PostInstall = filepath.Join(scriptDir, vars.Scripts.PostInstall)
}
if vars.Scripts.PreRemove != "" {
info.Scripts.PreRemove = filepath.Join(scriptDir, vars.Scripts.PreRemove)
}
if vars.Scripts.PostRemove != "" {
info.Scripts.PostRemove = filepath.Join(scriptDir, vars.Scripts.PostRemove)
}
if vars.Scripts.PreUpgrade != "" {
info.ArchLinux.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade)
info.APK.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade)
}
if vars.Scripts.PostUpgrade != "" {
info.ArchLinux.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade)
info.APK.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade)
}
if vars.Scripts.PreTrans != "" {
info.RPM.Scripts.PreTrans = filepath.Join(scriptDir, vars.Scripts.PreTrans)
}
if vars.Scripts.PostTrans != "" {
info.RPM.Scripts.PostTrans = filepath.Join(scriptDir, vars.Scripts.PostTrans)
}
}
// getBuildVars only gets the build variables, while disabling exec, stat, open, and readdir
func getBuildVars(ctx context.Context, script string, info *distro.OSRelease) (*BuildVars, error) {
fl, err := os.Open(script)
if err != nil {
return nil, err
}
file, err := syntax.NewParser().Parse(fl, "lure.sh")
if err != nil {
return nil, err
}
fl.Close()
runner, err := interp.New(
interp.Env(expand.ListEnviron()),
interp.ExecHandler(shutils.NopExec),
interp.StatHandler(shutils.NopStat),
interp.OpenHandler(shutils.NopOpen),
interp.ReadDirHandler(shutils.NopReadDir),
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, file)
if err != nil {
return nil, err
}
dec := decoder.New(info, runner)
var vars BuildVars
err = dec.DecodeVars(&vars)
if err != nil {
return nil, err
}
return &vars, nil
}
func archMatches(architectures []string) bool {
arch := runtime.GOARCH
if arch == "arm" {
arch = cpu.ARMVariant()
}
if slices.Contains(architectures, "arm") {
architectures = append(architectures, cpu.ARMVariant())
}
return slices.Contains(architectures, arch)
}
func uniq(ss ...*[]string) {

View File

@@ -22,6 +22,7 @@ import (
"context"
"errors"
"os"
"strings"
"go.arsenm.dev/lure/internal/shutils"
"mvdan.cc/sh/v3/expand"
@@ -35,6 +36,7 @@ type OSRelease struct {
Name string
PrettyName string
ID string
Like []string
BuildID string
ANSIColor string
HomeURL string
@@ -80,7 +82,7 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
return nil, ErrParse
}
return &OSRelease{
out := &OSRelease{
Name: runner.Vars["NAME"].Str,
PrettyName: runner.Vars["PRETTY_NAME"].Str,
ID: runner.Vars["ID"].Str,
@@ -91,5 +93,11 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
SupportURL: runner.Vars["SUPPORT_URL"].Str,
BugReportURL: runner.Vars["BUG_REPORT_URL"].Str,
Logo: runner.Vars["LOGO"].Str,
}, nil
}
if runner.Vars["ID_LIKE"].IsSet() {
out.Like = strings.Split(runner.Vars["ID_LIKE"].Str, " ")
}
return out, nil
}

5
docs/README.md Normal file
View File

@@ -0,0 +1,5 @@
# LURE Docs
- [Build Scripts](build-scripts.md)
- [Usage](usage.md)
- [Configuration](configuration.md)

249
docs/build-scripts.md Normal file
View File

@@ -0,0 +1,249 @@
# LURE Build Scripts
LURE uses build scripts similar to the AUR's PKGBUILDs. This is the documentation for those scripts.
---
## Table of Contents
- [Distro Overrides](#distro-overrides)
- [Variables](#variables)
- [name](#name)
- [version](#version)
- [release](#release)
- [epoch](#epoch)
- [desc](#desc)
- [homepage](#homepage)
- [maintainer](#maintainer)
- [architectures](#architectures)
- [licenses](#licenses)
- [provides](#provides)
- [conflicts](#conflicts)
- [deps](#deps)
- [build_deps](#build_deps)
- [replaces](#replaces)
- [sources](#sources)
- [checksums](#checksums)
- [backup](#backup)
- [scripts](#scripts)
---
## Distro Overrides
Allowing LURE to run on different distros provides some challenges. For example, some distros use different names for their packages. This is solved using distro overrides. Any variable or function used in a LURE build script may be overridden based on distro and CPU architecture. The way you do this is by appending the distro and/or architecture to the end of the name. For example, [ITD](https://gitea.arsenm.dev/Arsen6331/itd) depends on the `pactl` command as well as DBus and BlueZ. These are named somewhat differently on different distros. For ITD, I use the following for the dependencies:
```bash
deps=('dbus' 'bluez' 'pulseaudio-utils')
deps_arch=('dbus' 'bluez' 'libpulse')
deps_opensuse=('dbus-1' 'bluez' 'pulseaudio-utils')
```
Appending `arch` and `opensuse` to the end causes LURE to use the appropriate array based on the distro. If on Arch Linux, it will use `deps_arch`. If on OpenSUSE, it will use `deps_opensuse`, and if on anything else, it will use `deps`.
Names are checked in the following order:
- $name_$architecture_$distro
- $name_$distro
- $name_$architecture
- $name
Distro detection is performed by reading the `/usr/lib/os-release` and `/etc/os-release` files.
### Like distros
Inside the `os-release` file, there is a list of "like" distros. LURE takes this into account. For example, if a script contains `deps_debian` but not `deps_ubuntu`, Ubuntu builds will use `deps_debian` because Ubuntu is based on debian.
Most specificity is preferred, so if both `deps_debian` and `deps_ubuntu` is provided, Ubuntu and all Ubuntu-based distros will use `deps_ubuntu` while Debian and all Debian-based distros
that are not Ubuntu-based will use `deps_debian`.
Like distros are disabled when using the `LURE_DISTRO` environment variable.
## Variables
Any variables marked with `(*)` are required
### name (*)
The `name` variable contains the name of the package described by the script.
### version (*)
The `version` variable contains the version of the package. This should be the same as the version used by the author upstream.
Versions are compared using the [rpmvercmp](https://fedoraproject.org/wiki/Archive:Tools/RPM/VersionComparison) algorithm.
### release (*)
The `release` number is meant to differentiate between different builds of the same package version, such as if the script is changed but the version stays the same. The `release` must be an integer.
### epoch
The `epoch` number forces the package to be considered newer than versions with a lower epoch. It is meant to be used if the versioning scheme can't be used to determine which package is newer. Its use is discouraged and it should only be used if necessary. The `epoch` must be a positive integer.
### desc
The `desc` field contains the description for the package. It should not contain any newlines.
### homepage
The `homepage` field contains the URL to the website of the project packaged by this script.
### maintainer
The `maintainer` field contains the name and email address of the person maintaining the package. Example:
```text
Arsen Musayelyan <arsen@arsenm.dev>
```
While LURE does not require this field to be set, Debian has deprecated unset maintainer fields, and may disallow their use in `.deb` packages in the future.
### architectures
The `architectures` array contains all the architectures that this package supports. These match Go's GOARCH list, except for a few differences.
The `all` architecture will be translated to the proper term for the packaging format. For example, it will be changed to `noarch` if building a `.rpm`, or `any` if building an Arch package.
Since multiple variations of the `arm` architecture exist, the following values should be used:
`arm5`: armv5
`arm6`: armv6
`arm7`: armv7
LURE will attempt to detect which variant your system is using by checking for the existence of various CPU features. If this yields the wrong result or if you simply want to build for a different variant, the `LURE_ARM_VARIANT` variable should be set to the ARM variant you want. Example:
```shell
LURE_ARM_VARIANT=arm5 lure install ...
```
### licenses
The `licenses` array contains the licenses used by this package. Some valid values include `GPLv3` and `MIT`.
### provides
The `provides` array specifies what features the package provides. For example, if two packages build `ffmpeg` with different build flags, they should both have `ffmpeg` in the `provides` array.
### conflicts
The `conflicts` array contains names of packages that conflict with the one built by this script. If two different packages contain the executable for `ffmpeg`, they cannot be installed at the same time, so they conflict. The `provides` array will also be checked, so this array generally contains the same values as `provides`.
### deps
The `deps` array contains the dependencies for the package. LURE repos will be checked first, and if the packages exist there, they will be built and installed. Otherwise, they will be installed from the system repos by your package manager.
### build_deps
The `build_deps` array contains the dependencies that are required to build the package. They will be installed before the build starts. Similarly to the `deps` array, LURE repos will be checked first.
### replaces
The `replaces` array contains the packages that are replaced by this package. Generally, if package managers find a package with a `replaces` field set, they will remove the listed package(s) and install that one instead. This is only useful if the packages are being stored in a repo for your package manager.
### sources
The `sources` array contains URLs which are downloaded into `$srcdir` before the build starts.
If the URL provided is an archive or compressed file, it will be extracted. To disable this, add the `~archive=false` query parameter. Example:
Extracted:
```text
https://example.com/archive.tar.gz
```
Not extracted:
```text
https://example.com/archive.tar.gz?~archive=false
```
If the URL scheme starts with `git+`, the source will be downloaded as a git repo. The git download mode supports multiple parameters:
- `~tag`: Specify which tag of the repo to check out.
- `~branch`: Specify which branch of the repo to check out.
- `~commit`: Specify which commit of the repo to check out.
- `~depth`: Specify what depth should be used when cloning the repo. Must be an integer.
Examples:
```text
git+https://gitea.arsenm.dev/Arsen6331/itd?~branch=resource-loading&~depth=1
```
```text
git+https://gitea.arsenm.dev/Arsen6331/lure?~tag=v0.0.1
```
### checksums
The `checksums` array must be the same length as the `sources` array. It contains sha256 checksums for the source files. The files are checked against the checksums and the build fails if they don't match.
To skip the check for a particular source, set the corresponding checksum to `SKIP`.
### backup
The `backup` array contains files that should be backed up when upgrading and removing. The exact behavior of this depends on your package manager. All files within this array must be full destination paths. For example, if there's a config called `config` in `/etc` that you want to back up, you'd set it like so:
```bash
backup=('/etc/config')
```
### scripts
The `scripts` variable contains a Bash associative array that specifies the location of various scripts relative to the build script. Example:
```bash
scripts=(
['preinstall']='preinstall.sh'
['postinstall']='postinstall.sh'
['preremove']='preremove.sh'
['postremove']='postremove.sh'
['preupgrade']='preupgrade.sh'
['postupgrade']='postupgrade.sh'
['pretrans']='pretrans.sh'
['posttrans']='posttrans.sh'
)
```
Note: The quotes are required due to limitations with the bash parser used.
The `preupgrade` and `postupgrade` scripts are only available in `.apk` and Arch Linux packages.
The `pretrans` and `posttrans` scripts are only available in `.rpm` packages.
The rest of the scripts are available in all packages.
---
## Functions
Any variables marked with `(*)` are required
All functions start in the `$srcdir` directory
### prepare
The `prepare()` function runs first. It is meant to prepare the sources for building and packaging. This is the function in which patches should be applied, for example, by the `patch` command, and where tools like `go generate` should be executed.
### build
The `build()` function is where the package is actually built. Use the same commands that would be used to manually compile the software. Often, this function is just one line:
```bash
build() {
make
}
```
### package (*)
The `package()` function is where the built files are placed into the directory that will be used by LURE to build the package.
Any files that should be installed on the filesystem should go in the `$pkgdir` directory in this function. For example, if you have a binary called `bin` that should be placed in `/usr/bin` and a config file called `bin.cfg` that should be placed in `/etc`, the `package()` function might look like this:
```bash
package() {
install -Dm755 bin ${pkgdir}/usr/bin/bin
install -Dm644 bin.cfg ${pkgdir}/etc/bin.cfg
}
```

44
docs/configuration.md Normal file
View File

@@ -0,0 +1,44 @@
# Configuration
This page describes the configuration of LURE
---
## Table of Contents
- [Config file](#config-file)
- [rootCmd](#rootcmd)
- [repo](#repo)
---
## File locations
| Path | Description
| --: | :--
| ~/.config/lure/lure.toml | Config file
| ~/.cache/lure/pkgs | here the packages are built and stored
| ~/.cache/lure/repo | here are the git repos with all the `lure.sh` files
| | Example: `~/.cache/lure/repo/default/itd-bin/lure.sh`
---
## Config file
### rootCmd
The `rootCmd` field in the config specifies which command should be used for privilege elevation. The default value is `sudo`.
### repo
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:
```toml
[[repo]]
name = 'default'
url = 'https://github.com/Arsen6331/lure-repo.git'
```
The `default` repo is added by default. Any amount of repos may be added.
---

160
docs/usage.md Normal file
View File

@@ -0,0 +1,160 @@
# Usage
## Table of Contents
- [Commands](#commands)
- [install](#install)
- [remove](#remove)
- [upgrade](#upgrade)
- [info](#info)
- [list](#list)
- [build](#build)
- [addrepo](#addrepo)
- [removerepo](#removerepo)
- [refresh](#refresh)
- [Environment Variables](#environment-variables)
- [LURE_DISTRO](#lure_distro)
- [LURE_PKG_FORMAT](#lure_pkg_format)
- [LURE_ARM_VARIANT](#lure_arm_variant)
---
## Commands
### install
The install command installs a command from the LURE repos. Any packages that aren't found in LURE's repos get forwarded to the system package manager for installation.
Example:
```shell
lure in itd-bin
```
### remove
The remove command is for convenience. All it does is forwards the remove command to the system package manager.
Example:
```shell
lure rm firefox
```
### upgrade
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.
Example:
```shell
lure up
```
### info
The info command displays information about a package in LURE's repos.
Example:
```shell
lure info itd-bin
```
### list
The list command lists all LURE repo packages as well as their versions
Example:
```shell
lure ls
```
### build
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.
Example:
```shell
lure build
```
### addrepo
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.
Example:
```shell
lure ar -n default -u https://github.com/Arsen6331/lure-repo
```
### removerepo
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.
Example:
```shell
lure rr -n default
```
### refresh
The refresh command pulls all changes from all LURE repos that have changed.
Example:
```shell
lure ref
```
---
## Environment Variables
### LURE_DISTRO
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:
- `arch`
- `alpine`
- `opensuse`
- `debian`
### LURE_PKG_FORMAT
The `LURE_PKG_FORMAT` environment variable should be set to the packaging format that should be used. Valid values are:
- `archlinux`
- `apk`
- `rpm`
- `deb`
### LURE_ARM_VARIANT
The `LURE_ARM_VARIANT` environment variable dictates which ARM variant to build for, if LURE is running on an ARM system. Possible values include:
- `arm5`
- `arm6`
- `arm7`
---
## Cross-packaging for other Distributions
You can create packages for different distributions
setting the environment variables `LURE_DISTRO` and `LURE_PKG_FORMAT` as mentioned above.
Examples:
```
LURE_DISTRO=arch LURE_PKG_FORMAT=archlinux lure build
LURE_DISTRO=alpine LURE_PKG_FORMAT=apk lure build
LURE_DISTRO=opensuse LURE_PKG_FORMAT=rpm lure build
LURE_DISTRO=debian LURE_PKG_FORMAT=deb lure build
```
---

View File

@@ -76,7 +76,7 @@ func Get(ctx context.Context, opts GetOptions) error {
commit := query.Get("~commit")
query.Del("~commit")
depth := query.Get("~depth")
depthStr := query.Get("~depth")
query.Del("~depth")
var refName plumbing.ReferenceName
@@ -98,18 +98,18 @@ func Get(ctx context.Context, opts GetOptions) error {
dstDir = filepath.Join(opts.Destination, name)
}
depth, err := strconv.Atoi(depth)
if err != nil {
return err
depth := 0
if depthStr != "" {
depth, err = strconv.Atoi(depthStr)
if err != nil {
return err
}
}
cloneOpts := &git.CloneOptions{
URL: src.String(),
Progress: os.Stderr,
}
if depth != 0 {
cloneOpts.Depth = depth
Depth: depth,
}
repo, err := git.PlainCloneContext(ctx, dstDir, false, cloneOpts)

4
go.mod
View File

@@ -1,6 +1,6 @@
module go.arsenm.dev/lure
go 1.19
go 1.18
replace github.com/goreleaser/nfpm/v2 => github.com/Arsen6331/nfpm/v2 v2.0.0-20220922210414-eae88e8ea4b5
@@ -15,6 +15,7 @@ require (
go.arsenm.dev/logger v0.0.0-20220630204155-5ba23e583f0a
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.5.1
)
@@ -73,5 +74,4 @@ require (
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

59
info.go Normal file
View File

@@ -0,0 +1,59 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2022 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import (
"os"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/distro"
"gopkg.in/yaml.v3"
)
func infoCmd(c *cli.Context) error {
args := c.Args()
if args.Len() < 1 {
log.Fatalf("Command info expected at least 1 argument, got %d", args.Len()).Send()
}
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error parsing os-release").Err(err).Send()
}
found, err := findPkg(args.First())
if err != nil {
log.Fatal("Error finding package").Err(err).Send()
}
// if multiple are matched, only use the first one
script := found[0]
vars, err := getBuildVars(c.Context, script, info)
if err != nil {
log.Fatal("Error getting build variables").Err(err).Send()
}
err = yaml.NewEncoder(os.Stdout).Encode(vars)
if err != nil {
log.Fatal("Error encoding script variables").Err(err).Send()
}
return nil
}

View File

@@ -50,7 +50,7 @@ func installPkgs(ctx context.Context, pkgs []string, mgr manager.Manager) {
scripts, notFound := findPkgs(pkgs)
if len(notFound) > 0 {
err = mgr.Install(notFound...)
err = mgr.Install(nil, notFound...)
if err != nil {
log.Fatal("Error installing native packages").Err(err).Send()
}
@@ -66,7 +66,7 @@ func installScripts(ctx context.Context, mgr manager.Manager, scripts []string)
log.Fatal("Error building package").Err(err).Send()
}
err = mgr.InstallLocal(builtPkgs...)
err = mgr.InstallLocal(nil, builtPkgs...)
if err != nil {
log.Fatal("Error installing package").Err(err).Send()
}
@@ -84,7 +84,7 @@ func removeCmd(c *cli.Context) error {
log.Fatal("Unable to detect supported package manager on system").Send()
}
err := mgr.Remove(c.Args().Slice()...)
err := mgr.Remove(nil, c.Args().Slice()...)
if err != nil {
log.Fatal("Error removing packages").Err(err).Send()
}

26
internal/cpu/cpu.go Normal file
View File

@@ -0,0 +1,26 @@
package cpu
import (
"os"
"strings"
"golang.org/x/sys/cpu"
)
// ARMVariant checks which variant of ARM lure is running
// on, by using the same detection method as Go itself
func ARMVariant() string {
armEnv := os.Getenv("LURE_ARM_VARIANT")
// ensure value has "arm" prefix, such as arm5 or arm6
if strings.HasPrefix(armEnv, "arm") {
return armEnv
}
if cpu.ARM.HasVFPv3 {
return "arm7"
} else if cpu.ARM.HasVFP {
return "arm6"
} else {
return "arm5"
}
}

View File

@@ -22,13 +22,13 @@ import (
"context"
"errors"
"fmt"
"io"
"reflect"
"runtime"
"strings"
"github.com/mitchellh/mapstructure"
"go.arsenm.dev/lure/distro"
"go.arsenm.dev/lure/internal/cpu"
"golang.org/x/exp/slices"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
@@ -37,25 +37,27 @@ import (
var ErrInvalidType = errors.New("val must be a pointer to a struct")
type NotFoundError struct {
stype string
name string
type VarNotFoundError struct {
name string
}
func (nfe NotFoundError) Error() string {
return "required " + nfe.stype + " '" + nfe.name + "' could not be found"
func (nfe VarNotFoundError) Error() string {
return "required variable '" + nfe.name + "' could not be found"
}
// Decoder provides methods for decoding variable values
type Decoder struct {
info *distro.OSRelease
runner *interp.Runner
info *distro.OSRelease
runner *interp.Runner
// Enable distro overrides (true by default)
Overrides bool
// Enable using like distros for overrides (true by default)
LikeDistros bool
}
// New creates a new variable decoder
func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
return &Decoder{info, runner, true}
return &Decoder{info, runner, true, true}
}
// DecodeVar decodes a variable to val using reflection.
@@ -63,7 +65,7 @@ func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
func (d *Decoder) DecodeVar(name string, val any) error {
variable := d.getVar(name)
if variable == nil {
return NotFoundError{"variable", name}
return VarNotFoundError{name}
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
@@ -128,7 +130,7 @@ func (d *Decoder) DecodeVars(val any) error {
newVal := reflect.New(field.Type())
err := d.DecodeVar(name, newVal.Interface())
if _, ok := err.(NotFoundError); ok && !required {
if _, ok := err.(VarNotFoundError); ok && !required {
continue
} else if err != nil {
return err
@@ -158,31 +160,6 @@ func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) {
}, true
}
// WriteFunc writes the contents of a bash function to w.
func (d *Decoder) WriteFunc(name string, w io.Writer) error {
fn := d.getFunc(name)
if fn == nil {
return NotFoundError{"function", name}
}
printer := syntax.NewPrinter()
// Print individual statements instead of the entire block
block := fn.Cmd.(*syntax.Block)
for _, stmt := range block.Stmts {
err := printer.Print(w, stmt)
if err != nil {
return err
}
_, err = io.WriteString(w, "\n")
if err != nil {
return err
}
}
return nil
}
func (d *Decoder) getFunc(name string) *syntax.Stmt {
names := d.genPossibleNames(name)
for _, fnName := range names {
@@ -223,10 +200,35 @@ func (d *Decoder) genPossibleNames(name string) []string {
return []string{name}
}
return []string{
fmt.Sprintf("%s_%s_%s", name, runtime.GOARCH, d.info.ID),
fmt.Sprintf("%s_%s", name, d.info.ID),
fmt.Sprintf("%s_%s", name, runtime.GOARCH),
name,
architectures := []string{runtime.GOARCH}
if runtime.GOARCH == "arm" {
// More specific goes first
architectures[0] = cpu.ARMVariant()
architectures = append(architectures, "arm")
}
distros := []string{d.info.ID}
if d.LikeDistros {
distros = append(distros, d.info.Like...)
}
var out []string
for _, arch := range architectures {
for _, distro := range distros {
out = append(
out,
fmt.Sprintf("%s_%s_%s", name, arch, distro),
fmt.Sprintf("%s_%s", name, distro),
)
}
out = append(out, fmt.Sprintf("%s_%s", name, arch))
}
out = append(out, name)
for index, item := range out {
out[index] = strings.ReplaceAll(item, "-", "_")
}
return out
}

View File

@@ -22,7 +22,6 @@ import (
"context"
"io"
"os"
"os/exec"
)
func NopReadDir(context.Context, string) ([]os.FileInfo, error) {
@@ -34,7 +33,7 @@ func NopStat(context.Context, string, bool) (os.FileInfo, error) {
}
func NopExec(context.Context, []string) error {
return exec.ErrNotFound
return nil
}
func NopOpen(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) {
@@ -44,11 +43,11 @@ func NopOpen(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, err
type NopRWC struct{}
func (NopRWC) Read([]byte) (int, error) {
return 0, os.ErrClosed
return 0, io.EOF
}
func (NopRWC) Write([]byte) (int, error) {
return 0, os.ErrClosed
return 0, io.EOF
}
func (NopRWC) Close() error {

31
list.go Normal file
View File

@@ -0,0 +1,31 @@
package main
import (
"fmt"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/distro"
)
func listCmd(c *cli.Context) error {
info, err := distro.ParseOSRelease(c.Context)
if err != nil {
log.Fatal("Error parsing os-release").Err(err).Send()
}
pkgs, err := findPkg("*")
if err != nil {
log.Fatal("Error finding packages").Err(err).Send()
}
for _, script := range pkgs {
vars, err := getBuildVars(c.Context, script, info)
if err != nil {
log.Fatal("Error getting build variables").Err(err).Send()
}
fmt.Println(vars.Name, vars.Version)
}
return nil
}

11
main.go
View File

@@ -64,6 +64,17 @@ func main() {
Aliases: []string{"up"},
Action: upgradeCmd,
},
{
Name: "info",
Usage: "Print information about a package",
Action: infoCmd,
},
{
Name: "list",
Usage: "List LURE repo packages",
Aliases: []string{"ls"},
Action: listCmd,
},
{
Flags: []cli.Flag{
&cli.StringFlag{

View File

@@ -47,8 +47,9 @@ func (a *APK) SetRootCmd(s string) {
a.rootCmd = s
}
func (a *APK) Sync() error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apk", "update")
func (a *APK) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "update")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -57,8 +58,9 @@ func (a *APK) Sync() error {
return nil
}
func (a *APK) Install(pkgs ...string) error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apk", "add")
func (a *APK) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "add")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -68,8 +70,9 @@ func (a *APK) Install(pkgs ...string) error {
return nil
}
func (a *APK) InstallLocal(pkgs ...string) error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apk", "add", "--allow-untrusted")
func (a *APK) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "add", "--allow-untrusted")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -79,8 +82,9 @@ func (a *APK) InstallLocal(pkgs ...string) error {
return nil
}
func (a *APK) Remove(pkgs ...string) error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apt", "del")
func (a *APK) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "del")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -90,8 +94,9 @@ func (a *APK) Remove(pkgs ...string) error {
return nil
}
func (a *APK) Upgrade(pkgs ...string) error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apk", "upgrade")
func (a *APK) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "upgrade")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -101,13 +106,21 @@ func (a *APK) Upgrade(pkgs ...string) error {
return nil
}
func (a *APK) UpgradeAll() error {
return a.Upgrade()
func (a *APK) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
return a.Upgrade(opts)
}
func (a *APK) ListInstalled() (map[string]string, error) {
func (a *APK) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
cmd := exec.Command(getRootCmd(a.rootCmd), "apk", "list", "-I")
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), "apk", "list", "-I")
} else {
cmd = exec.Command("apk", "list", "-I")
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -141,3 +154,19 @@ func (a *APK) ListInstalled() (map[string]string, error) {
return out, nil
}
func (a *APK) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)
}
if !opts.NoConfirm {
cmd.Args = append(cmd.Args, "-i")
}
return cmd
}

View File

@@ -47,8 +47,9 @@ func (a *APT) SetRootCmd(s string) {
a.rootCmd = s
}
func (a *APT) Sync() error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apt", "update", "-y")
func (a *APT) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "update")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -57,8 +58,9 @@ func (a *APT) Sync() error {
return nil
}
func (a *APT) Install(pkgs ...string) error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apt", "install", "-y")
func (a *APT) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "install")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -68,12 +70,14 @@ func (a *APT) Install(pkgs ...string) error {
return nil
}
func (a *APT) InstallLocal(pkgs ...string) error {
return a.Install(pkgs...)
func (a *APT) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return a.Install(opts, pkgs...)
}
func (a *APT) Remove(pkgs ...string) error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apt", "remove", "-y")
func (a *APT) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "remove")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -83,12 +87,14 @@ func (a *APT) Remove(pkgs ...string) error {
return nil
}
func (a *APT) Upgrade(pkgs ...string) error {
return a.Install(pkgs...)
func (a *APT) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return a.Install(opts, pkgs...)
}
func (a *APT) UpgradeAll() error {
cmd := exec.Command(getRootCmd(a.rootCmd), "apt", "upgrade", "-y")
func (a *APT) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -97,9 +103,16 @@ func (a *APT) UpgradeAll() error {
return nil
}
func (a *APT) ListInstalled() (map[string]string, error) {
func (a *APT) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
cmd := exec.Command(getRootCmd(a.rootCmd), "dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), "dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
} else {
cmd = exec.Command("dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -127,3 +140,19 @@ func (a *APT) ListInstalled() (map[string]string, error) {
return out, nil
}
func (a *APT) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)
}
if opts.NoConfirm {
cmd.Args = append(cmd.Args, "-y")
}
return cmd
}

View File

@@ -47,8 +47,9 @@ func (d *DNF) SetRootCmd(s string) {
d.rootCmd = s
}
func (d *DNF) Sync() error {
cmd := exec.Command(getRootCmd(d.rootCmd), "dnf", "upgrade", "--assumeno")
func (d *DNF) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -57,8 +58,9 @@ func (d *DNF) Sync() error {
return nil
}
func (d *DNF) Install(pkgs ...string) error {
cmd := exec.Command(getRootCmd(d.rootCmd), "dnf", "install", "-y")
func (d *DNF) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "install")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -68,12 +70,14 @@ func (d *DNF) Install(pkgs ...string) error {
return nil
}
func (d *DNF) InstallLocal(pkgs ...string) error {
return d.Install(pkgs...)
func (d *DNF) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return d.Install(opts, pkgs...)
}
func (d *DNF) Remove(pkgs ...string) error {
cmd := exec.Command(getRootCmd(d.rootCmd), "dnf", "remove", "-y")
func (d *DNF) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "remove")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -83,8 +87,9 @@ func (d *DNF) Remove(pkgs ...string) error {
return nil
}
func (d *DNF) Upgrade(pkgs ...string) error {
cmd := exec.Command(getRootCmd(d.rootCmd), "dnf", "upgrade", "-y")
func (d *DNF) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "upgrade")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -94,8 +99,9 @@ func (d *DNF) Upgrade(pkgs ...string) error {
return nil
}
func (d *DNF) UpgradeAll() error {
cmd := exec.Command(getRootCmd(d.rootCmd), "dnf", "upgrade", "-y")
func (d *DNF) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -104,9 +110,16 @@ func (d *DNF) UpgradeAll() error {
return nil
}
func (d *DNF) ListInstalled() (map[string]string, error) {
func (d *DNF) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
cmd := exec.Command(getRootCmd(d.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(d.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
} else {
cmd = exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -134,3 +147,19 @@ func (d *DNF) ListInstalled() (map[string]string, error) {
return out, nil
}
func (d *DNF) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(d.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)
}
if opts.NoConfirm {
cmd.Args = append(cmd.Args, "-y")
}
return cmd
}

View File

@@ -23,6 +23,16 @@ import (
"os/exec"
)
type Opts struct {
AsRoot bool
NoConfirm bool
}
var DefaultOpts = &Opts{
AsRoot: true,
NoConfirm: false,
}
// DefaultRootCmd is the command used for privilege elevation by default
var DefaultRootCmd = "sudo"
@@ -52,19 +62,19 @@ type Manager interface {
// Sets the command used to elevate privileges. Defaults to DefaultRootCmd.
SetRootCmd(string)
// Sync fetches repositories without installing anything
Sync() error
Sync(*Opts) error
// Install installs packages
Install(...string) error
Install(*Opts, ...string) error
// Remove uninstalls packages
Remove(...string) error
Remove(*Opts, ...string) error
// Upgrade upgrades packages
Upgrade(...string) error
Upgrade(*Opts, ...string) error
// InstallLocal installs packages from local files rather than repos
InstallLocal(...string) error
InstallLocal(*Opts, ...string) error
// UpgradeAll upgrades all packages
UpgradeAll() error
UpgradeAll(*Opts) error
// ListInstalled returns all installed packages mapped to their versions
ListInstalled() (map[string]string, error)
ListInstalled(*Opts) (map[string]string, error)
}
// Detect returns the package manager detected on the system
@@ -101,3 +111,10 @@ func setCmdEnv(cmd *exec.Cmd) {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
func ensureOpts(opts *Opts) *Opts {
if opts == nil {
return DefaultOpts
}
return opts
}

View File

@@ -47,8 +47,9 @@ func (p *Pacman) SetRootCmd(s string) {
p.rootCmd = s
}
func (p *Pacman) Sync() error {
cmd := exec.Command(getRootCmd(p.rootCmd), "pacman", "--noconfirm", "-Sy")
func (p *Pacman) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-Sy")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -57,8 +58,9 @@ func (p *Pacman) Sync() error {
return nil
}
func (p *Pacman) Install(pkgs ...string) error {
cmd := exec.Command(getRootCmd(p.rootCmd), "pacman", "--noconfirm", "-S")
func (p *Pacman) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-S")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -68,8 +70,9 @@ func (p *Pacman) Install(pkgs ...string) error {
return nil
}
func (p *Pacman) InstallLocal(pkgs ...string) error {
cmd := exec.Command(getRootCmd(p.rootCmd), "pacman", "--noconfirm", "-U")
func (p *Pacman) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-U")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -79,8 +82,9 @@ func (p *Pacman) InstallLocal(pkgs ...string) error {
return nil
}
func (p *Pacman) Remove(pkgs ...string) error {
cmd := exec.Command(getRootCmd(p.rootCmd), "pacman", "--noconfirm", "-R")
func (p *Pacman) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-R")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -90,12 +94,14 @@ func (p *Pacman) Remove(pkgs ...string) error {
return nil
}
func (p *Pacman) Upgrade(pkgs ...string) error {
return p.Install(pkgs...)
func (p *Pacman) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return p.Install(opts, pkgs...)
}
func (p *Pacman) UpgradeAll() error {
cmd := exec.Command(getRootCmd(p.rootCmd), "pacman", "--noconfirm", "-Su")
func (p *Pacman) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-Su")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -104,9 +110,16 @@ func (p *Pacman) UpgradeAll() error {
return nil
}
func (p *Pacman) ListInstalled() (map[string]string, error) {
func (p *Pacman) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
cmd := exec.Command(getRootCmd(p.rootCmd), "pacman", "-Q")
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(p.rootCmd), "pacman", "-Q")
} else {
cmd = exec.Command("pacman", "-Q")
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -134,3 +147,19 @@ func (p *Pacman) ListInstalled() (map[string]string, error) {
return out, nil
}
func (p *Pacman) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(p.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)
}
if opts.NoConfirm {
cmd.Args = append(cmd.Args, "--noconfirm")
}
return cmd
}

View File

@@ -47,8 +47,9 @@ func (y *YUM) SetRootCmd(s string) {
y.rootCmd = s
}
func (y *YUM) Sync() error {
cmd := exec.Command(getRootCmd(y.rootCmd), "yum", "upgrade", "--assumeno")
func (y *YUM) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -57,8 +58,9 @@ func (y *YUM) Sync() error {
return nil
}
func (y *YUM) Install(pkgs ...string) error {
cmd := exec.Command(getRootCmd(y.rootCmd), "yum", "install", "-y")
func (y *YUM) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "install")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -68,12 +70,14 @@ func (y *YUM) Install(pkgs ...string) error {
return nil
}
func (y *YUM) InstallLocal(pkgs ...string) error {
return y.Install(pkgs...)
func (y *YUM) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return y.Install(opts, pkgs...)
}
func (y *YUM) Remove(pkgs ...string) error {
cmd := exec.Command(getRootCmd(y.rootCmd), "yum", "remove", "-y")
func (y *YUM) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "remove")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -83,8 +87,9 @@ func (y *YUM) Remove(pkgs ...string) error {
return nil
}
func (y *YUM) Upgrade(pkgs ...string) error {
cmd := exec.Command(getRootCmd(y.rootCmd), "yum", "upgrade", "-y")
func (y *YUM) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "upgrade")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -94,8 +99,9 @@ func (y *YUM) Upgrade(pkgs ...string) error {
return nil
}
func (y *YUM) UpgradeAll() error {
cmd := exec.Command(getRootCmd(y.rootCmd), "yum", "upgrade", "-y")
func (y *YUM) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -104,9 +110,16 @@ func (y *YUM) UpgradeAll() error {
return nil
}
func (y *YUM) ListInstalled() (map[string]string, error) {
func (y *YUM) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
cmd := exec.Command(getRootCmd(y.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(y.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
} else {
cmd = exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -134,3 +147,19 @@ func (y *YUM) ListInstalled() (map[string]string, error) {
return out, nil
}
func (y *YUM) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(y.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)
}
if opts.NoConfirm {
cmd.Args = append(cmd.Args, "-y")
}
return cmd
}

View File

@@ -47,8 +47,9 @@ func (z *Zypper) SetRootCmd(s string) {
z.rootCmd = s
}
func (z *Zypper) Sync() error {
cmd := exec.Command(getRootCmd(z.rootCmd), "zypper", "refresh")
func (z *Zypper) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "refresh")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -57,8 +58,9 @@ func (z *Zypper) Sync() error {
return nil
}
func (z *Zypper) Install(pkgs ...string) error {
cmd := exec.Command(getRootCmd(z.rootCmd), "zypper", "install", "-y")
func (z *Zypper) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "install", "-y")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -68,12 +70,14 @@ func (z *Zypper) Install(pkgs ...string) error {
return nil
}
func (z *Zypper) InstallLocal(pkgs ...string) error {
return z.Install(pkgs...)
func (z *Zypper) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return z.Install(opts, pkgs...)
}
func (z *Zypper) Remove(pkgs ...string) error {
cmd := exec.Command(getRootCmd(z.rootCmd), "zypper", "remove", "-y")
func (z *Zypper) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "remove", "-y")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -83,8 +87,9 @@ func (z *Zypper) Remove(pkgs ...string) error {
return nil
}
func (z *Zypper) Upgrade(pkgs ...string) error {
cmd := exec.Command(getRootCmd(z.rootCmd), "zypper", "update", "-y")
func (z *Zypper) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "update", "-y")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
@@ -94,8 +99,9 @@ func (z *Zypper) Upgrade(pkgs ...string) error {
return nil
}
func (z *Zypper) UpgradeAll() error {
cmd := exec.Command(getRootCmd(z.rootCmd), "zypper", "update", "-y")
func (z *Zypper) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "update", "-y")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
@@ -104,9 +110,16 @@ func (z *Zypper) UpgradeAll() error {
return nil
}
func (z *Zypper) ListInstalled() (map[string]string, error) {
func (z *Zypper) ListInstalled(opts *Opts) (map[string]string, error) {
opts = ensureOpts(opts)
out := map[string]string{}
cmd := exec.Command(getRootCmd(z.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(z.rootCmd), "rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
} else {
cmd = exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
}
stdout, err := cmd.StdoutPipe()
if err != nil {
@@ -134,3 +147,19 @@ func (z *Zypper) ListInstalled() (map[string]string, error) {
return out, nil
}
func (z *Zypper) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
var cmd *exec.Cmd
if opts.AsRoot {
cmd = exec.Command(getRootCmd(z.rootCmd), mgrCmd)
cmd.Args = append(cmd.Args, args...)
} else {
cmd = exec.Command(mgrCmd, args...)
}
if opts.NoConfirm {
cmd.Args = append(cmd.Args, "-y")
}
return cmd
}

View File

@@ -160,9 +160,9 @@ func pkgPrompt(options []string) ([]string, error) {
return nil, err
}
var out []string
for _, i := range choices {
out = append(out, options[i])
out := make([]string, len(choices))
for i, choiceIndex := range choices {
out[i] = options[choiceIndex]
}
return out, nil

View File

@@ -21,14 +21,10 @@ package main
import (
"context"
"fmt"
"os"
"github.com/urfave/cli/v2"
"go.arsenm.dev/lure/distro"
"go.arsenm.dev/lure/internal/shutils/decoder"
"go.arsenm.dev/lure/manager"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
func upgradeCmd(c *cli.Context) error {
@@ -57,7 +53,7 @@ func upgradeCmd(c *cli.Context) error {
}
func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]string, error) {
installed, err := mgr.ListInstalled()
installed, err := mgr.ListInstalled(nil)
if err != nil {
return nil, err
}
@@ -72,32 +68,9 @@ func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRe
// since we're not using a glob, we can assume a single item
script := scripts[0]
fl, err := os.Open(script)
vars, err := getBuildVars(ctx, script, info)
if err != nil {
return nil, err
}
file, err := syntax.NewParser().Parse(fl, "lure.sh")
if err != nil {
return nil, err
}
runner, err := interp.New()
if err != nil {
return nil, err
}
err = runner.Run(ctx, file)
if err != nil {
return nil, err
}
dec := decoder.New(info, runner)
var vars BuildVars
err = dec.DecodeVars(&vars)
if err != nil {
return nil, err
log.Fatal("Error getting build variables").Err(err).Send()
}
repoVer := vars.Version