Compare commits

...

No commits in common. "v0.0.7" and "master" have entirely different histories.

93 changed files with 3902 additions and 5626 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
liberapay: lure

View File

@ -1,12 +1,13 @@
before:
hooks:
- go mod tidy
- go generate
builds:
- id: lure
env:
- CGO_ENABLED=0
binary: lure
ldflags:
- -X go.elara.ws/lure/internal/config.Version={{.Version}}
goos:
- linux
goarch:
@ -16,23 +17,30 @@ builds:
- arm
- riscv64
archives:
- replacements:
386: i386
amd64: x86_64
arm64: aarch64
- name_template: >-
{{- .ProjectName}}-
{{- .Version}}-
{{- .Os}}-
{{- if .Arch | eq "amd64"}}x86_64
{{- else if .Arch | eq "386"}}i386
{{- else if .Arch | eq "arm64"}}aarch64
{{- else }}{{ .Arch }}{{ end -}}
files:
- scripts/completion/*
nfpms:
- id: lure
package_name: linux-user-repository
file_name_template: '{{.PackageName}}-{{.Version}}-{{.Os}}-{{.Arch}}'
file_name_template: >-
{{- .PackageName}}-
{{- .Version}}-
{{- .Os}}-
{{- if .Arch | eq "amd64"}}x86_64
{{- else if .Arch | eq "386"}}i386
{{- else if .Arch | eq "arm64"}}aarch64
{{- else }}{{ .Arch }}{{ end -}}
description: "Linux User REpository"
replacements:
386: i386
amd64: x86_64
arm64: aarch64
homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
maintainer: 'Arsen Musayelyan <arsen@arsenm.dev>'
homepage: 'https://lure.sh'
maintainer: 'Elara Musayelyan <elara@elara.ws>'
license: GPLv3
formats:
- apk
@ -43,6 +51,8 @@ nfpms:
- linux-user-repository
conflicts:
- linux-user-repository
recommends:
- aria2
contents:
- src: scripts/completion/bash
dst: /usr/share/bash-completion/completions/lure
@ -50,10 +60,10 @@ nfpms:
dst: /usr/share/zsh/site-functions/_lure
aurs:
- name: linux-user-repository-bin
homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
homepage: 'https://lure.sh'
description: "Linux User REpository"
maintainers:
- 'Arsen Musayelyan <arsen@arsenm.dev>'
- 'Elara Musayelyan <elara@elara.ws>'
license: GPLv3
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://aur@aur.archlinux.org/linux-user-repository-bin.git'
@ -64,6 +74,8 @@ aurs:
depends:
- sudo
- pacman
optdepends:
- 'aria2: for downloading torrent sources'
package: |-
# binaries
install -Dm755 ./lure "${pkgdir}/usr/bin/lure"
@ -73,11 +85,11 @@ aurs:
install -Dm755 ./scripts/completion/zsh ${pkgdir}/usr/share/zsh/site-functions/_lure
release:
gitea:
owner: Arsen6331
owner: lure
name: lure
gitea_urls:
api: 'https://gitea.arsenm.dev/api/v1/'
download: 'https://gitea.arsenm.dev'
api: 'https://gitea.elara.ws/api/v1/'
download: 'https://gitea.elara.ws'
skip_tls_verify: false
checksum:
name_template: 'checksums.txt'

View File

@ -1,3 +1,4 @@
platform: linux/amd64
pipeline:
release:
image: goreleaser/goreleaser

View File

@ -1,7 +1,8 @@
PREFIX ?= /usr/local
GIT_VERSION = $(shell git describe --tags )
lure: internal/config/version.txt
go build
lure:
CGO_ENABLED=0 go build -ldflags="-X 'go.elara.ws/lure/internal/config.Version=$(GIT_VERSION)'"
clean:
rm -f lure
@ -15,8 +16,5 @@ installmisc:
uninstall:
rm -f /usr/local/bin/lure
internal/config/version.txt:
go generate ./internal/config
.PHONY: install clean uninstall
.PHONY: install clean uninstall installmisc lure

View File

@ -2,9 +2,10 @@
# LURE (Linux User REpository)
[![Go Report Card](https://goreportcard.com/badge/go.arsenm.dev/lure)](https://goreportcard.com/report/go.arsenm.dev/lure)
[![status-badge](https://ci.arsenm.dev/api/badges/Arsen6331/lure/status.svg)](https://ci.arsenm.dev/Arsen6331/lure)
[![linux-user-repository-bin AUR package](https://img.shields.io/aur/version/linux-user-repository-bin?label=lure-bin&logo=archlinux)](https://aur.archlinux.org/packages/linux-user-repository-bin/)
[![Go Report Card](https://goreportcard.com/badge/go.elara.ws/lure)](https://goreportcard.com/report/go.elara.ws/lure)
[![status-badge](https://ci.elara.ws/api/badges/lure/lure/status.svg)](https://ci.elara.ws/lure/lure)
[![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)
[![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/)
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.
@ -19,16 +20,16 @@ LURE is written in pure Go and has zero dependencies after building. The only th
The LURE install script will automatically download and install the appropriate LURE package on your system. To use it, simply run the following command:
```bash
curl https://www.arsenm.dev/lure.sh | bash
curl -fsSL lure.sh/install | bash
```
**IMPORTANT**: This method is not recommended as it executes any code that is stored at that URL. In order to make sure nothing malicious is going to occur, download the script and inspect it before running.
**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.
### Packages
Distro packages and binary archives are provided at the latest Gitea release: https://gitea.arsenm.dev/Arsen6331/lure/releases/latest
Distro packages and binary archives are provided at the latest Gitea release: https://gitea.elara.ws/lure/lure/releases/latest
LURE is also available on the AUR as [lure-bin](https://aur.archlinux.org/packages/lure-bin)
LURE is also available on the AUR as [linux-user-repository-bin](https://aur.archlinux.org/packages/linux-user-repository-bin)
### Building from source
@ -42,7 +43,7 @@ sudo make install
## Why?
Arch Linux's AUR is a very useful feature. It's one of the main reasons I and many others use Arch on most of their devices. However, Arch is not always a good choice. Whether you're running a server that needs to be stable over long periods of time, or you're a beginner and feel intimidated by Arch, there are many different reasons not to use it. Such useful functionality should not be restricted to only a single distro. That is what LURE is meant to solve.
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.
---
@ -54,29 +55,23 @@ The documentation for LURE is in the [docs](docs) directory in this repo.
## Web Interface
LURE now has a web interface! It's open source, licensed under the AGPLv3 (https://gitea.arsenm.dev/Arsen6331/lure-web), and is available at https://lure.arsenm.dev.
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/.
---
## Repositories
Unlike the AUR, LURE supports third-party repositories. Also unlike the AUR, LURE's repos are single git repositories containing all the build scripts. Inside each LURE repo, there is a separate directory for each package, containing a `lure.sh` script, which is a PKGBUILD-like build script for LURE. The default repository is hosted on Github: https://github.com/Arsen6331/lure-repo, and information about its packages is displayed at https://lure.arsenm.dev/pkgs.
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.
---
## Dependencies
## Acknowledgements
As mentioned before, LURE has zero dependencies after compilation. Thanks to the following projects for making this possible:
Thanks to the following projects for making LURE possible:
- https://github.com/mvdan/sh
- https://github.com/go-git/go-git
- https://github.com/mholt/archiver
- https://github.com/goreleaser/nfpm
- https://github.com/charmbracelet/bubbletea
- https://gitlab.com/cznic/sqlite
---
## Planned Features
- Automated docker-based testing tool
- https://gitlab.com/cznic/sqlite

761
build.go
View File

@ -1,6 +1,6 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Arsen Musayelyan
* Copyright (C) 2023 Elara 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
@ -19,719 +19,82 @@
package main
import (
"bytes"
"context"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
_ "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"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"go.arsenm.dev/logger/log"
"go.arsenm.dev/lure/distro"
"go.arsenm.dev/lure/internal/cliutils"
"go.arsenm.dev/lure/internal/config"
"go.arsenm.dev/lure/internal/cpu"
"go.arsenm.dev/lure/internal/db"
"go.arsenm.dev/lure/internal/dl"
"go.arsenm.dev/lure/internal/repos"
"go.arsenm.dev/lure/internal/shutils"
"go.arsenm.dev/lure/internal/shutils/decoder"
"go.arsenm.dev/lure/manager"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"lure.sh/lure/internal/config"
"lure.sh/lure/internal/osutils"
"lure.sh/lure/internal/types"
"lure.sh/lure/pkg/build"
"lure.sh/lure/pkg/loggerctx"
"lure.sh/lure/pkg/manager"
"lure.sh/lure/pkg/repos"
)
// BuildVars represents the script variables required
// to build a package
type BuildVars struct {
Name string `sh:"name,required"`
Version string `sh:"version,required"`
Release int `sh:"release,required"`
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"`
Conflicts []string `sh:"conflicts"`
Depends []string `sh:"deps"`
BuildDepends []string `sh:"build_deps"`
Replaces []string `sh:"replaces"`
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:"postremove"`
PreUpgrade string `sh:"preupgrade"`
PostUpgrade string `sh:"postupgrade"`
PreTrans string `sh:"pretrans"`
PostTrans string `sh:"posttrans"`
}
func buildCmd(c *cli.Context) error {
script := c.String("script")
err := repos.Pull(c.Context, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect supported package manager on system").Send()
}
pkgPaths, _, err := buildPackage(c.Context, script, mgr, c.Bool("clean"))
if err != nil {
log.Fatal("Error building package").Err(err).Send()
}
wd, err := os.Getwd()
if err != nil {
log.Fatal("Error getting working directory").Err(err).Send()
}
for _, pkgPath := range pkgPaths {
name := filepath.Base(pkgPath)
err = os.Rename(pkgPath, filepath.Join(wd, name))
if err != nil {
log.Fatal("Error moving the package").Err(err).Send()
}
}
return nil
}
// buildPackage builds the script at the given path. It returns two slices. One contains the paths
// to the built package(s), the other contains the names of the built package(s).
func buildPackage(ctx context.Context, script string, mgr manager.Manager, clean bool) ([]string, []string, error) {
info, err := distro.ParseOSRelease(ctx)
if err != nil {
return nil, nil, err
}
var distroChanged bool
if distID, ok := os.LookupEnv("LURE_DISTRO"); ok {
info.ID = distID
// Since the distro was overwritten, we don't know what the
// like distros are, so set to nil
info.Like = nil
distroChanged = true
}
fl, err := os.Open(script)
if err != nil {
return nil, nil, err
}
file, err := syntax.NewParser().Parse(fl, "lure.sh")
if err != nil {
return nil, nil, err
}
fl.Close()
scriptDir := filepath.Dir(script)
env := genBuildEnv(info, scriptDir)
// The first pass is just used to get variable values and runs before
// the script is displayed, so it is restricted so as to prevent malicious
// code from executing.
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(rHelpers.ExecHandler(shutils.NopExec)),
interp.ReadDirHandler(shutils.RestrictedReadDir(scriptDir)),
interp.StatHandler(shutils.RestrictedStat(scriptDir)),
interp.OpenHandler(shutils.RestrictedOpen(scriptDir)),
)
if err != nil {
return nil, nil, err
}
err = runner.Run(ctx, file)
if err != nil {
return nil, nil, err
}
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
}
baseDir := filepath.Join(config.PkgsDir, vars.Name)
srcdir := filepath.Join(baseDir, "src")
pkgdir := filepath.Join(baseDir, "pkg")
if !clean {
builtPkgPath, ok, err := checkForBuiltPackage(mgr, &vars, getPkgFormat(mgr), baseDir)
if err != nil {
return nil, nil, err
}
if ok {
return []string{builtPkgPath}, nil, err
}
}
err = cliutils.PromptViewScript(script, vars.Name, cfg.PagerStyle, translator)
if err != nil {
log.Fatal("Failed to prompt user to view build script").Err(err).Send()
}
if !archMatches(vars.Architectures) {
buildAnyway, err := cliutils.YesNoPrompt("Your system's CPU architecture doesn't match this package. Do you want to build anyway?", true, translator)
if err != nil {
return nil, nil, err
}
if !buildAnyway {
os.Exit(1)
}
}
log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
// The second pass will be used to execute the actual functions,
// so it cannot be restricted. The script has already been displayed
// to the user by this point, so it should be safe
runner, err = interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.StdIO(os.Stdin, os.Stdout, os.Stderr),
interp.ExecHandler(helpers.ExecHandler(nil)),
)
if err != nil {
return nil, nil, err
}
err = runner.Run(ctx, file)
if err != nil {
return nil, nil, err
}
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
}
err = os.RemoveAll(baseDir)
if err != nil {
return nil, nil, err
}
err = os.MkdirAll(srcdir, 0o755)
if err != nil {
return nil, nil, err
}
err = os.MkdirAll(pkgdir, 0o755)
if err != nil {
return nil, nil, err
}
installed, err := mgr.ListInstalled(nil)
if err != nil {
return nil, nil, err
}
if instVer, ok := installed[vars.Name]; ok {
log.Warn("This package is already installed").
Str("name", vars.Name).
Str("version", instVer).
Send()
}
var buildDeps []string
if len(vars.BuildDepends) > 0 {
found, notFound, err := repos.FindPkgs(gdb, vars.BuildDepends)
if err != nil {
return nil, nil, err
}
found = filterBuildDeps(found, installed)
log.Info("Installing build dependencies").Send()
flattened := cliutils.FlattenPkgs(found, "install", translator)
buildDeps = packageNames(flattened)
installPkgs(ctx, flattened, notFound, mgr, clean)
}
var builtDeps, builtNames, repoDeps []string
if len(vars.Depends) > 0 {
log.Info("Installing dependencies").Send()
found, notFound, err := repos.FindPkgs(gdb, vars.Depends)
if err != nil {
return nil, nil, err
}
scripts := getScriptPaths(cliutils.FlattenPkgs(found, "install", translator))
for _, script := range scripts {
pkgPaths, pkgNames, err := buildPackage(ctx, script, mgr, clean)
if err != nil {
return nil, nil, err
}
builtDeps = append(builtDeps, pkgPaths...)
builtNames = append(builtNames, pkgNames...)
builtNames = append(builtNames, filepath.Base(filepath.Dir(script)))
}
repoDeps = notFound
}
log.Info("Downloading sources").Send()
err = getSources(ctx, srcdir, &vars)
if err != nil {
return nil, nil, err
}
err = setDirVars(ctx, runner, srcdir, pkgdir)
if err != nil {
return nil, nil, err
}
fn, ok := dec.GetFunc("version")
if ok {
log.Info("Executing version()").Send()
buf := &bytes.Buffer{}
err = fn(
ctx,
interp.Dir(srcdir),
interp.StdIO(os.Stdin, buf, os.Stderr),
)
if err != nil {
return nil, nil, err
}
newVer := strings.TrimSpace(buf.String())
err = setVersion(ctx, runner, newVer)
if err != nil {
return nil, nil, err
}
vars.Version = newVer
log.Info("Updating version").Str("new", newVer).Send()
}
fn, ok = dec.GetFunc("prepare")
if ok {
log.Info("Executing prepare()").Send()
err = fn(ctx, interp.Dir(srcdir))
if err != nil {
return nil, nil, err
}
}
fn, ok = dec.GetFunc("build")
if ok {
log.Info("Executing build()").Send()
err = fn(ctx, interp.Dir(srcdir))
if err != nil {
return nil, nil, err
}
}
fn, ok = dec.GetFunc("package")
if ok {
log.Info("Executing package()").Send()
err = fn(ctx, interp.Dir(srcdir))
if err != nil {
return nil, nil, err
}
} else {
log.Fatal("The package() function is required").Send()
}
log.Info("Building package metadata").Str("name", vars.Name).Send()
uniq(
&repoDeps,
&builtDeps,
&builtNames,
)
pkgInfo := &nfpm.Info{
Name: vars.Name,
Description: vars.Description,
Arch: cpu.Arch(),
Version: vars.Version,
Release: strconv.Itoa(vars.Release),
Homepage: vars.Homepage,
License: strings.Join(vars.Licenses, ", "),
Maintainer: vars.Maintainer,
Overridables: nfpm.Overridables{
Conflicts: vars.Conflicts,
Replaces: vars.Replaces,
Provides: vars.Provides,
Depends: append(repoDeps, builtNames...),
var buildCmd = &cli.Command{
Name: "build",
Usage: "Build a local package",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "script",
Aliases: []string{"s"},
Value: "lure.sh",
Usage: "Path to the build script",
},
}
&cli.StringFlag{
Name: "package",
Aliases: []string{"p"},
Usage: "Name of the package to build and its repo (example: default/go-bin)",
},
&cli.BoolFlag{
Name: "clean",
Aliases: []string{"c"},
Usage: "Build package from scratch even if there's an already built package available",
},
},
Action: func(c *cli.Context) error {
ctx := c.Context
log := loggerctx.From(ctx)
if vars.Epoch != 0 {
pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10)
}
script := c.String("script")
if c.String("package") != "" {
script = filepath.Join(config.GetPaths(ctx).RepoDir, c.String("package"), "lure.sh")
}
setScripts(&vars, pkgInfo, filepath.Dir(script))
err := repos.Pull(ctx, config.Config(ctx).Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
if slices.Contains(vars.Architectures, "all") {
pkgInfo.Arch = "all"
}
mgr := manager.Detect()
if mgr == nil {
log.Fatal("Unable to detect a supported package manager on the system").Send()
}
contents := []*files.Content{}
filepath.Walk(pkgdir, func(path string, fi os.FileInfo, err error) error {
trimmed := strings.TrimPrefix(path, pkgdir)
pkgPaths, _, err := build.BuildPackage(ctx, types.BuildOpts{
Script: script,
Manager: mgr,
Clean: c.Bool("clean"),
Interactive: c.Bool("interactive"),
})
if err != nil {
log.Fatal("Error building package").Err(err).Send()
}
if fi.IsDir() {
f, err := os.Open(path)
wd, err := os.Getwd()
if err != nil {
log.Fatal("Error getting working directory").Err(err).Send()
}
for _, pkgPath := range pkgPaths {
name := filepath.Base(pkgPath)
err = osutils.Move(pkgPath, filepath.Join(wd, name))
if err != nil {
return err
log.Fatal("Error moving the package").Err(err).Send()
}
_, err = f.Readdirnames(1)
if err != io.EOF {
return nil
}
contents = append(contents, &files.Content{
Source: path,
Destination: trimmed,
Type: "dir",
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
},
})
f.Close()
return nil
}
if fi.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(path)
if err != nil {
return err
}
link = strings.TrimPrefix(link, pkgdir)
contents = append(contents, &files.Content{
Source: link,
Destination: trimmed,
Type: "symlink",
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
},
})
return nil
}
fileContent := &files.Content{
Source: path,
Destination: trimmed,
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
Size: fi.Size(),
},
}
if slices.Contains(vars.Backup, trimmed) {
fileContent.Type = "config|noreplace"
}
contents = append(contents, fileContent)
return nil
})
pkgInfo.Overridables.Contents = contents
packager, err := nfpm.Get(getPkgFormat(mgr))
if err != nil {
return nil, nil, err
}
pkgName := packager.ConventionalFileName(pkgInfo)
pkgPath := filepath.Join(baseDir, pkgName)
pkgPaths := append(builtDeps, pkgPath)
pkgNames := append(builtNames, vars.Name)
pkgFile, err := os.Create(pkgPath)
if err != nil {
return nil, nil, err
}
log.Info("Compressing package").Str("name", pkgName).Send()
err = packager.Package(pkgInfo, pkgFile)
if err != nil {
return nil, nil, err
}
if len(buildDeps) > 0 {
removeBuildDeps, err := cliutils.YesNoPrompt("Would you like to remove build dependencies?", false, translator)
if err != nil {
return nil, nil, err
}
if removeBuildDeps {
err = mgr.Remove(
&manager.Opts{
AsRoot: true,
NoConfirm: true,
},
buildDeps...,
)
if err != nil {
return nil, nil, err
}
}
}
uniq(&pkgPaths, &pkgNames)
return pkgPaths, pkgNames, nil
}
func checkForBuiltPackage(mgr manager.Manager, vars *BuildVars, pkgFormat, baseDir string) (string, bool, error) {
filename, err := pkgFileName(vars, pkgFormat)
if err != nil {
return "", false, err
}
pkgPath := filepath.Join(baseDir, filename)
_, err = os.Stat(pkgPath)
if err != nil {
return "", false, nil
}
return pkgPath, true, nil
}
func pkgFileName(vars *BuildVars, pkgFormat string) (string, error) {
pkgInfo := &nfpm.Info{
Name: vars.Name,
Arch: cpu.Arch(),
Version: vars.Version,
Release: strconv.Itoa(vars.Release),
Epoch: strconv.FormatUint(uint64(vars.Epoch), 10),
}
packager, err := nfpm.Get(pkgFormat)
if err != nil {
return "", err
}
return packager.ConventionalFileName(pkgInfo), nil
}
func getPkgFormat(mgr manager.Manager) string {
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok {
pkgFormat = format
}
return pkgFormat
}
func genBuildEnv(info *distro.OSRelease, scriptdir string) []string {
env := os.Environ()
env = append(
env,
"DISTRO_NAME="+info.Name,
"DISTRO_PRETTY_NAME="+info.PrettyName,
"DISTRO_ID="+info.ID,
"DISTRO_VERSION_ID="+info.VersionID,
"DISTRO_ID_LIKE="+strings.Join(info.Like, " "),
"ARCH="+cpu.Arch(),
"NCPU="+strconv.Itoa(runtime.NumCPU()),
"scriptdir="+scriptdir,
)
return env
}
func getSources(ctx context.Context, srcdir string, bv *BuildVars) error {
if len(bv.Sources) != len(bv.Checksums) {
log.Fatal("The checksums array must be the same length as sources")
}
for i, src := range bv.Sources {
opts := dl.Options{
Name: fmt.Sprintf("%s[%d]", bv.Name, i),
URL: src,
Destination: srcdir,
Progress: os.Stderr,
}
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
checksum, err := hex.DecodeString(bv.Checksums[i])
if err != nil {
return err
}
opts.SHA256 = checksum
}
err := dl.Download(ctx, opts)
if err != nil {
return err
}
}
return nil
}
// setDirVars sets srcdir and pkgdir. It's a very hacky way of doing so,
// but setting the runner's Env and Vars fields doesn't seem to work.
func setDirVars(ctx context.Context, runner *interp.Runner, srcdir, pkgdir string) error {
cmd := "srcdir='" + srcdir + "'\npkgdir='" + pkgdir + "'\n"
fl, err := syntax.NewParser().Parse(strings.NewReader(cmd), "vars")
if err != nil {
return err
}
return runner.Run(ctx, fl)
}
func setScripts(vars *BuildVars, info *nfpm.Info, scriptDir string) {
if vars.Scripts.PreInstall != "" {
info.Scripts.PreInstall = filepath.Join(scriptDir, vars.Scripts.PreInstall)
}
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)
}
}
// archMatches checks if your system architecture matches
// one of the provided architectures
func archMatches(architectures []string) bool {
if slices.Contains(architectures, "all") {
return true
}
if slices.Contains(architectures, "arm") {
architectures = append(architectures, cpu.ARMVariant())
}
return slices.Contains(architectures, cpu.Arch())
}
func setVersion(ctx context.Context, r *interp.Runner, to string) error {
fl, err := syntax.NewParser().Parse(strings.NewReader("version='"+to+"'"), "")
if err != nil {
return err
}
return r.Run(ctx, fl)
}
func filterBuildDeps(found map[string][]db.Package, installed map[string]string) map[string][]db.Package {
out := map[string][]db.Package{}
for name, pkgs := range found {
var inner []db.Package
for _, pkg := range pkgs {
if _, ok := installed[pkg.Name]; !ok {
addToFiltered := true
for _, provides := range pkg.Provides.Val {
if _, ok := installed[provides]; ok {
addToFiltered = false
break
}
}
if addToFiltered {
inner = append(inner, pkg)
}
}
}
if len(inner) > 0 {
out[name] = inner
}
}
return out
}
func packageNames(pkgs []db.Package) []string {
names := make([]string, len(pkgs))
for i, p := range pkgs {
names[i] = p.Name
}
return names
}
// uniq removes all duplicates from string slices
func uniq(ss ...*[]string) {
for _, s := range ss {
slices.Sort(*s)
*s = slices.Compact(*s)
}
},
}

View File

@ -1,3 +0,0 @@
FROM alpine:latest
COPY lure-api-server /usr/bin/lure-api-server
ENTRYPOINT lure-api-server

View File

@ -1,3 +0,0 @@
# lure-api-server
`lure-api-server` is the backend API server for lure-web, the web interface for LURE.

View File

@ -1,205 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"context"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/twitchtv/twirp"
"go.arsenm.dev/logger/log"
"go.arsenm.dev/lure/internal/api"
"go.arsenm.dev/lure/internal/config"
"go.arsenm.dev/lure/internal/db"
"golang.org/x/text/language"
)
type lureWebAPI struct {
db *sqlx.DB
}
func (l lureWebAPI) Search(ctx context.Context, req *api.SearchRequest) (*api.SearchResponse, error) {
query := "(name LIKE ? OR description LIKE ? OR json_array_contains(provides, ?))"
args := []any{"%" + req.Query + "%", "%" + req.Query + "%", req.Query}
if req.FilterValue != nil && req.FilterType != api.FILTER_TYPE_NO_FILTER {
switch req.FilterType {
case api.FILTER_TYPE_IN_REPOSITORY:
query += " AND repository = ?"
case api.FILTER_TYPE_SUPPORTS_ARCH:
query += " AND json_array_contains(architectures, ?)"
}
args = append(args, *req.FilterValue)
}
if req.SortBy != api.SORT_BY_UNSORTED {
switch req.SortBy {
case api.SORT_BY_NAME:
query += " ORDER BY name"
case api.SORT_BY_REPOSITORY:
query += " ORDER BY repository"
case api.SORT_BY_VERSION:
query += " ORDER BY version"
}
}
if req.Limit != 0 {
query += " LIMIT " + strconv.FormatInt(req.Limit, 10)
}
result, err := db.GetPkgs(l.db, query, args...)
if err != nil {
return nil, err
}
out := &api.SearchResponse{}
for result.Next() {
pkg := &db.Package{}
err = result.StructScan(pkg)
if err != nil {
return nil, err
}
out.Packages = append(out.Packages, dbPkgToAPI(ctx, pkg))
}
return out, err
}
func (l lureWebAPI) GetPkg(ctx context.Context, req *api.GetPackageRequest) (*api.Package, error) {
pkg, err := db.GetPkg(l.db, "name = ? AND repository = ?", req.Name, req.Repository)
if err != nil {
return nil, err
}
return dbPkgToAPI(ctx, pkg), nil
}
func (l lureWebAPI) GetBuildScript(ctx context.Context, req *api.GetBuildScriptRequest) (*api.GetBuildScriptResponse, error) {
if strings.ContainsAny(req.Name, "./") || strings.ContainsAny(req.Repository, "./") {
return nil, twirp.NewError(twirp.InvalidArgument, "name and repository must not contain . or /")
}
scriptPath := filepath.Join(config.RepoDir, req.Repository, req.Name, "lure.sh")
_, err := os.Stat(scriptPath)
if os.IsNotExist(err) {
return nil, twirp.NewError(twirp.NotFound, "requested package not found")
} else if err != nil {
return nil, err
}
data, err := os.ReadFile(scriptPath)
if err != nil {
return nil, err
}
return &api.GetBuildScriptResponse{Script: string(data)}, nil
}
func dbPkgToAPI(ctx context.Context, pkg *db.Package) *api.Package {
return &api.Package{
Name: pkg.Name,
Repository: pkg.Repository,
Version: pkg.Version,
Release: int64(pkg.Release),
Epoch: ptr(int64(pkg.Epoch)),
Description: performTranslation(ctx, pkg.Description.Val),
Homepage: performTranslation(ctx, pkg.Homepage.Val),
Maintainer: performTranslation(ctx, pkg.Maintainer.Val),
Architectures: pkg.Architectures.Val,
Licenses: pkg.Licenses.Val,
Provides: pkg.Provides.Val,
Conflicts: pkg.Conflicts.Val,
Replaces: pkg.Replaces.Val,
Depends: dbMapToAPI(pkg.Depends.Val),
BuildDepends: dbMapToAPI(pkg.BuildDepends.Val),
}
}
func ptr[T any](v T) *T {
return &v
}
func dbMapToAPI(m map[string][]string) map[string]*api.StringList {
out := make(map[string]*api.StringList, len(m))
for override, list := range m {
out[override] = &api.StringList{Entries: list}
}
return out
}
func performTranslation(ctx context.Context, v map[string]string) *string {
alVal := ctx.Value(acceptLanguageKey{})
langVal := ctx.Value(langParameterKey{})
if alVal == nil && langVal == nil {
val, ok := v[""]
if !ok {
return ptr("<unknown>")
}
return &val
}
al, _ := alVal.(string)
lang, _ := langVal.(string)
tags, _, err := language.ParseAcceptLanguage(al)
if err != nil {
log.Warn("Error parsing Accept-Language header").Err(err).Send()
}
var bases []string
if lang != "" {
langTag, err := language.Parse(lang)
if err != nil {
log.Warn("Error parsing lang parameter").Err(err).Send()
bases = getLangBases(tags)
} else {
bases = getLangBases(append([]language.Tag{langTag}, tags...))
}
} else {
bases = getLangBases(tags)
}
if len(bases) == 1 {
bases = []string{"en", ""}
}
for _, name := range bases {
val, ok := v[name]
if !ok {
continue
}
return &val
}
return ptr("<unknown>")
}
func getLangBases(langs []language.Tag) []string {
out := make([]string, len(langs)+1)
for i, lang := range langs {
base, _ := lang.Base()
out[i] = base.String()
}
out[len(out)-1] = ""
return out
}

View File

@ -1,34 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"go.arsenm.dev/logger/log"
"go.arsenm.dev/lure/internal/config"
"go.arsenm.dev/lure/internal/types"
)
var cfg types.Config
func init() {
err := config.Decode(&cfg)
if err != nil {
log.Fatal("Error decoding config file").Err(err).Send()
}
}

View File

@ -1,36 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"github.com/jmoiron/sqlx"
"go.arsenm.dev/logger/log"
"go.arsenm.dev/lure/internal/config"
"go.arsenm.dev/lure/internal/db"
)
var gdb *sqlx.DB
func init() {
var err error
gdb, err = db.Open(config.DBPath)
if err != nil {
log.Fatal("Error opening database").Err(err).Send()
}
}

View File

@ -1,14 +0,0 @@
#!/bin/bash
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build
docker buildx build --platform linux/amd64 --tag arsen6331/lure-api-server:amd64 --no-cache .
CGO_ENABLED=0 GOARCH=arm64 GOOS=linux go build
docker buildx build --platform linux/arm64/v8 --tag arsen6331/lure-api-server:arm64 --no-cache .
docker login
docker push arsen6331/lure-api-server -a
docker manifest rm arsen6331/lure-api-server:latest
docker manifest create arsen6331/lure-api-server:latest --amend arsen6331/lure-api-server:arm64 --amend arsen6331/lure-api-server:amd64
docker manifest push arsen6331/lure-api-server:latest

View File

@ -1,117 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 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 (
"context"
"flag"
"net"
"net/http"
"os"
"github.com/twitchtv/twirp"
"go.arsenm.dev/logger"
"go.arsenm.dev/logger/log"
"go.arsenm.dev/lure/internal/api"
"go.arsenm.dev/lure/internal/repos"
)
func init() {
log.Logger = logger.NewPretty(os.Stderr)
}
func main() {
ctx := context.Background()
addr := flag.String("a", ":8080", "Listen address for API server")
logFile := flag.String("l", "", "Output file for JSON log")
flag.Parse()
if *logFile != "" {
fl, err := os.Create(*logFile)
if err != nil {
log.Fatal("Error creating log file").Err(err).Send()
}
defer fl.Close()
log.Logger = logger.NewMulti(log.Logger, logger.NewJSON(fl))
}
err := repos.Pull(ctx, gdb, cfg.Repos)
if err != nil {
log.Fatal("Error pulling repositories").Err(err).Send()
}
sigCh := make(chan struct{}, 200)
go repoPullWorker(ctx, sigCh)
var handler http.Handler
handler = api.NewAPIServer(
lureWebAPI{db: gdb},
twirp.WithServerPathPrefix(""),
)
handler = withAcceptLanguage(handler)
handler = allowAllCORSHandler(handler)
handler = handleWebhook(handler, sigCh)
ln, err := net.Listen("tcp", *addr)
if err != nil {
log.Fatal("Error starting listener").Err(err).Send()
}
log.Info("Starting HTTP API server").Str("addr", ln.Addr().String()).Send()
err = http.Serve(ln, handler)
if err != nil {
log.Fatal("Error while running server").Err(err).Send()
}
}
func allowAllCORSHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Access-Control-Allow-Origin", "*")
res.Header().Set("Access-Control-Allow-Headers", "*")
if req.Method == http.MethodOptions {
return
}
h.ServeHTTP(res, req)
})
}
type (
acceptLanguageKey struct{}
langParameterKey struct{}
)
func withAcceptLanguage(h http.Handler) http.Handler {
return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()
langs := req.Header.Get("Accept-Language")
ctx = context.WithValue(ctx, acceptLanguageKey{}, langs)
lang := req.URL.Query().Get("lang")
ctx = context.WithValue(ctx, langParameterKey{}, lang)
req = req.WithContext(ctx)
h.ServeHTTP(res, req)
})
}

View File

@ -1,107 +0,0 @@
/*
* LURE - Linux User REpository
* Copyright (C) 2023 Arsen Musayelyan
*
* This program is free software: you can redistribute it and/or modify