Compare commits
No commits in common. "v0.0.7" and "master" have entirely different histories.
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
liberapay: lure
|
@ -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'
|
||||
|
@ -1,3 +1,4 @@
|
||||
platform: linux/amd64
|
||||
pipeline:
|
||||
release:
|
||||
image: goreleaser/goreleaser
|
||||
|
10
Makefile
10
Makefile
@ -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
|
33
README.md
33
README.md
@ -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
761
build.go
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
FROM alpine:latest
|
||||
COPY lure-api-server /usr/bin/lure-api-server
|
||||
ENTRYPOINT lure-api-server
|
@ -1,3 +0,0 @@
|
||||
# lure-api-server
|
||||
|
||||
`lure-api-server` is the backend API server for lure-web, the web interface for LURE.
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
@ -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)
|
||||
})
|
||||
}
|
@ -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
|
||||