Compare commits
	
		
			191 Commits
		
	
	
		
			674cfe6b0d
			...
			v0.0.7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4d97210635 | |||
| 51f05ecc81 | |||
| 27082ba223 | |||
| 6c6a7152b4 | |||
| 681e31df4e | |||
| a84a9be782 | |||
| 825e89d0d3 | |||
| eb88fbd123 | |||
| 3c5613199e | |||
| 73bdb54496 | |||
| 4b5fd855cc | |||
| 2c8eb6be48 | |||
| d4396756d6 | |||
| c8be92c47b | |||
| ee7f4878d1 | |||
| bd769468a9 | |||
| 09d9053104 | |||
| d914391c40 | |||
| 0ece536810 | |||
| 192e3e8a4f | |||
| fa4604c26c | |||
| 163ad12575 | |||
| 2ba6136c99 | |||
| 438304f8ce | |||
| 657b562f31 | |||
| d26b288cde | |||
| e785c6b53d | |||
| ff8ed902ea | |||
| 1d2d46cbc5 | |||
| 374c206fae | |||
| d906dc8d86 | |||
| 2f81f7c605 | |||
| bb05a8d38b | |||
| 076f90bbd7 | |||
| e772ecf2ab | |||
| 3e0c110893 | |||
| 238f4cf682 | |||
| c41e7e1c18 | |||
| 0144ad17d9 | |||
| ddd9d1d63d | |||
| 0cb23911cf | |||
| de8399e40b | |||
| d226a6c154 | |||
| c2e0332552 | |||
| 43baf8024a | |||
| 8a1d0f4f54 | |||
| 806f49c472 | |||
| 55132437b3 | |||
| 8b26e96af0 | |||
| bd41075e8a | |||
| 8225f41d0e | |||
| db060db4b1 | |||
| 863b6e923a | |||
| 51b41bdd90 | |||
| 76e073c77f | |||
| 770881bf67 | |||
| be79eba4c2 | |||
| 3e6d5f57cf | |||
| db5c344b2d | |||
| a750f4630e | |||
| 852e98088b | |||
| c832359e43 | |||
| fae4337748 | |||
| f21d02e5d1 | |||
| ef98a37b4a | |||
| 3829dc4cc4 | |||
| efca9d116e | |||
| acb71b873e | |||
| 8366a2ce86 | |||
| 37a6c682b5 | |||
| 80187b0969 | |||
| cecaead5b0 | |||
| a84622e182 | |||
| dd86148c22 | |||
| 6aed71af24 | |||
| 422e41db40 | |||
| 8dc0e53700 | |||
| 891df91fee | |||
| b7922f9687 | |||
| 0b53c16f9c | |||
| 81013ce376 | |||
| eb4e2281fa | |||
| c9bc1e7fc3 | |||
| 22a74aa793 | |||
| 964c45ffc4 | |||
| 9c0d9d0a34 | |||
| a8a870ce86 | |||
| fe477ba6d1 | |||
| 6cd0802f64 | |||
| 19ced9795c | |||
| dbfcde0125 | |||
| 14078ec904 | |||
| 715ec7fbe8 | |||
| 3c260a7c5f | |||
| 5e8ed1c2a0 | |||
| c51248793e | |||
| 5ed538c2c4 | |||
| 56ba3ec644 | |||
| 5d411ac538 | |||
| 269222b688 | |||
| 79c0e7eb12 | |||
| fcd5c1c14b | |||
| 9fa3977d3a | |||
| c2d396d68f | |||
| 23fd711be2 | |||
| 8f9bdf69ad | |||
| dd33a30c35 | |||
| a106100312 | |||
| df72d95ab7 | |||
| 40cf0da2c0 | |||
| 66a6b30d87 | |||
| 4eac9cd8aa | |||
| 664f01a794 | |||
| 0ac8ccac81 | |||
| 56550a5135 | |||
| b4f4633f6a | |||
| 67b9801f42 | |||
| 7164aac0b4 | |||
| 4e71a5c35c | |||
| eaf49a4594 | |||
| c0439a2b90 | |||
| 43d6461c71 | |||
| 2e591d9f1c | |||
| 7d00c7b5fb | |||
| ead0c79139 | |||
| 1377ef1bc9 | |||
| a88adb43fe | |||
| 3663a8ef8f | |||
| 5f12d2aee2 | |||
| 99b70859d1 | |||
| 715fd6ccc9 | |||
| 9a06894cfa | |||
| 01a9f23a64 | |||
| 6013bdf8b9 | |||
| fbf0aa3b4f | |||
| e4b8348823 | |||
| 554987325b | |||
| 74051861bf | |||
| edf5b67825 | |||
| 84336e4a30 | |||
| b3479bdf91 | |||
| a2bd151837 | |||
| 46e2d3166f | |||
| 3437df8676 | |||
| 3361358b3c | |||
| 3ca052fea7 | |||
| 001e33dd2f | |||
| f30f4c7081 | |||
| 5bc81e3a30 | |||
| d941ce231e | |||
| e22bc0f10c | |||
| 8ff903b68f | |||
| 3bb7fe3690 | |||
| 26d139c34e | |||
| 8ceb61de9a | |||
| 24c807a941 | |||
| da7830d0e3 | |||
| 98a3b26a27 | |||
| da630f648d | |||
| c489f4864e | |||
| 320342cfb4 | |||
| 45ad9fbe39 | |||
| 3f2ec8ebd3 | |||
| 2c2a27c9f7 | |||
| 05a1ecea64 | |||
| d32437e8b2 | |||
| 8f95ff4676 | |||
| 7442da7105 | |||
| 07e41849e9 | |||
| 27fb08d5ba | |||
| d78064179f | |||
| 2157d9ecce | |||
| b686f810fb | |||
| c856bf0686 | |||
| c650c1dae0 | |||
| b3b6612ef2 | |||
| baf4cca4fb | |||
| e604f61151 | |||
| 8e74e58cad | |||
| be48f26e75 | |||
| b6265f4b1d | |||
| c0e535c630 | |||
| 2b6815e287 | |||
| a42c9b27e7 | |||
| e2c8335381 | |||
| b56641c659 | |||
| 61ba975e21 | |||
| 75a60070ba | |||
| a02a009b63 | |||
| 74adb915fc | |||
| bdca0a5ffc | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,2 +1,5 @@
 | 
			
		||||
/lure
 | 
			
		||||
/lure-api-server
 | 
			
		||||
/cmd/lure-api-server/lure-api-server
 | 
			
		||||
/dist/
 | 
			
		||||
/internal/config/version.txt
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
before:
 | 
			
		||||
  hooks:
 | 
			
		||||
    - go mod tidy
 | 
			
		||||
    - go generate
 | 
			
		||||
builds:
 | 
			
		||||
  - id: lure
 | 
			
		||||
    env:
 | 
			
		||||
@@ -9,18 +10,21 @@ builds:
 | 
			
		||||
    goos:
 | 
			
		||||
      - linux
 | 
			
		||||
    goarch:
 | 
			
		||||
      - 386
 | 
			
		||||
      - amd64
 | 
			
		||||
      - arm
 | 
			
		||||
      - 386
 | 
			
		||||
      - arm64
 | 
			
		||||
      - arm
 | 
			
		||||
      - riscv64
 | 
			
		||||
archives:
 | 
			
		||||
  - replacements:
 | 
			
		||||
      386: i386
 | 
			
		||||
      amd64: x86_64
 | 
			
		||||
      arm64: aarch64
 | 
			
		||||
    files:
 | 
			
		||||
      - scripts/completion/*
 | 
			
		||||
nfpms:
 | 
			
		||||
  - id: lure
 | 
			
		||||
    package_name: linux-user-repository
 | 
			
		||||
    file_name_template: '{{.PackageName}}-{{.Version}}-{{.Os}}-{{.Arch}}'
 | 
			
		||||
    description: "Linux User REpository"
 | 
			
		||||
    replacements:
 | 
			
		||||
@@ -28,31 +32,45 @@ nfpms:
 | 
			
		||||
      amd64: x86_64
 | 
			
		||||
      arm64: aarch64
 | 
			
		||||
    homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
 | 
			
		||||
    maintainer: 'Arsen Musyaelyan <arsen@arsenm.dev>'
 | 
			
		||||
    maintainer: 'Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
    license: GPLv3
 | 
			
		||||
    formats:
 | 
			
		||||
      - apk
 | 
			
		||||
      - deb
 | 
			
		||||
      - rpm
 | 
			
		||||
      - archlinux
 | 
			
		||||
    provides:
 | 
			
		||||
      - linux-user-repository
 | 
			
		||||
    conflicts:
 | 
			
		||||
      - linux-user-repository
 | 
			
		||||
    contents:
 | 
			
		||||
      - src: scripts/completion/bash
 | 
			
		||||
        dst: /usr/share/bash-completion/completions/lure
 | 
			
		||||
      - src: scripts/completion/zsh
 | 
			
		||||
        dst: /usr/share/zsh/site-functions/_lure
 | 
			
		||||
aurs:
 | 
			
		||||
  - name: lure-bin
 | 
			
		||||
  - name: linux-user-repository-bin
 | 
			
		||||
    homepage: 'https://gitea.arsenm.dev/Arsen6331/lure'
 | 
			
		||||
    description: "Linux User REpository"
 | 
			
		||||
    maintainers:
 | 
			
		||||
      - 'Arsen Musyaelyan <arsen@arsenm.dev>'
 | 
			
		||||
      - 'Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
    license: GPLv3
 | 
			
		||||
    private_key: '{{ .Env.AUR_KEY }}'
 | 
			
		||||
    git_url: 'ssh://aur@aur.archlinux.org/lure-bin.git'
 | 
			
		||||
    git_url: 'ssh://aur@aur.archlinux.org/linux-user-repository-bin.git'
 | 
			
		||||
    provides:
 | 
			
		||||
      - lure
 | 
			
		||||
      - linux-user-repository
 | 
			
		||||
    conflicts:
 | 
			
		||||
      - lure
 | 
			
		||||
      - linux-user-repository
 | 
			
		||||
    depends:
 | 
			
		||||
      - sudo
 | 
			
		||||
      - pacman
 | 
			
		||||
    package: |-
 | 
			
		||||
      # binaries
 | 
			
		||||
      install -Dm755 "./lure" "${pkgdir}/usr/bin/lure"
 | 
			
		||||
      install -Dm755 ./lure "${pkgdir}/usr/bin/lure"
 | 
			
		||||
 | 
			
		||||
      # completions
 | 
			
		||||
      install -Dm755 ./scripts/completion/bash ${pkgdir}/usr/share/bash-completion/completions/lure
 | 
			
		||||
      install -Dm755 ./scripts/completion/zsh ${pkgdir}/usr/share/zsh/site-functions/_lure
 | 
			
		||||
release:
 | 
			
		||||
  gitea:
 | 
			
		||||
    owner: Arsen6331
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.woodpecker.yml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
pipeline:
 | 
			
		||||
  release:
 | 
			
		||||
    image: goreleaser/goreleaser
 | 
			
		||||
    commands:
 | 
			
		||||
      - goreleaser release
 | 
			
		||||
    secrets: [ gitea_token, aur_key ]
 | 
			
		||||
    when:
 | 
			
		||||
      event: tag
 | 
			
		||||
							
								
								
									
										15
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								Makefile
									
									
									
									
									
								
							@@ -1,13 +1,22 @@
 | 
			
		||||
lure:
 | 
			
		||||
PREFIX ?= /usr/local
 | 
			
		||||
 | 
			
		||||
lure: internal/config/version.txt
 | 
			
		||||
	go build
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	rm -f lure
 | 
			
		||||
 | 
			
		||||
install: lure
 | 
			
		||||
	sudo install -Dm755 lure /usr/local/bin/lure
 | 
			
		||||
install: lure installmisc
 | 
			
		||||
	install -Dm755 lure $(DESTDIR)$(PREFIX)/bin/lure
 | 
			
		||||
 | 
			
		||||
installmisc:
 | 
			
		||||
	install -Dm755 scripts/completion/bash $(DESTDIR)$(PREFIX)/share/bash-completion/completions/lure
 | 
			
		||||
	install -Dm755 scripts/completion/zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_lure
 | 
			
		||||
 | 
			
		||||
uninstall:
 | 
			
		||||
	rm -f /usr/local/bin/lure
 | 
			
		||||
	
 | 
			
		||||
internal/config/version.txt:
 | 
			
		||||
	go generate ./internal/config
 | 
			
		||||
 | 
			
		||||
.PHONY: install clean uninstall
 | 
			
		||||
							
								
								
									
										55
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								README.md
									
									
									
									
									
								
							@@ -1,16 +1,31 @@
 | 
			
		||||
<img src="assets/logo.png" alt="LURE Logo" width="200">
 | 
			
		||||
 | 
			
		||||
# LURE (Linux User REpository)
 | 
			
		||||
 | 
			
		||||
[](https://goreportcard.com/report/go.arsenm.dev/lure)
 | 
			
		||||
[](https://aur.archlinux.org/packages/lure-bin/)
 | 
			
		||||
[](https://ci.arsenm.dev/Arsen6331/lure)
 | 
			
		||||
[](https://aur.archlinux.org/packages/linux-user-repository-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.
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
LURE is written in pure Go and has zero dependencies after it's built. The only things LURE needs are a command for privilege elevation such as `sudo`, `doas`, etc. as well as a supported package manager. Currently, LURE supports `apt`, `pacman`, `apk`, `dnf`, `yum`, and `zypper`. If a supported package manager exists on your system, it will be detected and used automatically.
 | 
			
		||||
LURE is written in pure Go and has zero dependencies after building. The only things LURE requires are a command for privilege elevation such as `sudo`, `doas`, etc. as well as a supported package manager. Currently, LURE supports `apt`, `pacman`, `apk`, `dnf`, `yum`, and `zypper`. If a supported package manager exists on your system, it will be detected and used automatically.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Installation
 | 
			
		||||
 | 
			
		||||
### Install script
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**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.
 | 
			
		||||
 | 
			
		||||
### Packages
 | 
			
		||||
 | 
			
		||||
Distro packages and binary archives are provided at the latest Gitea release: https://gitea.arsenm.dev/Arsen6331/lure/releases/latest
 | 
			
		||||
 | 
			
		||||
LURE is also available on the AUR as [lure-bin](https://aur.archlinux.org/packages/lure-bin)
 | 
			
		||||
@@ -27,11 +42,7 @@ sudo make install
 | 
			
		||||
 | 
			
		||||
## Why?
 | 
			
		||||
 | 
			
		||||
The AUR is an amazing feature, and it's one of the main reasons I use Arch on all my daily driver devices. It is really simple while providing really useful functionality. I feel such a solution shouldn't be stuck in only a single distro, so I made LURE.
 | 
			
		||||
 | 
			
		||||
Like the AUR, it uses simple bash build scripts, but it doesn't depend on bash being installed at all. It uses an embedded, pure Go implementation of bash instead. Similarly, it uses Git to download the repos and sources, but doesn't depend on Git being installed.
 | 
			
		||||
 | 
			
		||||
This means it's really easy to deploy LURE on any distro that it has support for and on any CPU architecture. It also supports and automatically detects many package managers, so it's not limited to just `pacman`.
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
@@ -41,39 +52,31 @@ The documentation for LURE is in the [docs](docs) directory in this repo.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Cross-packaging for other Distributions
 | 
			
		||||
## Web Interface
 | 
			
		||||
 | 
			
		||||
You can create packages for different distributions  
 | 
			
		||||
setting the environment variables `LURE_DISTRO` and `LURE_PKG_FORMAT`.
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
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
 | 
			
		||||
```
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Repositories
 | 
			
		||||
 | 
			
		||||
Unlike the AUR, LURE supports using multiple repos. Also unlike the AUR, LURE's repos are a single git repo containing all the build scripts. Inside each LURE repo, there should be 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.
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Dependencies
 | 
			
		||||
 | 
			
		||||
As mentioned before, LURE has zero dependencies after it's built. All functionality that could be pure Go is pure Go. Thanks to the following packages for making this possible:
 | 
			
		||||
As mentioned before, LURE has zero dependencies after compilation. Thanks to the following projects for making this possible:
 | 
			
		||||
 | 
			
		||||
- 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
 | 
			
		||||
- 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 install script
 | 
			
		||||
- Automated docker-based testing tool
 | 
			
		||||
- Web interface for repos
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										372
									
								
								build.go
									
									
									
									
									
								
							
							
						
						
									
										372
									
								
								build.go
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -19,8 +19,10 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
@@ -28,7 +30,6 @@ 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"
 | 
			
		||||
@@ -38,9 +39,14 @@ import (
 | 
			
		||||
 | 
			
		||||
	"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/download"
 | 
			
		||||
	"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"
 | 
			
		||||
@@ -76,7 +82,7 @@ type Scripts struct {
 | 
			
		||||
	PreInstall  string `sh:"preinstall"`
 | 
			
		||||
	PostInstall string `sh:"postinstall"`
 | 
			
		||||
	PreRemove   string `sh:"preremove"`
 | 
			
		||||
	PostRemove  string `sh:"postinstall"`
 | 
			
		||||
	PostRemove  string `sh:"postremove"`
 | 
			
		||||
	PreUpgrade  string `sh:"preupgrade"`
 | 
			
		||||
	PostUpgrade string `sh:"postupgrade"`
 | 
			
		||||
	PreTrans    string `sh:"pretrans"`
 | 
			
		||||
@@ -86,29 +92,52 @@ type Scripts struct {
 | 
			
		||||
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()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, pkgNames, err := buildPackage(c.Context, script, mgr)
 | 
			
		||||
	pkgPaths, _, err := buildPackage(c.Context, script, mgr, c.Bool("clean"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error building package").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Package(s) built successfully").Any("names", pkgNames).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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]string, []string, error) {
 | 
			
		||||
// 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)
 | 
			
		||||
@@ -123,11 +152,19 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
 | 
			
		||||
	fl.Close()
 | 
			
		||||
 | 
			
		||||
	env := genBuildEnv(info)
 | 
			
		||||
	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
 | 
			
		||||
@@ -140,21 +177,44 @@ 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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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) {
 | 
			
		||||
		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,
 | 
			
		||||
		)
 | 
			
		||||
		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)
 | 
			
		||||
		}
 | 
			
		||||
@@ -162,9 +222,30 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
 | 
			
		||||
	log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send()
 | 
			
		||||
 | 
			
		||||
	baseDir := filepath.Join(cacheDir, "pkgs", vars.Name)
 | 
			
		||||
	srcdir := filepath.Join(baseDir, "src")
 | 
			
		||||
	pkgdir := filepath.Join(baseDir, "pkg")
 | 
			
		||||
	// 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 {
 | 
			
		||||
@@ -181,18 +262,46 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
		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()
 | 
			
		||||
		installPkgs(ctx, vars.BuildDepends, mgr)
 | 
			
		||||
 | 
			
		||||
		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()
 | 
			
		||||
 | 
			
		||||
		scripts, notFound := findPkgs(vars.Depends)
 | 
			
		||||
		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)
 | 
			
		||||
			pkgPaths, pkgNames, err := buildPackage(ctx, script, mgr, clean)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
			}
 | 
			
		||||
@@ -215,11 +324,36 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fn, ok := dec.GetFunc("prepare")
 | 
			
		||||
	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, srcdir)
 | 
			
		||||
		err = fn(ctx, interp.Dir(srcdir))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -229,7 +363,7 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
	if ok {
 | 
			
		||||
		log.Info("Executing build()").Send()
 | 
			
		||||
 | 
			
		||||
		err = fn(ctx, srcdir)
 | 
			
		||||
		err = fn(ctx, interp.Dir(srcdir))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -239,7 +373,7 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
	if ok {
 | 
			
		||||
		log.Info("Executing package()").Send()
 | 
			
		||||
 | 
			
		||||
		err = fn(ctx, srcdir)
 | 
			
		||||
		err = fn(ctx, interp.Dir(srcdir))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -247,6 +381,8 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
		log.Fatal("The package() function is required").Send()
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	log.Info("Building package metadata").Str("name", vars.Name).Send()
 | 
			
		||||
 | 
			
		||||
	uniq(
 | 
			
		||||
		&repoDeps,
 | 
			
		||||
		&builtDeps,
 | 
			
		||||
@@ -256,7 +392,7 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
	pkgInfo := &nfpm.Info{
 | 
			
		||||
		Name:        vars.Name,
 | 
			
		||||
		Description: vars.Description,
 | 
			
		||||
		Arch:        runtime.GOARCH,
 | 
			
		||||
		Arch:        cpu.Arch(),
 | 
			
		||||
		Version:     vars.Version,
 | 
			
		||||
		Release:     strconv.Itoa(vars.Release),
 | 
			
		||||
		Homepage:    vars.Homepage,
 | 
			
		||||
@@ -280,10 +416,6 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
		pkgInfo.Arch = "all"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pkgInfo.Arch == "arm" {
 | 
			
		||||
		pkgInfo.Arch = cpu.ARMVariant()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	contents := []*files.Content{}
 | 
			
		||||
	filepath.Walk(pkgdir, func(path string, fi os.FileInfo, err error) error {
 | 
			
		||||
		trimmed := strings.TrimPrefix(path, pkgdir)
 | 
			
		||||
@@ -317,6 +449,7 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			link = strings.TrimPrefix(link, pkgdir)
 | 
			
		||||
 | 
			
		||||
			contents = append(contents, &files.Content{
 | 
			
		||||
				Source:      link,
 | 
			
		||||
@@ -352,12 +485,7 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
 | 
			
		||||
	pkgInfo.Overridables.Contents = contents
 | 
			
		||||
 | 
			
		||||
	pkgFormat := mgr.Format()
 | 
			
		||||
	if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok {
 | 
			
		||||
		pkgFormat = format
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	packager, err := nfpm.Get(pkgFormat)
 | 
			
		||||
	packager, err := nfpm.Get(getPkgFormat(mgr))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
@@ -373,16 +501,15 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
		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(vars.BuildDepends) > 0 {
 | 
			
		||||
		var removeBuildDeps bool
 | 
			
		||||
		err = survey.AskOne(&survey.Confirm{
 | 
			
		||||
			Message: "Would you like to remove build dependencies?",
 | 
			
		||||
		}, &removeBuildDeps)
 | 
			
		||||
	if len(buildDeps) > 0 {
 | 
			
		||||
		removeBuildDeps, err := cliutils.YesNoPrompt("Would you like to remove build dependencies?", false, translator)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
@@ -393,7 +520,7 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
					AsRoot:    true,
 | 
			
		||||
					NoConfirm: true,
 | 
			
		||||
				},
 | 
			
		||||
				vars.BuildDepends...,
 | 
			
		||||
				buildDeps...,
 | 
			
		||||
			)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
@@ -406,17 +533,62 @@ func buildPackage(ctx context.Context, script string, mgr manager.Manager) ([]st
 | 
			
		||||
	return pkgPaths, pkgNames, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func genBuildEnv(info *distro.OSRelease) []string {
 | 
			
		||||
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_BUILD_ID="+info.BuildID,
 | 
			
		||||
		"DISTRO_VERSION_ID="+info.VersionID,
 | 
			
		||||
		"DISTRO_ID_LIKE="+strings.Join(info.Like, " "),
 | 
			
		||||
 | 
			
		||||
		"ARCH="+runtime.GOARCH,
 | 
			
		||||
		"ARCH="+cpu.Arch(),
 | 
			
		||||
		"NCPU="+strconv.Itoa(runtime.NumCPU()),
 | 
			
		||||
 | 
			
		||||
		"scriptdir="+scriptdir,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	return env
 | 
			
		||||
@@ -428,21 +600,22 @@ func getSources(ctx context.Context, srcdir string, bv *BuildVars) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, src := range bv.Sources {
 | 
			
		||||
		opts := download.GetOptions{
 | 
			
		||||
			SourceURL:   src,
 | 
			
		||||
		opts := dl.Options{
 | 
			
		||||
			Name:        fmt.Sprintf("%s[%d]", bv.Name, i),
 | 
			
		||||
			URL:         src,
 | 
			
		||||
			Destination: srcdir,
 | 
			
		||||
			EncloseGit:  true,
 | 
			
		||||
			Progress:    os.Stderr,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if bv.Checksums[i] != "SKIP" {
 | 
			
		||||
		if !strings.EqualFold(bv.Checksums[i], "SKIP") {
 | 
			
		||||
			checksum, err := hex.DecodeString(bv.Checksums[i])
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			opts.SHA256Sum = checksum
 | 
			
		||||
			opts.SHA256 = checksum
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := download.Get(ctx, opts)
 | 
			
		||||
		err := dl.Download(ctx, opts)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -498,61 +671,64 @@ func setScripts(vars *BuildVars, info *nfpm.Info, scriptDir string) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// archMatches checks if your system architecture matches
 | 
			
		||||
// one of the provided architectures
 | 
			
		||||
func archMatches(architectures []string) bool {
 | 
			
		||||
	arch := runtime.GOARCH
 | 
			
		||||
 | 
			
		||||
	if arch == "arm" {
 | 
			
		||||
		arch = cpu.ARMVariant()
 | 
			
		||||
	if slices.Contains(architectures, "all") {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if slices.Contains(architectures, "arm") {
 | 
			
		||||
		architectures = append(architectures, cpu.ARMVariant())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return slices.Contains(architectures, arch)
 | 
			
		||||
	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)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								cmd/lure-api-server/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								cmd/lure-api-server/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
FROM alpine:latest
 | 
			
		||||
COPY lure-api-server /usr/bin/lure-api-server
 | 
			
		||||
ENTRYPOINT lure-api-server
 | 
			
		||||
							
								
								
									
										3
									
								
								cmd/lure-api-server/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								cmd/lure-api-server/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
# lure-api-server
 | 
			
		||||
 | 
			
		||||
`lure-api-server` is the backend API server for lure-web, the web interface for LURE.
 | 
			
		||||
							
								
								
									
										205
									
								
								cmd/lure-api-server/api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								cmd/lure-api-server/api.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,205 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								cmd/lure-api-server/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								cmd/lure-api-server/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								cmd/lure-api-server/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								cmd/lure-api-server/db.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								cmd/lure-api-server/docker.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										14
									
								
								cmd/lure-api-server/docker.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
#!/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
 | 
			
		||||
							
								
								
									
										117
									
								
								cmd/lure-api-server/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								cmd/lure-api-server/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								cmd/lure-api-server/webhook.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								cmd/lure-api-server/webhook.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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"
 | 
			
		||||
	"crypto/hmac"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func handleWebhook(next http.Handler, sigCh chan<- struct{}) http.Handler {
 | 
			
		||||
	return http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
 | 
			
		||||
		if req.URL.Path == "/webhook" {
 | 
			
		||||
			if req.Method != http.MethodPost {
 | 
			
		||||
				res.WriteHeader(http.StatusMethodNotAllowed)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if req.Header.Get("X-GitHub-Event") != "push" {
 | 
			
		||||
				http.Error(res, "Only push events are accepted by this bot", http.StatusBadRequest)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err := verifySecure(req)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				http.Error(res, err.Error(), http.StatusInternalServerError)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			sigCh <- struct{}{}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		next.ServeHTTP(res, req)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func verifySecure(req *http.Request) error {
 | 
			
		||||
	sigStr := req.Header.Get("X-Hub-Signature-256")
 | 
			
		||||
	sig, err := hex.DecodeString(strings.TrimPrefix(sigStr, "sha256="))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	secretStr, ok := os.LookupEnv("LURE_API_GITHUB_SECRET")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("LURE_API_GITHUB_SECRET must be set to the secret used for setting up the github webhook\n\n")
 | 
			
		||||
	}
 | 
			
		||||
	secret := []byte(secretStr)
 | 
			
		||||
 | 
			
		||||
	h := hmac.New(sha256.New, secret)
 | 
			
		||||
	_, err = io.Copy(h, req.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !hmac.Equal(h.Sum(nil), sig) {
 | 
			
		||||
		log.Warn("Insecure webhook request").
 | 
			
		||||
			Str("from", req.RemoteAddr).
 | 
			
		||||
			Bytes("sig", sig).
 | 
			
		||||
			Bytes("hmac", h.Sum(nil)).
 | 
			
		||||
			Send()
 | 
			
		||||
 | 
			
		||||
		return errors.New("webhook signature mismatch")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func repoPullWorker(ctx context.Context, sigCh <-chan struct{}) {
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-sigCh:
 | 
			
		||||
			err := repos.Pull(ctx, gdb, cfg.Repos)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Warn("Error while pulling repositories").Err(err).Send()
 | 
			
		||||
			}
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										103
									
								
								config.go
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								config.go
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -19,107 +19,18 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/pelletier/go-toml/v2"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/types"
 | 
			
		||||
	"go.arsenm.dev/lure/manager"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	cacheDir string
 | 
			
		||||
	cfgPath  string
 | 
			
		||||
	config   Config
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	RootCmd string `toml:"rootCmd"`
 | 
			
		||||
	Repos   []Repo `toml:"repo"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Repo struct {
 | 
			
		||||
	Name string `toml:"name"`
 | 
			
		||||
	URL  string `toml:"url"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var defaultConfig = Config{
 | 
			
		||||
	RootCmd: "sudo",
 | 
			
		||||
	Repos: []Repo{
 | 
			
		||||
		{
 | 
			
		||||
			Name: "default",
 | 
			
		||||
			URL:  "https://github.com/Arsen6331/lure-repo.git",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
var cfg types.Config
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	cfg, cache, err := makeDirs()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error creating directories").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	cacheDir = cache
 | 
			
		||||
 | 
			
		||||
	cfgPath = filepath.Join(cfg, "lure.toml")
 | 
			
		||||
 | 
			
		||||
	cfgFl, err := os.Open(cfgPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error opening config file").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	defer cfgFl.Close()
 | 
			
		||||
 | 
			
		||||
	err = toml.NewDecoder(cfgFl).Decode(&config)
 | 
			
		||||
	err := config.Decode(&cfg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error decoding config file").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	manager.DefaultRootCmd = config.RootCmd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func makeDirs() (string, string, error) {
 | 
			
		||||
	cfgDir, err := os.UserConfigDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseCfgPath := filepath.Join(cfgDir, "lure")
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(baseCfgPath, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cfgPath := filepath.Join(baseCfgPath, "lure.toml")
 | 
			
		||||
 | 
			
		||||
	if _, err := os.Stat(cfgPath); err != nil {
 | 
			
		||||
		cfgFl, err := os.Create(cfgPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = toml.NewEncoder(cfgFl).Encode(&defaultConfig)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cfgFl.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cacheDir, err := os.UserCacheDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	baseCachePath := filepath.Join(cacheDir, "lure")
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(filepath.Join(baseCachePath, "repo"), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(filepath.Join(baseCachePath, "pkgs"), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return baseCfgPath, baseCachePath, nil
 | 
			
		||||
	manager.DefaultRootCmd = cfg.RootCmd
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								db.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -37,7 +37,7 @@ type OSRelease struct {
 | 
			
		||||
	PrettyName       string
 | 
			
		||||
	ID               string
 | 
			
		||||
	Like             []string
 | 
			
		||||
	BuildID          string
 | 
			
		||||
	VersionID        string
 | 
			
		||||
	ANSIColor        string
 | 
			
		||||
	HomeURL          string
 | 
			
		||||
	DocumentationURL string
 | 
			
		||||
@@ -46,8 +46,16 @@ type OSRelease struct {
 | 
			
		||||
	Logo             string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OSReleaseName returns the NAME field of the
 | 
			
		||||
var parsed *OSRelease
 | 
			
		||||
 | 
			
		||||
// OSReleaseName returns a struct parsed from the system's os-release
 | 
			
		||||
// file. It checks /etc/os-release as well as /usr/lib/os-release.
 | 
			
		||||
// The returned OSRelease struct is a singleton.
 | 
			
		||||
func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
 | 
			
		||||
	if parsed != nil {
 | 
			
		||||
		return parsed, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fl, err := os.Open("/usr/lib/os-release")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fl, err = os.Open("/etc/os-release")
 | 
			
		||||
@@ -86,7 +94,7 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
 | 
			
		||||
		Name:             runner.Vars["NAME"].Str,
 | 
			
		||||
		PrettyName:       runner.Vars["PRETTY_NAME"].Str,
 | 
			
		||||
		ID:               runner.Vars["ID"].Str,
 | 
			
		||||
		BuildID:          runner.Vars["BUILD_ID"].Str,
 | 
			
		||||
		VersionID:        runner.Vars["VERSION_ID"].Str,
 | 
			
		||||
		ANSIColor:        runner.Vars["ANSI_COLOR"].Str,
 | 
			
		||||
		HomeURL:          runner.Vars["HOME_URL"].Str,
 | 
			
		||||
		DocumentationURL: runner.Vars["DOCUMENTATION_URL"].Str,
 | 
			
		||||
@@ -99,5 +107,6 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
 | 
			
		||||
		out.Like = strings.Split(runner.Vars["ID_LIKE"].Str, " ")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parsed = out
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
# LURE Docs
 | 
			
		||||
 | 
			
		||||
- [Build Scripts](build-scripts.md)
 | 
			
		||||
- [Usage](usage.md)
 | 
			
		||||
- [Configuration](configuration.md)
 | 
			
		||||
- [Usage](usage.md)
 | 
			
		||||
- [Packages](packages)
 | 
			
		||||
    - [Build Scripts](packages/build-scripts.md)
 | 
			
		||||
    - [Package Conventions](packages/conventions.md)
 | 
			
		||||
    - [Adding Packages to LURE's repo](packages/adding-packages.md)
 | 
			
		||||
@@ -1,240 +0,0 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										5
									
								
								docs/packages/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								docs/packages/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# LURE Docs > Packages
 | 
			
		||||
 | 
			
		||||
- [Build Scripts](build-scripts.md)
 | 
			
		||||
- [Package Conventions](conventions.md)
 | 
			
		||||
- [Adding Packages to LURE's repo](adding-packages.md)
 | 
			
		||||
							
								
								
									
										23
									
								
								docs/packages/adding-packages.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								docs/packages/adding-packages.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# Adding Packages to LURE's repo
 | 
			
		||||
 | 
			
		||||
## Requirements
 | 
			
		||||
 | 
			
		||||
- `go` (1.18+)
 | 
			
		||||
- `git`
 | 
			
		||||
- `lure-analyzer`
 | 
			
		||||
    - `go install go.arsenm.dev/lure-repo-bot/cmd/lure-analyzer@latest`
 | 
			
		||||
- `shfmt`
 | 
			
		||||
    - May be available in distro repos
 | 
			
		||||
    - `go install mvdan.cc/sh/v3/cmd/shfmt@latest`
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## How to submit a package
 | 
			
		||||
 | 
			
		||||
LURE's repo is hosted on Github at https://github.com/Arsen6331/lure-repo. In it, there are multiple directories each containing a `lure.sh` file. In order to add a package to LURE's repo, simply create a PR with a [build script](./build-scripts.md) and place it in a directory with the same name as the package.
 | 
			
		||||
 | 
			
		||||
Upon submitting the PR, [lure-repo-bot](https://github.com/Arsen6331/lure-repo-bot) will pull your PR and analyze it, providing suggestions for fixes as review comments. If there are no problems, the bot will approve your changes. If there are issues, re-request review from the bot after you've finished applying the fixes and it will automatically review the PR again.
 | 
			
		||||
 | 
			
		||||
All scripts submitted to the LURE repo should be formatted with `shfmt`. If they are not properly formatted, Github Actions will add suggestions in the "Files Changed" tab of the PR.
 | 
			
		||||
 | 
			
		||||
Once your PR is merged, LURE will pull the changed repo and your package will be available for people to install.
 | 
			
		||||
							
								
								
									
										485
									
								
								docs/packages/build-scripts.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										485
									
								
								docs/packages/build-scripts.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,485 @@
 | 
			
		||||
# 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)
 | 
			
		||||
- [Functions](#functions)
 | 
			
		||||
    - [prepare](#prepare)
 | 
			
		||||
    - [version](#version-1)
 | 
			
		||||
    - [build](#build)
 | 
			
		||||
    - [package](#package)
 | 
			
		||||
- [Environment Variables](#environment-variables)
 | 
			
		||||
    - [DISTRO_NAME](#distro_name)
 | 
			
		||||
    - [DISTRO_PRETTY_NAME](#distro_pretty_name)
 | 
			
		||||
    - [DISTRO_ID](#distro_id)
 | 
			
		||||
    - [DISTRO_VERSION_ID](#distro_version_id)
 | 
			
		||||
    - [ARCH](#arch)
 | 
			
		||||
    - [NCPU](#ncpu)
 | 
			
		||||
- [Helper Commands](#helper-commands)
 | 
			
		||||
    - [install-binary](#install-binary)
 | 
			
		||||
    - [install-systemd](#install-systemd)
 | 
			
		||||
    - [install-systemd-user](#install-systemd-user)
 | 
			
		||||
    - [install-config](#install-config)
 | 
			
		||||
    - [install-license](#install-license)
 | 
			
		||||
    - [install-completion](#install-completion)
 | 
			
		||||
    - [install-manual](#install-manual)
 | 
			
		||||
    - [install-desktop](#install-desktop)
 | 
			
		||||
    - [install-library](#install-library)
 | 
			
		||||
    - [git-version](#git-version)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 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. In order to standardize license names, values should be [SPDX Identifiers](https://spdx.org/licenses/) such as `Apache-2.0`, `MIT`, and `GPL-3.0-only`. If the project uses a license that is not standardized in SPDX, use the value `Custom`. If the project has multiple nonstandard licenses, include `Custom` as many times as there are nonstandard licenses.
 | 
			
		||||
 | 
			
		||||
### 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:
 | 
			
		||||
 | 
			
		||||
- `~rev`: Specify which revision of the repo to check out.
 | 
			
		||||
- `~depth`: Specify what depth should be used when cloning the repo. Must be an integer.
 | 
			
		||||
- `~name`: Specify the name of the directory into which the git repo should be cloned.
 | 
			
		||||
- `~recursive`: If set to true, submodules will be cloned recursively. It is false by default.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```text
 | 
			
		||||
git+https://gitea.arsenm.dev/Arsen6331/itd?~rev=resource-loading&~depth=1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
```text
 | 
			
		||||
git+https://gitea.arsenm.dev/Arsen6331/lure?~rev=v0.0.1&~recursive=true
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 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
 | 
			
		||||
 | 
			
		||||
This section documents user-defined functions that can be added to build scripts. Any functions marked with `(*)` are required.
 | 
			
		||||
 | 
			
		||||
All functions are executed in the `$srcdir` directory
 | 
			
		||||
 | 
			
		||||
### version
 | 
			
		||||
 | 
			
		||||
The `version()` function updates the `version` variable. This allows for automatically deriving the version from sources. This is most useful for git packages, which usually don't need to be changed, so their `version` variable stays the same.
 | 
			
		||||
 | 
			
		||||
An example of using this for git:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
version() {
 | 
			
		||||
	cd "$srcdir/itd"
 | 
			
		||||
	printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The AUR equivalent is the [`pkgver()` function](https://wiki.archlinux.org/title/VCS_package_guidelines#The_pkgver()_function)
 | 
			
		||||
 | 
			
		||||
### prepare
 | 
			
		||||
 | 
			
		||||
The `prepare()` function 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
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Environment Variables
 | 
			
		||||
 
 | 
			
		||||
LURE exposes several values as environment variables for use in build scripts.
 | 
			
		||||
 | 
			
		||||
### DISTRO_NAME
 | 
			
		||||
 | 
			
		||||
The `DISTRO_NAME` variable is the name of the distro as defined in its `os-release` file.
 | 
			
		||||
 | 
			
		||||
For example, it's set to `Fedora Linux` in a Fedora 36 docker image
 | 
			
		||||
 | 
			
		||||
### DISTRO_PRETTY_NAME
 | 
			
		||||
 | 
			
		||||
The `DISTRO_PRETTY_NAME` variable is the "pretty" name of the distro as defined in its `os-release` file.
 | 
			
		||||
 | 
			
		||||
For example, it's set to `Fedora Linux 36 (Container Image)` in a Fedora 36 docker image
 | 
			
		||||
 | 
			
		||||
### DISTRO_ID
 | 
			
		||||
 | 
			
		||||
The `DISTRO_ID` variable is the identifier of the distro as defined in its `os-release` file. This is the same as what LURE uses for overrides.
 | 
			
		||||
 | 
			
		||||
For example, it's set to `fedora` in a Fedora 36 docker image
 | 
			
		||||
 | 
			
		||||
### DISTRO_ID_LIKE
 | 
			
		||||
 | 
			
		||||
The `DISTRO_ID_LIKE` variable contains identifiers of similar distros to the one running, separated by spaces.
 | 
			
		||||
 | 
			
		||||
For example, it's set to `opensuse suse` in an OpenSUSE Tumbleweed docker image and `rhel fedora` in a CentOS 8 docker image.
 | 
			
		||||
 | 
			
		||||
### DISTRO_VERSION_ID
 | 
			
		||||
 | 
			
		||||
The `DISTRO_VERSION_ID` variable is the version identifier of the distro as defined in its `os-release` file.
 | 
			
		||||
 | 
			
		||||
For example, it's set to `36` in a Fedora 36 docker image and `11` in a Debian Bullseye docker image
 | 
			
		||||
 | 
			
		||||
### ARCH
 | 
			
		||||
 | 
			
		||||
The `ARCH` variable is the architecture of the machine running the script. It uses the same naming convention as the values in the `architectures` array
 | 
			
		||||
 | 
			
		||||
### NCPU
 | 
			
		||||
 | 
			
		||||
The `NCPU` variable is the amount of CPUs available on the machine running the script. It will be set to `8` on a quad core machine with hyperthreading, for example.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Helper Commands
 | 
			
		||||
 | 
			
		||||
LURE provides various commands to help packagers create proper cross-distro packages. These commands should be used wherever possible instead of doing the tasks manually.
 | 
			
		||||
 | 
			
		||||
### install-binary
 | 
			
		||||
 | 
			
		||||
`install-binary` accepts 1-2 arguments. The first argument is the binary you'd like to install. The second is the filename that should be used.
 | 
			
		||||
 | 
			
		||||
If the filename argument is not provided, tha name of the input file will be used.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-binary ./itd
 | 
			
		||||
install-binary ./itd itd-2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-systemd
 | 
			
		||||
 | 
			
		||||
`install-systemd` installs regular systemd system services (see `install-systemd-user` for user services)
 | 
			
		||||
 | 
			
		||||
It accepts 1-2 arguments. The first argument is the service you'd like to install. The second is the filename that should be used.
 | 
			
		||||
 | 
			
		||||
If the filename argument is not provided, tha name of the input file will be used.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-systemd ./syncthing@.service
 | 
			
		||||
install-systemd-user ./syncthing@.service sync-thing@.service
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-systemd-user
 | 
			
		||||
 | 
			
		||||
`install-systemd-user` installs systemd user services (services like `itd` meant to be started with `--user`).
 | 
			
		||||
 | 
			
		||||
It accepts 1-2 arguments. The first argument is the service you'd like to install. The second is the filename that should be used.
 | 
			
		||||
 | 
			
		||||
If the filename argument is not provided, tha name of the input file will be used.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-systemd-user ./itd.service
 | 
			
		||||
install-systemd-user ./itd.service infinitime-daemon.service
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-config
 | 
			
		||||
 | 
			
		||||
`install-config` installs configuration files into the `/etc` directory
 | 
			
		||||
 | 
			
		||||
It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used.
 | 
			
		||||
 | 
			
		||||
If the filename argument is not provided, tha name of the input file will be used.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-config ./itd.toml
 | 
			
		||||
install-config ./itd.example.toml itd.toml
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-license
 | 
			
		||||
 | 
			
		||||
`install-license` installs a license file
 | 
			
		||||
 | 
			
		||||
It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used.
 | 
			
		||||
 | 
			
		||||
If the filename argument is not provided, tha name of the input file will be used.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-license ./LICENSE itd/LICENSE
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-completion
 | 
			
		||||
 | 
			
		||||
`install-completion` installs shell completions
 | 
			
		||||
 | 
			
		||||
It currently supports `bash`, `zsh`, and `fish`
 | 
			
		||||
 | 
			
		||||
Completions are read from stdin, so they can either be piped in or retrieved from files
 | 
			
		||||
 | 
			
		||||
Two arguments are required for this function. The first one is the name of the shell and the second is the name of the completion.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
./k9s completion fish | install-completion fish k9s
 | 
			
		||||
install-completion bash k9s <./k9s/completions/k9s.bash
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-manual
 | 
			
		||||
 | 
			
		||||
`install-manual` installs manpages. It accepts a single argument, which is the path to the manpage.
 | 
			
		||||
 | 
			
		||||
The install path will be determined based on the number at the end of the filename. If a number cannot be extracted, an error will be returned.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-manual ./man/strelaysrv.1
 | 
			
		||||
install-manual ./mdoc.7
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-desktop
 | 
			
		||||
 | 
			
		||||
`install-desktop` installs desktop files for applications. It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used.
 | 
			
		||||
 | 
			
		||||
If the filename argument is not provided, tha name of the input file will be used.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-desktop ./${name}/share/admc.desktop
 | 
			
		||||
install-desktop ./${name}/share/admc.desktop admc-app.desktop
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### install-library
 | 
			
		||||
 | 
			
		||||
`install-library` installs shared and static libraries to the correct location.
 | 
			
		||||
 | 
			
		||||
This is the most important helper as it contains logic to figure out where to install libraries based on the target distro and CPU architecture. It should almost always be used to install all libraries.
 | 
			
		||||
 | 
			
		||||
It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used.
 | 
			
		||||
 | 
			
		||||
If the filename argument is not provided, tha name of the input file will be used.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
install-library ./${name}/build/libadldap.so
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### git-version
 | 
			
		||||
 | 
			
		||||
`git-version` returns a version number based on the git revision of a repository.
 | 
			
		||||
 | 
			
		||||
If an argument is provided, it will be used as the path to the repo. Otherwise, the current directory will be used.
 | 
			
		||||
 | 
			
		||||
The version number will be the amount of revisions, a dot, and the short hash of the current revision. For example: `118.e4b8348`.
 | 
			
		||||
 | 
			
		||||
The AUR's convention includes an `r` at the beginning of the version number. This is ommitted because some distros expect the version number to start with a digit.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
git-version
 | 
			
		||||
git-version "$srcdir/itd"
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										36
									
								
								docs/packages/conventions.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								docs/packages/conventions.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
# Package Conventions
 | 
			
		||||
 | 
			
		||||
## General
 | 
			
		||||
 | 
			
		||||
Packages should have the name(s) of what they contain in their `provides` and `conflicts` arrays. That way, they can be installed by users without needing to know the full package name. For example, there are two LURE packages for ITD: `itd-bin`, and `itd-git`. Both of them have provides and conflicts arrays specifying the two commands they install: `itd`, and `itctl`. This means that if a user wants to install ITD, they simply have to type `lure in itd` and LURE will prompt them for which one they want to install.
 | 
			
		||||
 | 
			
		||||
## Binary packages
 | 
			
		||||
 | 
			
		||||
Packages that install download and install precompiled binaries should have a `-bin` suffix.
 | 
			
		||||
 | 
			
		||||
## Git packages
 | 
			
		||||
 | 
			
		||||
Packages that build and install programs from source code cloned directly from Git should have a `-git` suffix.
 | 
			
		||||
 | 
			
		||||
The versions of these packages should consist of the amount of revisions followed by the current revision, separated by a period. For example: `183.80187b0`. Note that unlike the AUR, there is no `r` at the beginning. This is because some package managers refuse to install packages whose version numbers don't start with a digit.
 | 
			
		||||
 | 
			
		||||
This version number can be obtained using the following command:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
printf "%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The `version()` function for such packages should use the LURE-provided `git-version` helper command, like so:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
version() {
 | 
			
		||||
    cd "$srcdir/$name"
 | 
			
		||||
    git-version
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This uses LURE's embedded Git implementation, which ensures that the user doesn't need Git installed on their system in order to install `-git` packages.
 | 
			
		||||
 | 
			
		||||
## Other packages
 | 
			
		||||
 | 
			
		||||
Packages that download sources for a specific version of a program should not have any suffix, even if those sources are downloaded from Git.
 | 
			
		||||
@@ -12,6 +12,8 @@
 | 
			
		||||
    - [addrepo](#addrepo)
 | 
			
		||||
    - [removerepo](#removerepo)
 | 
			
		||||
    - [refresh](#refresh)
 | 
			
		||||
    - [fix](#fix)
 | 
			
		||||
    - [version](#version)
 | 
			
		||||
- [Environment Variables](#environment-variables)
 | 
			
		||||
    - [LURE_DISTRO](#lure_distro)
 | 
			
		||||
    - [LURE_PKG_FORMAT](#lure_pkg_format)
 | 
			
		||||
@@ -23,12 +25,21 @@
 | 
			
		||||
 | 
			
		||||
### 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.
 | 
			
		||||
The install command installs a package from the LURE repos. Any packages that aren't found in LURE's repos get forwarded to the system package manager for installation.
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard.
 | 
			
		||||
 | 
			
		||||
If multiple packages are found, you will be prompted to select which you want to install.
 | 
			
		||||
 | 
			
		||||
By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build.
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
lure in itd-bin
 | 
			
		||||
lure in itd-bin # only finds itd-bin
 | 
			
		||||
lure in itd # finds itd-bin and itd-git
 | 
			
		||||
lure in it% # finds itd-bin, itd-git, and itgui-git
 | 
			
		||||
lure in -c itd-bin
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### remove
 | 
			
		||||
@@ -45,6 +56,8 @@ lure rm firefox
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build.
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
@@ -55,20 +68,36 @@ lure up
 | 
			
		||||
 | 
			
		||||
The info command displays information about a package in LURE's repos.
 | 
			
		||||
 | 
			
		||||
The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard.
 | 
			
		||||
 | 
			
		||||
If multiple packages are found, you will be prompted to select which you want to show.
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
lure info itd-bin
 | 
			
		||||
lure info itd-bin # only finds itd-bin
 | 
			
		||||
lure info itd # finds itd-bin and itd-git
 | 
			
		||||
lure info it% # finds itd-bin, itd-git, and itgui-git
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### list
 | 
			
		||||
 | 
			
		||||
The list command lists all LURE repo packages as well as their versions
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
This command accepts a single optional argument. This argument is a pattern to filter found packages against.
 | 
			
		||||
 | 
			
		||||
The pattern does not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard.
 | 
			
		||||
 | 
			
		||||
There is a `-I` or `--installed` flag that filters out any packages that are not installed on the system
 | 
			
		||||
 | 
			
		||||
Examples:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
lure ls
 | 
			
		||||
lure ls # lists all LURE packages
 | 
			
		||||
lure ls -I # lists all installed packages
 | 
			
		||||
lure ls i% # lists all packages starting with "i"
 | 
			
		||||
lure ls %d # lists all packages ending with "d"
 | 
			
		||||
lure ls -I i% # lists all installed packages that start with "i"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### build
 | 
			
		||||
@@ -111,6 +140,28 @@ Example:
 | 
			
		||||
lure ref
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### fix
 | 
			
		||||
 | 
			
		||||
The fix command attempts to fix issues with LURE by deleting and rebuilding LURE's cache
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
lure fix
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### version
 | 
			
		||||
 | 
			
		||||
The version command returns the current LURE version and exits
 | 
			
		||||
 | 
			
		||||
Example:
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
lure version
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## Environment Variables
 | 
			
		||||
 | 
			
		||||
### LURE_DISTRO
 | 
			
		||||
@@ -138,3 +189,21 @@ The `LURE_ARM_VARIANT` environment variable dictates which ARM variant to build
 | 
			
		||||
- `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
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
@@ -1,256 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 download
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
	"github.com/mholt/archiver/v4"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrChecksumMismatch = errors.New("checksums did not match")
 | 
			
		||||
 | 
			
		||||
type GetOptions struct {
 | 
			
		||||
	SourceURL   string
 | 
			
		||||
	Destination string
 | 
			
		||||
	SHA256Sum   []byte
 | 
			
		||||
	// EncloseGit determines if Get will create an enclosing
 | 
			
		||||
	// directory for git repos
 | 
			
		||||
	EncloseGit bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get downloads from a URL
 | 
			
		||||
func Get(ctx context.Context, opts GetOptions) error {
 | 
			
		||||
	dest, err := filepath.Abs(opts.Destination)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	opts.Destination = dest
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(opts.Destination, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	src, err := url.Parse(opts.SourceURL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	query := src.Query()
 | 
			
		||||
 | 
			
		||||
	tag := query.Get("~tag")
 | 
			
		||||
	query.Del("~tag")
 | 
			
		||||
 | 
			
		||||
	branch := query.Get("~branch")
 | 
			
		||||
	query.Del("~branch")
 | 
			
		||||
 | 
			
		||||
	commit := query.Get("~commit")
 | 
			
		||||
	query.Del("~commit")
 | 
			
		||||
 | 
			
		||||
	depthStr := query.Get("~depth")
 | 
			
		||||
	query.Del("~depth")
 | 
			
		||||
 | 
			
		||||
	var refName plumbing.ReferenceName
 | 
			
		||||
	if tag != "" {
 | 
			
		||||
		refName = plumbing.NewTagReferenceName(tag)
 | 
			
		||||
	} else if branch != "" {
 | 
			
		||||
		refName = plumbing.NewBranchReferenceName(branch)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(src.Scheme, "git+") {
 | 
			
		||||
		src.Scheme = strings.TrimPrefix(src.Scheme, "git+")
 | 
			
		||||
		src.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
		name := path.Base(src.Path)
 | 
			
		||||
		name = strings.TrimSuffix(name, ".git")
 | 
			
		||||
 | 
			
		||||
		dstDir := opts.Destination
 | 
			
		||||
		if opts.EncloseGit {
 | 
			
		||||
			dstDir = filepath.Join(opts.Destination, name)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		depth := 0
 | 
			
		||||
		if depthStr != "" {
 | 
			
		||||
			depth, err = strconv.Atoi(depthStr)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cloneOpts := &git.CloneOptions{
 | 
			
		||||
			URL:      src.String(),
 | 
			
		||||
			Progress: os.Stderr,
 | 
			
		||||
			Depth:    depth,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		repo, err := git.PlainCloneContext(ctx, dstDir, false, cloneOpts)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		w, err := repo.Worktree()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		checkoutOpts := &git.CheckoutOptions{}
 | 
			
		||||
		if refName != "" {
 | 
			
		||||
			checkoutOpts.Branch = refName
 | 
			
		||||
		} else if commit != "" {
 | 
			
		||||
			checkoutOpts.Hash = plumbing.NewHash(commit)
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return w.Checkout(checkoutOpts)
 | 
			
		||||
	} else {
 | 
			
		||||
		name := query.Get("~name")
 | 
			
		||||
		query.Del("~name")
 | 
			
		||||
 | 
			
		||||
		archive := query.Get("~archive")
 | 
			
		||||
		query.Del("~archive")
 | 
			
		||||
 | 
			
		||||
		src.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
		if name == "" {
 | 
			
		||||
			name = path.Base(src.Path)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, src.String(), nil)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res, err := http.DefaultClient.Do(req)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		hash := sha256.New()
 | 
			
		||||
 | 
			
		||||
		format, input, err := archiver.Identify(name, res.Body)
 | 
			
		||||
		if err == archiver.ErrNoMatch || archive == "false" {
 | 
			
		||||
			fl, err := os.Create(filepath.Join(opts.Destination, name))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			w := io.MultiWriter(hash, fl)
 | 
			
		||||
 | 
			
		||||
			_, err = io.Copy(w, input)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			res.Body.Close()
 | 
			
		||||
			fl.Close()
 | 
			
		||||
 | 
			
		||||
			if opts.SHA256Sum != nil {
 | 
			
		||||
				sum := hash.Sum(nil)
 | 
			
		||||
				if !bytes.Equal(opts.SHA256Sum, sum) {
 | 
			
		||||
					return ErrChecksumMismatch
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		} else {
 | 
			
		||||
			r := io.TeeReader(input, hash)
 | 
			
		||||
			fname := format.Name()
 | 
			
		||||
 | 
			
		||||
			switch format := format.(type) {
 | 
			
		||||
			case archiver.Extractor:
 | 
			
		||||
				err = format.Extract(ctx, r, nil, func(ctx context.Context, f archiver.File) error {
 | 
			
		||||
					fr, err := f.Open()
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
					defer fr.Close()
 | 
			
		||||
 | 
			
		||||
					path := filepath.Join(opts.Destination, f.NameInArchive)
 | 
			
		||||
 | 
			
		||||
					err = os.MkdirAll(filepath.Dir(path), 0o755)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					if f.IsDir() {
 | 
			
		||||
						err = os.Mkdir(path, 0o755)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							return err
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						outFl, err := os.Create(path)
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							return err
 | 
			
		||||
						}
 | 
			
		||||
						defer outFl.Close()
 | 
			
		||||
 | 
			
		||||
						_, err = io.Copy(outFl, fr)
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
					return nil
 | 
			
		||||
				})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			case archiver.Decompressor:
 | 
			
		||||
				rc, err := format.OpenReader(r)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				defer rc.Close()
 | 
			
		||||
 | 
			
		||||
				path := filepath.Join(opts.Destination, name)
 | 
			
		||||
				path = strings.TrimSuffix(path, fname)
 | 
			
		||||
 | 
			
		||||
				outFl, err := os.Create(path)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				_, err = io.Copy(outFl, rc)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if opts.SHA256Sum != nil {
 | 
			
		||||
				sum := hash.Sum(nil)
 | 
			
		||||
				if !bytes.Equal(opts.SHA256Sum, sum) {
 | 
			
		||||
					return ErrChecksumMismatch
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								fix.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								fix.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 (
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func fixCmd(c *cli.Context) error {
 | 
			
		||||
	gdb.Close()
 | 
			
		||||
 | 
			
		||||
	log.Info("Removing cache directory").Send()
 | 
			
		||||
 | 
			
		||||
	err := os.RemoveAll(config.CacheDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Unable to remove cache directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Rebuilding cache").Send()
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(config.CacheDir, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Unable to create new cache directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Make sure the DB is rebuilt when repos are pulled
 | 
			
		||||
	gdb, err = db.Open(config.DBPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error initializing database").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	config.DBPresent = false
 | 
			
		||||
 | 
			
		||||
	err = repos.Pull(c.Context, gdb, cfg.Repos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error pulling repos").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Done").Send()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										75
									
								
								go.mod
									
									
									
									
									
								
							@@ -2,20 +2,33 @@ module go.arsenm.dev/lure
 | 
			
		||||
 | 
			
		||||
go 1.18
 | 
			
		||||
 | 
			
		||||
replace github.com/goreleaser/nfpm/v2 => github.com/Arsen6331/nfpm/v2 v2.0.0-20220922210414-eae88e8ea4b5
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/AlecAivazis/survey/v2 v2.3.6
 | 
			
		||||
	github.com/PuerkitoBio/purell v1.2.0
 | 
			
		||||
	github.com/alecthomas/chroma/v2 v2.4.0
 | 
			
		||||
	github.com/charmbracelet/bubbles v0.14.0
 | 
			
		||||
	github.com/charmbracelet/bubbletea v0.23.1
 | 
			
		||||
	github.com/charmbracelet/lipgloss v0.6.0
 | 
			
		||||
	github.com/go-git/go-billy/v5 v5.3.1
 | 
			
		||||
	github.com/go-git/go-git/v5 v5.4.2
 | 
			
		||||
	github.com/goreleaser/nfpm/v2 v2.18.1
 | 
			
		||||
	github.com/goreleaser/nfpm/v2 v2.20.0
 | 
			
		||||
	github.com/jmoiron/sqlx v1.3.5
 | 
			
		||||
	github.com/mholt/archiver/v4 v4.0.0-alpha.7
 | 
			
		||||
	github.com/mitchellh/mapstructure v1.5.0
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.0.5
 | 
			
		||||
	github.com/urfave/cli/v2 v2.16.3
 | 
			
		||||
	go.arsenm.dev/logger v0.0.0-20220630204155-5ba23e583f0a
 | 
			
		||||
	github.com/muesli/reflow v0.3.0
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.0.6
 | 
			
		||||
	github.com/schollz/progressbar/v3 v3.13.0
 | 
			
		||||
	github.com/twitchtv/twirp v8.1.3+incompatible
 | 
			
		||||
	github.com/urfave/cli/v2 v2.23.7
 | 
			
		||||
	github.com/vmihailenco/msgpack/v5 v5.3.5
 | 
			
		||||
	go.arsenm.dev/logger v0.0.0-20230126004036-a8cbbe3b6fe6
 | 
			
		||||
	go.arsenm.dev/translate v0.0.0-20230113025904-5ad1ec0ed296
 | 
			
		||||
	golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b
 | 
			
		||||
	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f
 | 
			
		||||
	golang.org/x/sys v0.4.0
 | 
			
		||||
	golang.org/x/text v0.6.0
 | 
			
		||||
	google.golang.org/protobuf v1.27.1
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1
 | 
			
		||||
	modernc.org/sqlite v1.20.0
 | 
			
		||||
	mvdan.cc/sh/v3 v3.5.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -25,23 +38,24 @@ require (
 | 
			
		||||
	github.com/Masterminds/semver v1.5.0 // indirect
 | 
			
		||||
	github.com/Masterminds/semver/v3 v3.1.1 // indirect
 | 
			
		||||
	github.com/Masterminds/sprig v2.22.0+incompatible // indirect
 | 
			
		||||
	github.com/Microsoft/go-winio v0.5.1 // indirect
 | 
			
		||||
	github.com/Microsoft/go-winio v0.5.2 // indirect
 | 
			
		||||
	github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c // indirect
 | 
			
		||||
	github.com/acomagu/bufpipe v1.0.3 // indirect
 | 
			
		||||
	github.com/andybalholm/brotli v1.0.4 // indirect
 | 
			
		||||
	github.com/aymanbagabas/go-osc52 v1.0.3 // indirect
 | 
			
		||||
	github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
 | 
			
		||||
	github.com/cavaliergopher/cpio v1.0.1 // indirect
 | 
			
		||||
	github.com/containerd/console v1.0.3 // indirect
 | 
			
		||||
	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 | 
			
		||||
	github.com/dlclark/regexp2 v1.4.0 // indirect
 | 
			
		||||
	github.com/dsnet/compress v0.0.1 // indirect
 | 
			
		||||
	github.com/emirpasic/gods v1.12.0 // indirect
 | 
			
		||||
	github.com/frankban/quicktest v1.14.3 // indirect
 | 
			
		||||
	github.com/go-git/gcfg v1.5.0 // indirect
 | 
			
		||||
	github.com/go-git/go-billy/v5 v5.3.1 // indirect
 | 
			
		||||
	github.com/gobwas/glob v0.2.3 // indirect
 | 
			
		||||
	github.com/golang/snappy v0.0.4 // indirect
 | 
			
		||||
	github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e // indirect
 | 
			
		||||
	github.com/google/uuid v1.3.0 // indirect
 | 
			
		||||
	github.com/gookit/color v1.5.1 // indirect
 | 
			
		||||
	github.com/gookit/color v1.5.2 // indirect
 | 
			
		||||
	github.com/goreleaser/chglog v0.2.2 // indirect
 | 
			
		||||
	github.com/goreleaser/fileglob v1.3.0 // indirect
 | 
			
		||||
	github.com/huandu/xstrings v1.3.2 // indirect
 | 
			
		||||
@@ -49,29 +63,50 @@ require (
 | 
			
		||||
	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 | 
			
		||||
	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
 | 
			
		||||
	github.com/kevinburke/ssh_config v1.1.0 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.15.5 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.15.11 // indirect
 | 
			
		||||
	github.com/klauspost/pgzip v1.2.5 // indirect
 | 
			
		||||
	github.com/mattn/go-colorable v0.1.2 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.14 // indirect
 | 
			
		||||
	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 | 
			
		||||
	github.com/mattn/go-colorable v0.1.11 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.17 // indirect
 | 
			
		||||
	github.com/mattn/go-localereader v0.0.1 // indirect
 | 
			
		||||
	github.com/mattn/go-runewidth v0.0.14 // indirect
 | 
			
		||||
	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
 | 
			
		||||
	github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
 | 
			
		||||
	github.com/mitchellh/copystructure v1.2.0 // indirect
 | 
			
		||||
	github.com/mitchellh/go-homedir v1.1.0 // indirect
 | 
			
		||||
	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 | 
			
		||||
	github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
 | 
			
		||||
	github.com/muesli/cancelreader v0.2.2 // indirect
 | 
			
		||||
	github.com/muesli/termenv v0.13.0 // indirect
 | 
			
		||||
	github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
 | 
			
		||||
	github.com/pierrec/lz4/v4 v4.1.14 // indirect
 | 
			
		||||
	github.com/pierrec/lz4/v4 v4.1.15 // indirect
 | 
			
		||||
	github.com/pkg/errors v0.9.1 // indirect
 | 
			
		||||
	github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
 | 
			
		||||
	github.com/rivo/uniseg v0.4.3 // indirect
 | 
			
		||||
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 | 
			
		||||
	github.com/sergi/go-diff v1.2.0 // indirect
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.0 // indirect
 | 
			
		||||
	github.com/therootcompany/xz v1.0.1 // indirect
 | 
			
		||||
	github.com/ulikunitz/xz v0.5.10 // indirect
 | 
			
		||||
	github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
 | 
			
		||||
	github.com/xanzy/ssh-agent v0.3.1 // indirect
 | 
			
		||||
	github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
 | 
			
		||||
	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
 | 
			
		||||
	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
 | 
			
		||||
	gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
 | 
			
		||||
	golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20220812174116-3211cb980234 // indirect
 | 
			
		||||
	golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
 | 
			
		||||
	golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
 | 
			
		||||
	golang.org/x/text v0.3.7 // indirect
 | 
			
		||||
	golang.org/x/term v0.4.0 // indirect
 | 
			
		||||
	golang.org/x/tools v0.1.12 // indirect
 | 
			
		||||
	gopkg.in/warnings.v0 v0.1.2 // indirect
 | 
			
		||||
	lukechampine.com/uint128 v1.2.0 // indirect
 | 
			
		||||
	modernc.org/cc/v3 v3.40.0 // indirect
 | 
			
		||||
	modernc.org/ccgo/v3 v3.16.13 // indirect
 | 
			
		||||
	modernc.org/libc v1.21.5 // indirect
 | 
			
		||||
	modernc.org/mathutil v1.5.0 // indirect
 | 
			
		||||
	modernc.org/memory v1.4.0 // indirect
 | 
			
		||||
	modernc.org/opt v0.1.3 // indirect
 | 
			
		||||
	modernc.org/strutil v1.1.3 // indirect
 | 
			
		||||
	modernc.org/token v1.0.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										188
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								go.sum
									
									
									
									
									
								
							@@ -2,8 +2,6 @@ github.com/AlecAivazis/survey/v2 v2.3.6 h1:NvTuVHISgTHEHeBFqt6BHOe4Ny/NwGZr7w+F8
 | 
			
		||||
github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI=
 | 
			
		||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
 | 
			
		||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
 | 
			
		||||
github.com/Arsen6331/nfpm/v2 v2.0.0-20220922210414-eae88e8ea4b5 h1:SFWe7Ho60w43hXEIxCdiAXZvUyM9GF/L90jMK42s8gU=
 | 
			
		||||
github.com/Arsen6331/nfpm/v2 v2.0.0-20220922210414-eae88e8ea4b5/go.mod h1:O4K1mvEORY78CSCInptGG5MWJ19yr9xFTgWWUtY1R7o=
 | 
			
		||||
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
 | 
			
		||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 | 
			
		||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 | 
			
		||||
@@ -16,8 +14,8 @@ github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
 | 
			
		||||
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
 | 
			
		||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 | 
			
		||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
 | 
			
		||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
 | 
			
		||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
 | 
			
		||||
@@ -25,14 +23,23 @@ github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c h1:bNpaLLv2Y4
 | 
			
		||||
github.com/ProtonMail/go-crypto v0.0.0-20210512092938-c05353c2d58c/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
 | 
			
		||||
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f h1:CGq7OieOz3wyQJ1fO8S0eO9TCW1JyvLrf8fhzz1i8ko=
 | 
			
		||||
github.com/ProtonMail/gopenpgp/v2 v2.2.2 h1:u2m7xt+CZWj88qK1UUNBoXeJCFJwJCZ/Ff4ymGoxEXs=
 | 
			
		||||
github.com/PuerkitoBio/purell v1.2.0 h1:/Jdm5QfyM8zdlqT6WVZU4cfP23sot6CEHA4CS49Ezig=
 | 
			
		||||
github.com/PuerkitoBio/purell v1.2.0/go.mod h1:OhLRTaaIzhvIyofkJfB24gokC7tM42Px5UhoT32THBk=
 | 
			
		||||
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
 | 
			
		||||
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
 | 
			
		||||
github.com/alecthomas/assert/v2 v2.2.0 h1:f6L/b7KE2bfA+9O4FL3CM/xJccDEwPVYd5fALBiuwvw=
 | 
			
		||||
github.com/alecthomas/chroma/v2 v2.4.0 h1:Loe2ZjT5x3q1bcWwemqyqEi8p11/IV/ncFCeLYDpWC4=
 | 
			
		||||
github.com/alecthomas/chroma/v2 v2.4.0/go.mod h1:6kHzqF5O6FUSJzBXW7fXELjb+e+7OXW4UpoPqMO7IBQ=
 | 
			
		||||
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
 | 
			
		||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
 | 
			
		||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 | 
			
		||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
 | 
			
		||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
 | 
			
		||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 | 
			
		||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 | 
			
		||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 | 
			
		||||
github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
 | 
			
		||||
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
 | 
			
		||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
 | 
			
		||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
 | 
			
		||||
github.com/caarlos0/go-rpmutils v0.2.1-0.20211112020245-2cd62ff89b11 h1:IRrDwVlWQr6kS1U8/EtyA1+EHcc4yl8pndcqXWrEamg=
 | 
			
		||||
@@ -40,6 +47,17 @@ github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8
 | 
			
		||||
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
 | 
			
		||||
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
 | 
			
		||||
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
 | 
			
		||||
github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og=
 | 
			
		||||
github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
 | 
			
		||||
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
 | 
			
		||||
github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck=
 | 
			
		||||
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
 | 
			
		||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
 | 
			
		||||
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
 | 
			
		||||
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
 | 
			
		||||
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
 | 
			
		||||
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
 | 
			
		||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
 | 
			
		||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
 | 
			
		||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 | 
			
		||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 | 
			
		||||
@@ -48,9 +66,12 @@ github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
 | 
			
		||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
 | 
			
		||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
 | 
			
		||||
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
 | 
			
		||||
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
 | 
			
		||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
 | 
			
		||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
 | 
			
		||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
 | 
			
		||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 | 
			
		||||
@@ -67,25 +88,30 @@ github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2Su
 | 
			
		||||
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
 | 
			
		||||
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
 | 
			
		||||
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
 | 
			
		||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 | 
			
		||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 | 
			
		||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 | 
			
		||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 | 
			
		||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 | 
			
		||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
			
		||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 | 
			
		||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 | 
			
		||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
 | 
			
		||||
github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e h1:6Jn9JtfCn20uycra92LxTkq5yfBKNSFlRJPBk8/Cxhg=
 | 
			
		||||
github.com/google/rpmpack v0.0.0-20220314092521-38642b5e571e/go.mod h1:83rLnx5vhPyN/mDzBYJWtiPf+9xnSVQynTpqZWe7OnY=
 | 
			
		||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
			
		||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
 | 
			
		||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 | 
			
		||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ=
 | 
			
		||||
github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM=
 | 
			
		||||
github.com/gookit/color v1.5.2 h1:uLnfXcaFjlrDnQDT+NCBcfhrXqYTx/rcCa6xn01Y8yI=
 | 
			
		||||
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
 | 
			
		||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
 | 
			
		||||
github.com/goreleaser/chglog v0.2.2 h1:V7nf07baXtGAgGevvqgW2MM4kZ6gOr12vKNSAU3VIZ0=
 | 
			
		||||
github.com/goreleaser/chglog v0.2.2/go.mod h1:2s5JwtCOWjZa8AIneL+xdUl9SRuigCjRHNHsX30dupE=
 | 
			
		||||
github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I=
 | 
			
		||||
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
 | 
			
		||||
github.com/goreleaser/nfpm/v2 v2.20.0 h1:Q/CrX54KUMluz6+M/pjTbknFd5Dao8qXi0C6ZuFCtfY=
 | 
			
		||||
github.com/goreleaser/nfpm/v2 v2.20.0/go.mod h1:/Fh6XfwT/T+D4qtNC2iXmHSD/1UT20JkvBXyJ6nFmOY=
 | 
			
		||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 | 
			
		||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
 | 
			
		||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
 | 
			
		||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
 | 
			
		||||
@@ -96,16 +122,18 @@ github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK
 | 
			
		||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 | 
			
		||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 | 
			
		||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
 | 
			
		||||
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
 | 
			
		||||
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
 | 
			
		||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 | 
			
		||||
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
 | 
			
		||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 | 
			
		||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
 | 
			
		||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 | 
			
		||||
github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o=
 | 
			
		||||
github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 | 
			
		||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
			
		||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 | 
			
		||||
github.com/klauspost/compress v1.15.5 h1:qyCLMz2JCrKADihKOh9FxnW3houKeNsp2h5OEz0QSEA=
 | 
			
		||||
github.com/klauspost/compress v1.15.5/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 | 
			
		||||
github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c=
 | 
			
		||||
github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
 | 
			
		||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 | 
			
		||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 | 
			
		||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 | 
			
		||||
@@ -118,18 +146,37 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 | 
			
		||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 | 
			
		||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
			
		||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 | 
			
		||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
 | 
			
		||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
 | 
			
		||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 | 
			
		||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
 | 
			
		||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 | 
			
		||||
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
 | 
			
		||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
 | 
			
		||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
 | 
			
		||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
 | 
			
		||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 | 
			
		||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
 | 
			
		||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
 | 
			
		||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 | 
			
		||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
 | 
			
		||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
 | 
			
		||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 | 
			
		||||
github.com/mholt/archiver/v4 v4.0.0-alpha.7 h1:xzByj8G8tj0Oq7ZYYU4+ixL/CVb5ruWCm0EZQ1PjOkE=
 | 
			
		||||
github.com/mholt/archiver/v4 v4.0.0-alpha.7/go.mod h1:Fs8qUkO74HHaidabihzYephJH8qmGD/nCP6tE5xC9BM=
 | 
			
		||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
 | 
			
		||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
 | 
			
		||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
 | 
			
		||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 | 
			
		||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 | 
			
		||||
@@ -138,72 +185,107 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
 | 
			
		||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 | 
			
		||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 | 
			
		||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 | 
			
		||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
 | 
			
		||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
 | 
			
		||||
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 | 
			
		||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
 | 
			
		||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
 | 
			
		||||
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
 | 
			
		||||
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
 | 
			
		||||
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
 | 
			
		||||
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 | 
			
		||||
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
 | 
			
		||||
github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
 | 
			
		||||
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
 | 
			
		||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 | 
			
		||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
 | 
			
		||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
 | 
			
		||||
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
 | 
			
		||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
 | 
			
		||||
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
 | 
			
		||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 | 
			
		||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 | 
			
		||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
			
		||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
 | 
			
		||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 | 
			
		||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 | 
			
		||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 | 
			
		||||
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
 | 
			
		||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
 | 
			
		||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 | 
			
		||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
			
		||||
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
 | 
			
		||||
github.com/schollz/progressbar/v3 v3.13.0 h1:9TeeWRcjW2qd05I8Kf9knPkW4vLM/hYoa6z9ABvxje8=
 | 
			
		||||
github.com/schollz/progressbar/v3 v3.13.0/go.mod h1:ZBYnSuLAX2LU8P8UiKN/KgF2DY58AJC8yfVYLPC8Ly4=
 | 
			
		||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 | 
			
		||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 | 
			
		||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 | 
			
		||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
 | 
			
		||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
 | 
			
		||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
 | 
			
		||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 | 
			
		||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 | 
			
		||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 | 
			
		||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
 | 
			
		||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
 | 
			
		||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 | 
			
		||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 | 
			
		||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 | 
			
		||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
 | 
			
		||||
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
 | 
			
		||||
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
 | 
			
		||||
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
 | 
			
		||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
			
		||||
github.com/urfave/cli/v2 v2.16.3 h1:gHoFIwpPjoyIMbJp/VFd+/vuD0dAgFK4B6DpEMFJfQk=
 | 
			
		||||
github.com/urfave/cli/v2 v2.16.3/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI=
 | 
			
		||||
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
 | 
			
		||||
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 | 
			
		||||
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
 | 
			
		||||
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
 | 
			
		||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
 | 
			
		||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
 | 
			
		||||
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
 | 
			
		||||
github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo=
 | 
			
		||||
github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w=
 | 
			
		||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
 | 
			
		||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
 | 
			
		||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
 | 
			
		||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
 | 
			
		||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
 | 
			
		||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 | 
			
		||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
 | 
			
		||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
 | 
			
		||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
 | 
			
		||||
go.arsenm.dev/logger v0.0.0-20220630204155-5ba23e583f0a h1:9SF7Af1OnN9BlWk9yNYCZuv/uAhDMhzfZxtWJEqy6EE=
 | 
			
		||||
go.arsenm.dev/logger v0.0.0-20220630204155-5ba23e583f0a/go.mod h1:RV2qydKDdoyaRkhAq8JEGvojR8eJ6bjq5WnSIlH7gYw=
 | 
			
		||||
go.arsenm.dev/logger v0.0.0-20230126004036-a8cbbe3b6fe6 h1:p5EkGdoRX4ddMJcYQzRPTHk8oVyMv0q8agaAbPr/oak=
 | 
			
		||||
go.arsenm.dev/logger v0.0.0-20230126004036-a8cbbe3b6fe6/go.mod h1:Sg0thD0AZwMib+fD+YFOgmTWwEOqPbMjSSmWef6mEog=
 | 
			
		||||
go.arsenm.dev/translate v0.0.0-20230113025904-5ad1ec0ed296 h1:uOJuOOn/sPe4YX9MD98tCoeLQTopIk17dJt0fwCeJrk=
 | 
			
		||||
go.arsenm.dev/translate v0.0.0-20230113025904-5ad1ec0ed296/go.mod h1:+rZV+tkYEPgZyP0OWBH477vWNwxN3pcAcukcjzgQjco=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 | 
			
		||||
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b h1:SCE/18RnFsLrjydh/R/s5EVvHoZprqEQUuoxK8q2Pc4=
 | 
			
		||||
golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
 | 
			
		||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
 | 
			
		||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
 | 
			
		||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
 | 
			
		||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
			
		||||
golang.org/x/net v0.0.0-20220812174116-3211cb980234 h1:RDqmgfe7SvlMWoqC3xwQ2blLO3fcWcxMa3eBLRdRW7E=
 | 
			
		||||
golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 | 
			
		||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
 | 
			
		||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@@ -220,18 +302,30 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
 | 
			
		||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
 | 
			
		||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 | 
			
		||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
			
		||||
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
 | 
			
		||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
 | 
			
		||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
			
		||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
 | 
			
		||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
 | 
			
		||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 | 
			
		||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
 | 
			
		||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
@@ -248,5 +342,29 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
 | 
			
		||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 | 
			
		||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
 | 
			
		||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
 | 
			
		||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
 | 
			
		||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
 | 
			
		||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
 | 
			
		||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
 | 
			
		||||
modernc.org/libc v1.21.5 h1:xBkU9fnHV+hvZuPSRszN0AXDG4M7nwPLwTWwkYcvLCI=
 | 
			
		||||
modernc.org/libc v1.21.5/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
 | 
			
		||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
 | 
			
		||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
 | 
			
		||||
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
 | 
			
		||||
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
 | 
			
		||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
 | 
			
		||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
 | 
			
		||||
modernc.org/sqlite v1.20.0 h1:80zmD3BGkm8BZ5fUi/4lwJQHiO3GXgIUvZRXpoIfROY=
 | 
			
		||||
modernc.org/sqlite v1.20.0/go.mod h1:EsYz8rfOvLCiYTy5ZFsOYzoCcRMu98YYkwAcCw5YIYw=
 | 
			
		||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
 | 
			
		||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
 | 
			
		||||
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
 | 
			
		||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
 | 
			
		||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 | 
			
		||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
 | 
			
		||||
mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ=
 | 
			
		||||
mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										279
									
								
								helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,279 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/object"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrNoPipe         = errors.New("command requires data to be piped in")
 | 
			
		||||
	ErrNoDetectManNum = errors.New("manual number cannot be detected from the filename")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var helpers = shutils.ExecFuncs{
 | 
			
		||||
	"install-binary":       installHelperCmd("/usr/bin", 0o755),
 | 
			
		||||
	"install-systemd-user": installHelperCmd("/usr/lib/systemd/user", 0o644),
 | 
			
		||||
	"install-systemd":      installHelperCmd("/usr/lib/systemd/system", 0o644),
 | 
			
		||||
	"install-config":       installHelperCmd("/etc", 0o644),
 | 
			
		||||
	"install-license":      installHelperCmd("/usr/share/licenses", 0o644),
 | 
			
		||||
	"install-desktop":      installHelperCmd("/usr/share/applications", 0o644),
 | 
			
		||||
	"install-manual":       installManualCmd,
 | 
			
		||||
	"install-completion":   installCompletionCmd,
 | 
			
		||||
	"install-library":      installLibraryCmd,
 | 
			
		||||
	"git-version":          gitVersionCmd,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// rHelpers contains restricted read-only helpers that don't modify any state
 | 
			
		||||
var rHelpers = shutils.ExecFuncs{
 | 
			
		||||
	"git-version": gitVersionCmd,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installHelperCmd(prefix string, perms os.FileMode) shutils.ExecFunc {
 | 
			
		||||
	return func(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
		if len(args) < 1 {
 | 
			
		||||
			return shutils.InsufficientArgsError(cmd, 1, len(args))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		from := resolvePath(hc, args[0])
 | 
			
		||||
		to := ""
 | 
			
		||||
		if len(args) > 1 {
 | 
			
		||||
			to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, args[1])
 | 
			
		||||
		} else {
 | 
			
		||||
			to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := helperInstall(from, to, perms)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("%s: %w", cmd, err)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installManualCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	if len(args) < 1 {
 | 
			
		||||
		return shutils.InsufficientArgsError(cmd, 1, len(args))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	from := resolvePath(hc, args[0])
 | 
			
		||||
	number := filepath.Base(from)
 | 
			
		||||
	// The man page may be compressed with gzip.
 | 
			
		||||
	// If it is, the .gz extension must be removed to properly
 | 
			
		||||
	// detect the number at the end of the filename.
 | 
			
		||||
	number = strings.TrimSuffix(number, ".gz")
 | 
			
		||||
	number = strings.TrimPrefix(filepath.Ext(number), ".")
 | 
			
		||||
 | 
			
		||||
	// If number is not actually a number, return an error
 | 
			
		||||
	if _, err := strconv.Atoi(number); err != nil {
 | 
			
		||||
		return fmt.Errorf("install-manual: %w", ErrNoDetectManNum)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prefix := "/usr/share/man/man" + number
 | 
			
		||||
	to := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from))
 | 
			
		||||
 | 
			
		||||
	return helperInstall(from, to, 0o644)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installCompletionCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	// If the command's stdin is the same as the system's,
 | 
			
		||||
	// that means nothing was piped in. In this case, return an error.
 | 
			
		||||
	if hc.Stdin == os.Stdin {
 | 
			
		||||
		return fmt.Errorf("install-completion: %w", ErrNoPipe)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(args) < 2 {
 | 
			
		||||
		return shutils.InsufficientArgsError(cmd, 2, len(args))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	shell := args[0]
 | 
			
		||||
	name := args[1]
 | 
			
		||||
 | 
			
		||||
	var prefix string
 | 
			
		||||
	switch shell {
 | 
			
		||||
	case "bash":
 | 
			
		||||
		prefix = "/usr/share/bash-completion/completions"
 | 
			
		||||
	case "zsh":
 | 
			
		||||
		prefix = "/usr/share/zsh/site-functions"
 | 
			
		||||
		name = "_" + name
 | 
			
		||||
	case "fish":
 | 
			
		||||
		prefix = "/usr/share/fish/vendor_completions.d"
 | 
			
		||||
		name += ".fish"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	path := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, name)
 | 
			
		||||
 | 
			
		||||
	err := os.MkdirAll(filepath.Dir(path), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dst, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0o644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer dst.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(dst, hc.Stdin)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installLibraryCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	prefix := getLibPrefix(hc)
 | 
			
		||||
	fn := installHelperCmd(prefix, 0o755)
 | 
			
		||||
	return fn(hc, cmd, args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// See https://wiki.debian.org/Multiarch/Tuples
 | 
			
		||||
var multiarchTupleMap = map[string]string{
 | 
			
		||||
	"386":      "i386-linux-gnu",
 | 
			
		||||
	"amd64":    "x86_64-linux-gnu",
 | 
			
		||||
	"arm5":     "arm-linux-gnueabi",
 | 
			
		||||
	"arm6":     "arm-linux-gnueabihf",
 | 
			
		||||
	"arm7":     "arm-linux-gnueabihf",
 | 
			
		||||
	"arm64":    "aarch64-linux-gnu",
 | 
			
		||||
	"mips":     "mips-linux-gnu",
 | 
			
		||||
	"mipsle":   "mipsel-linux-gnu",
 | 
			
		||||
	"mips64":   "mips64-linux-gnuabi64",
 | 
			
		||||
	"mips64le": "mips64el-linux-gnuabi64",
 | 
			
		||||
	"ppc64":    "powerpc64-linux-gnu",
 | 
			
		||||
	"ppc64le":  "powerpc64le-linux-gnu",
 | 
			
		||||
	"s390x":    "s390x-linux-gnu",
 | 
			
		||||
	"riscv64":  "riscv64-linux-gnu",
 | 
			
		||||
	"loong64":  "loongarch64-linux-gnu",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// usrLibDistros is a list of distros that don't support
 | 
			
		||||
// /usr/lib64, and must use /usr/lib
 | 
			
		||||
var usrLibDistros = []string{
 | 
			
		||||
	"arch",
 | 
			
		||||
	"alpine",
 | 
			
		||||
	"void",
 | 
			
		||||
	"chimera",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Based on CMake's GNUInstallDirs
 | 
			
		||||
func getLibPrefix(hc interp.HandlerContext) string {
 | 
			
		||||
	if dir, ok := os.LookupEnv("LURE_LIB_DIR"); ok {
 | 
			
		||||
		return dir
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := "/usr/lib"
 | 
			
		||||
 | 
			
		||||
	distroID := hc.Env.Get("DISTRO_ID").Str
 | 
			
		||||
	distroLike := strings.Split(hc.Env.Get("DISTRO_ID_LIKE").Str, " ")
 | 
			
		||||
 | 
			
		||||
	for _, usrLibDistro := range usrLibDistros {
 | 
			
		||||
		if distroID == usrLibDistro || slices.Contains(distroLike, usrLibDistro) {
 | 
			
		||||
			return out
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wordSize := unsafe.Sizeof(uintptr(0))
 | 
			
		||||
	if wordSize == 8 {
 | 
			
		||||
		out = "/usr/lib64"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	architecture := hc.Env.Get("ARCH").Str
 | 
			
		||||
 | 
			
		||||
	if distroID == "debian" || slices.Contains(distroLike, "debian") {
 | 
			
		||||
		triple, ok := multiarchTupleMap[architecture]
 | 
			
		||||
		if ok {
 | 
			
		||||
			out = filepath.Join("/usr/lib", triple)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	path := hc.Dir
 | 
			
		||||
	if len(args) > 0 {
 | 
			
		||||
		path = resolvePath(hc, args[0])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := git.PlainOpen(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	revNum := 0
 | 
			
		||||
	commits, err := r.Log(&git.LogOptions{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commits.ForEach(func(*object.Commit) error {
 | 
			
		||||
		revNum++
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	HEAD, err := r.Head()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hash := HEAD.Hash().String()
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintf(hc.Stdout, "%d.%s", revNum, hash[:7])
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func helperInstall(from, to string, perms os.FileMode) error {
 | 
			
		||||
	err := os.MkdirAll(filepath.Dir(to), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	src, err := os.Open(from)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer src.Close()
 | 
			
		||||
 | 
			
		||||
	dst, err := os.OpenFile(to, os.O_TRUNC|os.O_CREATE|os.O_RDWR, perms)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer dst.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(dst, src)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func resolvePath(hc interp.HandlerContext, path string) string {
 | 
			
		||||
	if !filepath.IsAbs(path) {
 | 
			
		||||
		return filepath.Join(hc.Dir, path)
 | 
			
		||||
	}
 | 
			
		||||
	return path
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										62
									
								
								info.go
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								info.go
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -19,10 +19,17 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/cliutils"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/overrides"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
	"gopkg.in/yaml.v3"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -32,28 +39,55 @@ func infoCmd(c *cli.Context) error {
 | 
			
		||||
		log.Fatalf("Command info expected at least 1 argument, got %d", args.Len()).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := repos.Pull(c.Context, gdb, cfg.Repos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error pulling repositories").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	found, _, err := repos.FindPkgs(gdb, args.Slice())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error finding packages").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(found) == 0 {
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pkgs := cliutils.FlattenPkgs(found, "show", translator)
 | 
			
		||||
 | 
			
		||||
	var names []string
 | 
			
		||||
	all := c.Bool("all")
 | 
			
		||||
 | 
			
		||||
	if !all {
 | 
			
		||||
		info, err := distro.ParseOSRelease(c.Context)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		log.Fatal("Error parsing os-release").Err(err).Send()
 | 
			
		||||
			log.Fatal("Error parsing os-release file").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	found, err := findPkg(args.First())
 | 
			
		||||
		names, err = overrides.Resolve(
 | 
			
		||||
			info,
 | 
			
		||||
			overrides.DefaultOpts.
 | 
			
		||||
				WithLanguages([]string{config.SystemLang()}),
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
		log.Fatal("Error finding package").Err(err).Send()
 | 
			
		||||
			log.Fatal("Error resolving overrides").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)
 | 
			
		||||
	for _, pkg := range pkgs {
 | 
			
		||||
		if !all {
 | 
			
		||||
			err = yaml.NewEncoder(os.Stdout).Encode(overrides.ResolvePackage(&pkg, names))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("Error encoding script variables").Err(err).Send()
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err = yaml.NewEncoder(os.Stdout).Encode(pkg)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("Error encoding script variables").Err(err).Send()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println("---")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								install.go
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								install.go
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -20,8 +20,15 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/cliutils"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
	"go.arsenm.dev/lure/manager"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -36,32 +43,48 @@ func installCmd(c *cli.Context) error {
 | 
			
		||||
		log.Fatal("Unable to detect supported package manager on system").Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	installPkgs(c.Context, args.Slice(), mgr)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installPkgs(ctx context.Context, pkgs []string, mgr manager.Manager) {
 | 
			
		||||
	err := pullRepos(ctx)
 | 
			
		||||
	err := repos.Pull(c.Context, gdb, cfg.Repos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error pulling repositories").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scripts, notFound := findPkgs(pkgs)
 | 
			
		||||
	found, notFound, err := repos.FindPkgs(gdb, args.Slice())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error finding packages").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	installPkgs(c.Context, cliutils.FlattenPkgs(found, "install", translator), notFound, mgr, c.Bool("clean"))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// installPkgs installs non-LURE packages via the package manager, then builds and installs LURE
 | 
			
		||||
// packages
 | 
			
		||||
func installPkgs(ctx context.Context, pkgs []db.Package, notFound []string, mgr manager.Manager, clean bool) {
 | 
			
		||||
	if len(notFound) > 0 {
 | 
			
		||||
		err = mgr.Install(nil, notFound...)
 | 
			
		||||
		err := mgr.Install(nil, notFound...)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error installing native packages").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	installScripts(ctx, mgr, scripts)
 | 
			
		||||
	installScripts(ctx, mgr, getScriptPaths(pkgs), clean)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installScripts(ctx context.Context, mgr manager.Manager, scripts []string) {
 | 
			
		||||
// getScriptPaths generates a slice of script paths corresponding to the
 | 
			
		||||
// given packages
 | 
			
		||||
func getScriptPaths(pkgs []db.Package) []string {
 | 
			
		||||
	var scripts []string
 | 
			
		||||
	for _, pkg := range pkgs {
 | 
			
		||||
		scriptPath := filepath.Join(config.RepoDir, pkg.Repository, pkg.Name, "lure.sh")
 | 
			
		||||
		scripts = append(scripts, scriptPath)
 | 
			
		||||
	}
 | 
			
		||||
	return scripts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// installScripts builds and installs LURE build scripts
 | 
			
		||||
func installScripts(ctx context.Context, mgr manager.Manager, scripts []string, clean bool) {
 | 
			
		||||
	for _, script := range scripts {
 | 
			
		||||
		builtPkgs, _, err := buildPackage(ctx, script, mgr)
 | 
			
		||||
		builtPkgs, _, err := buildPackage(ctx, script, mgr, clean)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error building package").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								internal/api/gen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								internal/api/gen.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 api
 | 
			
		||||
 | 
			
		||||
//go:generate protoc --twirp_out=. lure.proto
 | 
			
		||||
//go:generate protoc --go_out=. lure.proto
 | 
			
		||||
							
								
								
									
										885
									
								
								internal/api/lure.pb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										885
									
								
								internal/api/lure.pb.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,885 @@
 | 
			
		||||
// Code generated by protoc-gen-go. DO NOT EDIT.
 | 
			
		||||
// versions:
 | 
			
		||||
// 	protoc-gen-go v1.28.1
 | 
			
		||||
// 	protoc        v3.21.9
 | 
			
		||||
// source: lure.proto
 | 
			
		||||
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 | 
			
		||||
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
 | 
			
		||||
	reflect "reflect"
 | 
			
		||||
	sync "sync"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// Verify that this generated code is sufficiently up-to-date.
 | 
			
		||||
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
 | 
			
		||||
	// Verify that runtime/protoimpl is sufficiently up-to-date.
 | 
			
		||||
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SORT_BY represents possible things to sort packages by
 | 
			
		||||
type SORT_BY int32
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	SORT_BY_UNSORTED   SORT_BY = 0
 | 
			
		||||
	SORT_BY_NAME       SORT_BY = 1
 | 
			
		||||
	SORT_BY_REPOSITORY SORT_BY = 2
 | 
			
		||||
	SORT_BY_VERSION    SORT_BY = 3
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Enum value maps for SORT_BY.
 | 
			
		||||
var (
 | 
			
		||||
	SORT_BY_name = map[int32]string{
 | 
			
		||||
		0: "UNSORTED",
 | 
			
		||||
		1: "NAME",
 | 
			
		||||
		2: "REPOSITORY",
 | 
			
		||||
		3: "VERSION",
 | 
			
		||||
	}
 | 
			
		||||
	SORT_BY_value = map[string]int32{
 | 
			
		||||
		"UNSORTED":   0,
 | 
			
		||||
		"NAME":       1,
 | 
			
		||||
		"REPOSITORY": 2,
 | 
			
		||||
		"VERSION":    3,
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (x SORT_BY) Enum() *SORT_BY {
 | 
			
		||||
	p := new(SORT_BY)
 | 
			
		||||
	*p = x
 | 
			
		||||
	return p
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x SORT_BY) String() string {
 | 
			
		||||
	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (SORT_BY) Descriptor() protoreflect.EnumDescriptor {
 | 
			
		||||
	return file_lure_proto_enumTypes[0].Descriptor()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (SORT_BY) Type() protoreflect.EnumType {
 | 
			
		||||
	return &file_lure_proto_enumTypes[0]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x SORT_BY) Number() protoreflect.EnumNumber {
 | 
			
		||||
	return protoreflect.EnumNumber(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use SORT_BY.Descriptor instead.
 | 
			
		||||
func (SORT_BY) EnumDescriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{0}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FILTER_TYPE represents possible filters for packages
 | 
			
		||||
type FILTER_TYPE int32
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	FILTER_TYPE_NO_FILTER     FILTER_TYPE = 0
 | 
			
		||||
	FILTER_TYPE_IN_REPOSITORY FILTER_TYPE = 1
 | 
			
		||||
	FILTER_TYPE_SUPPORTS_ARCH FILTER_TYPE = 2
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Enum value maps for FILTER_TYPE.
 | 
			
		||||
var (
 | 
			
		||||
	FILTER_TYPE_name = map[int32]string{
 | 
			
		||||
		0: "NO_FILTER",
 | 
			
		||||
		1: "IN_REPOSITORY",
 | 
			
		||||
		2: "SUPPORTS_ARCH",
 | 
			
		||||
	}
 | 
			
		||||
	FILTER_TYPE_value = map[string]int32{
 | 
			
		||||
		"NO_FILTER":     0,
 | 
			
		||||
		"IN_REPOSITORY": 1,
 | 
			
		||||
		"SUPPORTS_ARCH": 2,
 | 
			
		||||
	}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (x FILTER_TYPE) Enum() *FILTER_TYPE {
 | 
			
		||||
	p := new(FILTER_TYPE)
 | 
			
		||||
	*p = x
 | 
			
		||||
	return p
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x FILTER_TYPE) String() string {
 | 
			
		||||
	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (FILTER_TYPE) Descriptor() protoreflect.EnumDescriptor {
 | 
			
		||||
	return file_lure_proto_enumTypes[1].Descriptor()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (FILTER_TYPE) Type() protoreflect.EnumType {
 | 
			
		||||
	return &file_lure_proto_enumTypes[1]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x FILTER_TYPE) Number() protoreflect.EnumNumber {
 | 
			
		||||
	return protoreflect.EnumNumber(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use FILTER_TYPE.Descriptor instead.
 | 
			
		||||
func (FILTER_TYPE) EnumDescriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{1}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchRequest is a request to search for packages
 | 
			
		||||
type SearchRequest struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Query       string      `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"`
 | 
			
		||||
	Limit       int64       `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
 | 
			
		||||
	SortBy      SORT_BY     `protobuf:"varint,3,opt,name=sort_by,json=sortBy,proto3,enum=lure.SORT_BY" json:"sort_by,omitempty"`
 | 
			
		||||
	FilterType  FILTER_TYPE `protobuf:"varint,4,opt,name=filter_type,json=filterType,proto3,enum=lure.FILTER_TYPE" json:"filter_type,omitempty"`
 | 
			
		||||
	FilterValue *string     `protobuf:"bytes,5,opt,name=filter_value,json=filterValue,proto3,oneof" json:"filter_value,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) Reset() {
 | 
			
		||||
	*x = SearchRequest{}
 | 
			
		||||
	if protoimpl.UnsafeEnabled {
 | 
			
		||||
		mi := &file_lure_proto_msgTypes[0]
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		ms.StoreMessageInfo(mi)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*SearchRequest) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_lure_proto_msgTypes[0]
 | 
			
		||||
	if protoimpl.UnsafeEnabled && x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use SearchRequest.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*SearchRequest) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{0}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) GetQuery() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Query
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) GetLimit() int64 {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Limit
 | 
			
		||||
	}
 | 
			
		||||
	return 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) GetSortBy() SORT_BY {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.SortBy
 | 
			
		||||
	}
 | 
			
		||||
	return SORT_BY_UNSORTED
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) GetFilterType() FILTER_TYPE {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.FilterType
 | 
			
		||||
	}
 | 
			
		||||
	return FILTER_TYPE_NO_FILTER
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchRequest) GetFilterValue() string {
 | 
			
		||||
	if x != nil && x.FilterValue != nil {
 | 
			
		||||
		return *x.FilterValue
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StringList contains a list of strings
 | 
			
		||||
type StringList struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Entries []string `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *StringList) Reset() {
 | 
			
		||||
	*x = StringList{}
 | 
			
		||||
	if protoimpl.UnsafeEnabled {
 | 
			
		||||
		mi := &file_lure_proto_msgTypes[1]
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		ms.StoreMessageInfo(mi)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *StringList) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*StringList) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *StringList) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_lure_proto_msgTypes[1]
 | 
			
		||||
	if protoimpl.UnsafeEnabled && x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use StringList.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*StringList) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{1}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *StringList) GetEntries() []string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Entries
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Package represents a LURE package
 | 
			
		||||
type Package struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Name          string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
 | 
			
		||||
	Repository    string                 `protobuf:"bytes,2,opt,name=repository,proto3" json:"repository,omitempty"`
 | 
			
		||||
	Version       string                 `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"`
 | 
			
		||||
	Release       int64                  `protobuf:"varint,4,opt,name=release,proto3" json:"release,omitempty"`
 | 
			
		||||
	Epoch         *int64                 `protobuf:"varint,5,opt,name=epoch,proto3,oneof" json:"epoch,omitempty"`
 | 
			
		||||
	Description   *string                `protobuf:"bytes,6,opt,name=description,proto3,oneof" json:"description,omitempty"`
 | 
			
		||||
	Homepage      *string                `protobuf:"bytes,7,opt,name=homepage,proto3,oneof" json:"homepage,omitempty"`
 | 
			
		||||
	Maintainer    *string                `protobuf:"bytes,8,opt,name=maintainer,proto3,oneof" json:"maintainer,omitempty"`
 | 
			
		||||
	Architectures []string               `protobuf:"bytes,9,rep,name=architectures,proto3" json:"architectures,omitempty"`
 | 
			
		||||
	Licenses      []string               `protobuf:"bytes,10,rep,name=licenses,proto3" json:"licenses,omitempty"`
 | 
			
		||||
	Provides      []string               `protobuf:"bytes,11,rep,name=provides,proto3" json:"provides,omitempty"`
 | 
			
		||||
	Conflicts     []string               `protobuf:"bytes,12,rep,name=conflicts,proto3" json:"conflicts,omitempty"`
 | 
			
		||||
	Replaces      []string               `protobuf:"bytes,13,rep,name=replaces,proto3" json:"replaces,omitempty"`
 | 
			
		||||
	Depends       map[string]*StringList `protobuf:"bytes,14,rep,name=depends,proto3" json:"depends,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
 | 
			
		||||
	BuildDepends  map[string]*StringList `protobuf:"bytes,15,rep,name=build_depends,json=buildDepends,proto3" json:"build_depends,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) Reset() {
 | 
			
		||||
	*x = Package{}
 | 
			
		||||
	if protoimpl.UnsafeEnabled {
 | 
			
		||||
		mi := &file_lure_proto_msgTypes[2]
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		ms.StoreMessageInfo(mi)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*Package) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *Package) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_lure_proto_msgTypes[2]
 | 
			
		||||
	if protoimpl.UnsafeEnabled && x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use Package.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*Package) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{2}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetName() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Name
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetRepository() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Repository
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetVersion() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Version
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetRelease() int64 {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Release
 | 
			
		||||
	}
 | 
			
		||||
	return 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetEpoch() int64 {
 | 
			
		||||
	if x != nil && x.Epoch != nil {
 | 
			
		||||
		return *x.Epoch
 | 
			
		||||
	}
 | 
			
		||||
	return 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetDescription() string {
 | 
			
		||||
	if x != nil && x.Description != nil {
 | 
			
		||||
		return *x.Description
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetHomepage() string {
 | 
			
		||||
	if x != nil && x.Homepage != nil {
 | 
			
		||||
		return *x.Homepage
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetMaintainer() string {
 | 
			
		||||
	if x != nil && x.Maintainer != nil {
 | 
			
		||||
		return *x.Maintainer
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetArchitectures() []string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Architectures
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetLicenses() []string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Licenses
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetProvides() []string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Provides
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetConflicts() []string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Conflicts
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetReplaces() []string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Replaces
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetDepends() map[string]*StringList {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Depends
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *Package) GetBuildDepends() map[string]*StringList {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.BuildDepends
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetPackageRequest struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Name       string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
 | 
			
		||||
	Repository string `protobuf:"bytes,2,opt,name=repository,proto3" json:"repository,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetPackageRequest) Reset() {
 | 
			
		||||
	*x = GetPackageRequest{}
 | 
			
		||||
	if protoimpl.UnsafeEnabled {
 | 
			
		||||
		mi := &file_lure_proto_msgTypes[3]
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		ms.StoreMessageInfo(mi)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetPackageRequest) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*GetPackageRequest) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *GetPackageRequest) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_lure_proto_msgTypes[3]
 | 
			
		||||
	if protoimpl.UnsafeEnabled && x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use GetPackageRequest.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*GetPackageRequest) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{3}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetPackageRequest) GetName() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Name
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetPackageRequest) GetRepository() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Repository
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchResponse contains returned packages
 | 
			
		||||
type SearchResponse struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Packages []*Package `protobuf:"bytes,1,rep,name=packages,proto3" json:"packages,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchResponse) Reset() {
 | 
			
		||||
	*x = SearchResponse{}
 | 
			
		||||
	if protoimpl.UnsafeEnabled {
 | 
			
		||||
		mi := &file_lure_proto_msgTypes[4]
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		ms.StoreMessageInfo(mi)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchResponse) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*SearchResponse) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *SearchResponse) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_lure_proto_msgTypes[4]
 | 
			
		||||
	if protoimpl.UnsafeEnabled && x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use SearchResponse.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*SearchResponse) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{4}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *SearchResponse) GetPackages() []*Package {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Packages
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetBuildScriptRequest struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Name       string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
 | 
			
		||||
	Repository string `protobuf:"bytes,2,opt,name=repository,proto3" json:"repository,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptRequest) Reset() {
 | 
			
		||||
	*x = GetBuildScriptRequest{}
 | 
			
		||||
	if protoimpl.UnsafeEnabled {
 | 
			
		||||
		mi := &file_lure_proto_msgTypes[5]
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		ms.StoreMessageInfo(mi)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptRequest) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*GetBuildScriptRequest) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptRequest) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_lure_proto_msgTypes[5]
 | 
			
		||||
	if protoimpl.UnsafeEnabled && x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use GetBuildScriptRequest.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*GetBuildScriptRequest) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{5}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptRequest) GetName() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Name
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptRequest) GetRepository() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Repository
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetBuildScriptResponse struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Script string `protobuf:"bytes,1,opt,name=script,proto3" json:"script,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptResponse) Reset() {
 | 
			
		||||
	*x = GetBuildScriptResponse{}
 | 
			
		||||
	if protoimpl.UnsafeEnabled {
 | 
			
		||||
		mi := &file_lure_proto_msgTypes[6]
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		ms.StoreMessageInfo(mi)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptResponse) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*GetBuildScriptResponse) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptResponse) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_lure_proto_msgTypes[6]
 | 
			
		||||
	if protoimpl.UnsafeEnabled && x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use GetBuildScriptResponse.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*GetBuildScriptResponse) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_lure_proto_rawDescGZIP(), []int{6}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *GetBuildScriptResponse) GetScript() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Script
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var File_lure_proto protoreflect.FileDescriptor
 | 
			
		||||
 | 
			
		||||
var file_lure_proto_rawDesc = []byte{
 | 
			
		||||
	0x0a, 0x0a, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x6c, 0x75,
 | 
			
		||||
	0x72, 0x65, 0x22, 0xd0, 0x01, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71,
 | 
			
		||||
	0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x01, 0x20,
 | 
			
		||||
	0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69,
 | 
			
		||||
	0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
 | 
			
		||||
	0x12, 0x26, 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28,
 | 
			
		||||
	0x0e, 0x32, 0x0d, 0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x42, 0x59,
 | 
			
		||||
	0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x32, 0x0a, 0x0b, 0x66, 0x69, 0x6c, 0x74,
 | 
			
		||||
	0x65, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e,
 | 
			
		||||
	0x6c, 0x75, 0x72, 0x65, 0x2e, 0x46, 0x49, 0x4c, 0x54, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45,
 | 
			
		||||
	0x52, 0x0a, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, 0x0c,
 | 
			
		||||
	0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01,
 | 
			
		||||
	0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x56, 0x61, 0x6c, 0x75,
 | 
			
		||||
	0x65, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f,
 | 
			
		||||
	0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c,
 | 
			
		||||
	0x69, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01,
 | 
			
		||||
	0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xe4, 0x05,
 | 
			
		||||
	0x0a, 0x07, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
 | 
			
		||||
	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a,
 | 
			
		||||
	0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
 | 
			
		||||
	0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18, 0x0a,
 | 
			
		||||
	0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
 | 
			
		||||
	0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61,
 | 
			
		||||
	0x73, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73,
 | 
			
		||||
	0x65, 0x12, 0x19, 0x0a, 0x05, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03,
 | 
			
		||||
	0x48, 0x00, 0x52, 0x05, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0b,
 | 
			
		||||
	0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28,
 | 
			
		||||
	0x09, 0x48, 0x01, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e,
 | 
			
		||||
	0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x68, 0x6f, 0x6d, 0x65, 0x70, 0x61, 0x67, 0x65, 0x18,
 | 
			
		||||
	0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x08, 0x68, 0x6f, 0x6d, 0x65, 0x70, 0x61, 0x67,
 | 
			
		||||
	0x65, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0a, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e,
 | 
			
		||||
	0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x0a, 0x6d, 0x61, 0x69, 0x6e,
 | 
			
		||||
	0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x24, 0x0a, 0x0d, 0x61, 0x72, 0x63,
 | 
			
		||||
	0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09,
 | 
			
		||||
	0x52, 0x0d, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12,
 | 
			
		||||
	0x1a, 0x0a, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28,
 | 
			
		||||
	0x09, 0x52, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x70,
 | 
			
		||||
	0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70,
 | 
			
		||||
	0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x6c,
 | 
			
		||||
	0x69, 0x63, 0x74, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x66,
 | 
			
		||||
	0x6c, 0x69, 0x63, 0x74, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65,
 | 
			
		||||
	0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65,
 | 
			
		||||
	0x73, 0x12, 0x34, 0x0a, 0x07, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x18, 0x0e, 0x20, 0x03,
 | 
			
		||||
	0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67,
 | 
			
		||||
	0x65, 0x2e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07,
 | 
			
		||||
	0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x12, 0x44, 0x0a, 0x0d, 0x62, 0x75, 0x69, 0x6c, 0x64,
 | 
			
		||||
	0x5f, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f,
 | 
			
		||||
	0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x75,
 | 
			
		||||
	0x69, 0x6c, 0x64, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52,
 | 
			
		||||
	0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x1a, 0x4c, 0x0a,
 | 
			
		||||
	0x0c, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a,
 | 
			
		||||
	0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
 | 
			
		||||
	0x26, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10,
 | 
			
		||||
	0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74,
 | 
			
		||||
	0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x51, 0x0a, 0x11, 0x42,
 | 
			
		||||
	0x75, 0x69, 0x6c, 0x64, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
 | 
			
		||||
	0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b,
 | 
			
		||||
	0x65, 0x79, 0x12, 0x26, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
 | 
			
		||||
	0x0b, 0x32, 0x10, 0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c,
 | 
			
		||||
	0x69, 0x73, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x08,
 | 
			
		||||
	0x0a, 0x06, 0x5f, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, 0x73,
 | 
			
		||||
	0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x68, 0x6f, 0x6d,
 | 
			
		||||
	0x65, 0x70, 0x61, 0x67, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61,
 | 
			
		||||
	0x69, 0x6e, 0x65, 0x72, 0x22, 0x47, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x61, 0x63, 0x6b, 0x61,
 | 
			
		||||
	0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
 | 
			
		||||
	0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a,
 | 
			
		||||
	0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28,
 | 
			
		||||
	0x09, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x22, 0x3b, 0x0a,
 | 
			
		||||
	0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
 | 
			
		||||
	0x29, 0x0a, 0x08, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
 | 
			
		||||
	0x0b, 0x32, 0x0d, 0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65,
 | 
			
		||||
	0x52, 0x08, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x22, 0x4b, 0x0a, 0x15, 0x47, 0x65,
 | 
			
		||||
	0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75,
 | 
			
		||||
	0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
 | 
			
		||||
	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73,
 | 
			
		||||
	0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x70,
 | 
			
		||||
	0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x22, 0x30, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x42, 0x75,
 | 
			
		||||
	0x69, 0x6c, 0x64, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
 | 
			
		||||
	0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28,
 | 
			
		||||
	0x09, 0x52, 0x06, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x2a, 0x3e, 0x0a, 0x07, 0x53, 0x4f, 0x52,
 | 
			
		||||
	0x54, 0x5f, 0x42, 0x59, 0x12, 0x0c, 0x0a, 0x08, 0x55, 0x4e, 0x53, 0x4f, 0x52, 0x54, 0x45, 0x44,
 | 
			
		||||
	0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x41, 0x4d, 0x45, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a,
 | 
			
		||||
	0x52, 0x45, 0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07,
 | 
			
		||||
	0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x2a, 0x42, 0x0a, 0x0b, 0x46, 0x49, 0x4c,
 | 
			
		||||
	0x54, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x12, 0x0d, 0x0a, 0x09, 0x4e, 0x4f, 0x5f, 0x46,
 | 
			
		||||
	0x49, 0x4c, 0x54, 0x45, 0x52, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x49, 0x4e, 0x5f, 0x52, 0x45,
 | 
			
		||||
	0x50, 0x4f, 0x53, 0x49, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x55,
 | 
			
		||||
	0x50, 0x50, 0x4f, 0x52, 0x54, 0x53, 0x5f, 0x41, 0x52, 0x43, 0x48, 0x10, 0x02, 0x32, 0xb9, 0x01,
 | 
			
		||||
	0x0a, 0x03, 0x41, 0x50, 0x49, 0x12, 0x33, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12,
 | 
			
		||||
	0x13, 0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71,
 | 
			
		||||
	0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x53, 0x65, 0x61, 0x72,
 | 
			
		||||
	0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x06, 0x47, 0x65,
 | 
			
		||||
	0x74, 0x50, 0x6b, 0x67, 0x12, 0x17, 0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x50,
 | 
			
		||||
	0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0d, 0x2e,
 | 
			
		||||
	0x6c, 0x75, 0x72, 0x65, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x4b, 0x0a, 0x0e,
 | 
			
		||||
	0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x1b,
 | 
			
		||||
	0x2e, 0x6c, 0x75, 0x72, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x63,
 | 
			
		||||
	0x72, 0x69, 0x70, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x6c, 0x75,
 | 
			
		||||
	0x72, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x53, 0x63, 0x72, 0x69, 0x70,
 | 
			
		||||
	0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x08, 0x5a, 0x06, 0x2e, 0x2e, 0x2f,
 | 
			
		||||
	0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	file_lure_proto_rawDescOnce sync.Once
 | 
			
		||||
	file_lure_proto_rawDescData = file_lure_proto_rawDesc
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func file_lure_proto_rawDescGZIP() []byte {
 | 
			
		||||
	file_lure_proto_rawDescOnce.Do(func() {
 | 
			
		||||
		file_lure_proto_rawDescData = protoimpl.X.CompressGZIP(file_lure_proto_rawDescData)
 | 
			
		||||
	})
 | 
			
		||||
	return file_lure_proto_rawDescData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var file_lure_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
 | 
			
		||||
var file_lure_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
 | 
			
		||||
var file_lure_proto_goTypes = []interface{}{
 | 
			
		||||
	(SORT_BY)(0),                   // 0: lure.SORT_BY
 | 
			
		||||
	(FILTER_TYPE)(0),               // 1: lure.FILTER_TYPE
 | 
			
		||||
	(*SearchRequest)(nil),          // 2: lure.SearchRequest
 | 
			
		||||
	(*StringList)(nil),             // 3: lure.StringList
 | 
			
		||||
	(*Package)(nil),                // 4: lure.Package
 | 
			
		||||
	(*GetPackageRequest)(nil),      // 5: lure.GetPackageRequest
 | 
			
		||||
	(*SearchResponse)(nil),         // 6: lure.SearchResponse
 | 
			
		||||
	(*GetBuildScriptRequest)(nil),  // 7: lure.GetBuildScriptRequest
 | 
			
		||||
	(*GetBuildScriptResponse)(nil), // 8: lure.GetBuildScriptResponse
 | 
			
		||||
	nil,                            // 9: lure.Package.DependsEntry
 | 
			
		||||
	nil,                            // 10: lure.Package.BuildDependsEntry
 | 
			
		||||
}
 | 
			
		||||
var file_lure_proto_depIdxs = []int32{
 | 
			
		||||
	0,  // 0: lure.SearchRequest.sort_by:type_name -> lure.SORT_BY
 | 
			
		||||
	1,  // 1: lure.SearchRequest.filter_type:type_name -> lure.FILTER_TYPE
 | 
			
		||||
	9,  // 2: lure.Package.depends:type_name -> lure.Package.DependsEntry
 | 
			
		||||
	10, // 3: lure.Package.build_depends:type_name -> lure.Package.BuildDependsEntry
 | 
			
		||||
	4,  // 4: lure.SearchResponse.packages:type_name -> lure.Package
 | 
			
		||||
	3,  // 5: lure.Package.DependsEntry.value:type_name -> lure.StringList
 | 
			
		||||
	3,  // 6: lure.Package.BuildDependsEntry.value:type_name -> lure.StringList
 | 
			
		||||
	2,  // 7: lure.API.Search:input_type -> lure.SearchRequest
 | 
			
		||||
	5,  // 8: lure.API.GetPkg:input_type -> lure.GetPackageRequest
 | 
			
		||||
	7,  // 9: lure.API.GetBuildScript:input_type -> lure.GetBuildScriptRequest
 | 
			
		||||
	6,  // 10: lure.API.Search:output_type -> lure.SearchResponse
 | 
			
		||||
	4,  // 11: lure.API.GetPkg:output_type -> lure.Package
 | 
			
		||||
	8,  // 12: lure.API.GetBuildScript:output_type -> lure.GetBuildScriptResponse
 | 
			
		||||
	10, // [10:13] is the sub-list for method output_type
 | 
			
		||||
	7,  // [7:10] is the sub-list for method input_type
 | 
			
		||||
	7,  // [7:7] is the sub-list for extension type_name
 | 
			
		||||
	7,  // [7:7] is the sub-list for extension extendee
 | 
			
		||||
	0,  // [0:7] is the sub-list for field type_name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { file_lure_proto_init() }
 | 
			
		||||
func file_lure_proto_init() {
 | 
			
		||||
	if File_lure_proto != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if !protoimpl.UnsafeEnabled {
 | 
			
		||||
		file_lure_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
 | 
			
		||||
			switch v := v.(*SearchRequest); i {
 | 
			
		||||
			case 0:
 | 
			
		||||
				return &v.state
 | 
			
		||||
			case 1:
 | 
			
		||||
				return &v.sizeCache
 | 
			
		||||
			case 2:
 | 
			
		||||
				return &v.unknownFields
 | 
			
		||||
			default:
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		file_lure_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
 | 
			
		||||
			switch v := v.(*StringList); i {
 | 
			
		||||
			case 0:
 | 
			
		||||
				return &v.state
 | 
			
		||||
			case 1:
 | 
			
		||||
				return &v.sizeCache
 | 
			
		||||
			case 2:
 | 
			
		||||
				return &v.unknownFields
 | 
			
		||||
			default:
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		file_lure_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
 | 
			
		||||
			switch v := v.(*Package); i {
 | 
			
		||||
			case 0:
 | 
			
		||||
				return &v.state
 | 
			
		||||
			case 1:
 | 
			
		||||
				return &v.sizeCache
 | 
			
		||||
			case 2:
 | 
			
		||||
				return &v.unknownFields
 | 
			
		||||
			default:
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		file_lure_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
 | 
			
		||||
			switch v := v.(*GetPackageRequest); i {
 | 
			
		||||
			case 0:
 | 
			
		||||
				return &v.state
 | 
			
		||||
			case 1:
 | 
			
		||||
				return &v.sizeCache
 | 
			
		||||
			case 2:
 | 
			
		||||
				return &v.unknownFields
 | 
			
		||||
			default:
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		file_lure_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
 | 
			
		||||
			switch v := v.(*SearchResponse); i {
 | 
			
		||||
			case 0:
 | 
			
		||||
				return &v.state
 | 
			
		||||
			case 1:
 | 
			
		||||
				return &v.sizeCache
 | 
			
		||||
			case 2:
 | 
			
		||||
				return &v.unknownFields
 | 
			
		||||
			default:
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		file_lure_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
 | 
			
		||||
			switch v := v.(*GetBuildScriptRequest); i {
 | 
			
		||||
			case 0:
 | 
			
		||||
				return &v.state
 | 
			
		||||
			case 1:
 | 
			
		||||
				return &v.sizeCache
 | 
			
		||||
			case 2:
 | 
			
		||||
				return &v.unknownFields
 | 
			
		||||
			default:
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		file_lure_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
 | 
			
		||||
			switch v := v.(*GetBuildScriptResponse); i {
 | 
			
		||||
			case 0:
 | 
			
		||||
				return &v.state
 | 
			
		||||
			case 1:
 | 
			
		||||
				return &v.sizeCache
 | 
			
		||||
			case 2:
 | 
			
		||||
				return &v.unknownFields
 | 
			
		||||
			default:
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	file_lure_proto_msgTypes[0].OneofWrappers = []interface{}{}
 | 
			
		||||
	file_lure_proto_msgTypes[2].OneofWrappers = []interface{}{}
 | 
			
		||||
	type x struct{}
 | 
			
		||||
	out := protoimpl.TypeBuilder{
 | 
			
		||||
		File: protoimpl.DescBuilder{
 | 
			
		||||
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 | 
			
		||||
			RawDescriptor: file_lure_proto_rawDesc,
 | 
			
		||||
			NumEnums:      2,
 | 
			
		||||
			NumMessages:   9,
 | 
			
		||||
			NumExtensions: 0,
 | 
			
		||||
			NumServices:   1,
 | 
			
		||||
		},
 | 
			
		||||
		GoTypes:           file_lure_proto_goTypes,
 | 
			
		||||
		DependencyIndexes: file_lure_proto_depIdxs,
 | 
			
		||||
		EnumInfos:         file_lure_proto_enumTypes,
 | 
			
		||||
		MessageInfos:      file_lure_proto_msgTypes,
 | 
			
		||||
	}.Build()
 | 
			
		||||
	File_lure_proto = out.File
 | 
			
		||||
	file_lure_proto_rawDesc = nil
 | 
			
		||||
	file_lure_proto_goTypes = nil
 | 
			
		||||
	file_lure_proto_depIdxs = nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								internal/api/lure.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								internal/api/lure.proto
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
syntax = "proto3";
 | 
			
		||||
package lure;
 | 
			
		||||
 | 
			
		||||
// Slight hack to provide protoc with a package name
 | 
			
		||||
option go_package = "../api";
 | 
			
		||||
 | 
			
		||||
// SORT_BY represents possible things to sort packages by
 | 
			
		||||
enum SORT_BY {
 | 
			
		||||
    UNSORTED = 0;
 | 
			
		||||
    NAME = 1;
 | 
			
		||||
    REPOSITORY = 2;
 | 
			
		||||
    VERSION = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FILTER_TYPE represents possible filters for packages
 | 
			
		||||
enum FILTER_TYPE {
 | 
			
		||||
    NO_FILTER = 0;
 | 
			
		||||
    IN_REPOSITORY = 1;
 | 
			
		||||
    SUPPORTS_ARCH = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchRequest is a request to search for packages
 | 
			
		||||
message SearchRequest {
 | 
			
		||||
    string query = 1;
 | 
			
		||||
    int64 limit = 2;
 | 
			
		||||
    SORT_BY sort_by = 3;
 | 
			
		||||
    FILTER_TYPE filter_type = 4;
 | 
			
		||||
    optional string filter_value = 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StringList contains a list of strings
 | 
			
		||||
message StringList {
 | 
			
		||||
    repeated string entries = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Package represents a LURE package
 | 
			
		||||
message Package {
 | 
			
		||||
    string name = 1;
 | 
			
		||||
    string repository = 2;
 | 
			
		||||
    string version = 3;
 | 
			
		||||
    int64 release = 4;
 | 
			
		||||
    optional int64 epoch = 5;
 | 
			
		||||
    optional string description = 6;
 | 
			
		||||
    optional string homepage = 7;
 | 
			
		||||
    optional string maintainer = 8;
 | 
			
		||||
    repeated string architectures = 9;
 | 
			
		||||
    repeated string licenses = 10;
 | 
			
		||||
    repeated string provides = 11;
 | 
			
		||||
    repeated string conflicts = 12;
 | 
			
		||||
    repeated string replaces = 13;
 | 
			
		||||
    map<string, StringList> depends = 14;
 | 
			
		||||
    map<string, StringList> build_depends = 15;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message GetPackageRequest {
 | 
			
		||||
    string name = 1;
 | 
			
		||||
    string repository = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SearchResponse contains returned packages
 | 
			
		||||
message SearchResponse {
 | 
			
		||||
    repeated Package packages = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message GetBuildScriptRequest {
 | 
			
		||||
    string name = 1;
 | 
			
		||||
    string repository = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message GetBuildScriptResponse {
 | 
			
		||||
    string script = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Web is the LURE Web service
 | 
			
		||||
service API {
 | 
			
		||||
    // Search searches through LURE packages in the database
 | 
			
		||||
    rpc Search(SearchRequest) returns (SearchResponse);
 | 
			
		||||
    // GetPkg gets a single LURE package from the database
 | 
			
		||||
    rpc GetPkg(GetPackageRequest) returns (Package);
 | 
			
		||||
    // GetBuildScript returns the build script for the given package
 | 
			
		||||
    rpc GetBuildScript(GetBuildScriptRequest) returns (GetBuildScriptResponse);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1703
									
								
								internal/api/lure.twirp.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1703
									
								
								internal/api/lure.twirp.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										134
									
								
								internal/cliutils/prompt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								internal/cliutils/prompt.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 cliutils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/pager"
 | 
			
		||||
	"go.arsenm.dev/translate"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// YesNoPrompt asks the user a yes or no question, using def as the default answer
 | 
			
		||||
func YesNoPrompt(msg string, def bool, t translate.Translator) (bool, error) {
 | 
			
		||||
	var answer bool
 | 
			
		||||
	err := survey.AskOne(
 | 
			
		||||
		&survey.Confirm{
 | 
			
		||||
			Message: t.TranslateTo(msg, config.Language),
 | 
			
		||||
			Default: def,
 | 
			
		||||
		},
 | 
			
		||||
		&answer,
 | 
			
		||||
	)
 | 
			
		||||
	return answer, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PromptViewScript asks the user if they'd like to see a script,
 | 
			
		||||
// shows it if they answer yes, then asks if they'd still like to
 | 
			
		||||
// continue, and exits if they answer no.
 | 
			
		||||
func PromptViewScript(script, name, style string, t translate.Translator) error {
 | 
			
		||||
	view, err := YesNoPrompt(t.TranslateTo("Would you like to view the build script for", config.Language)+" "+name, false, t)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if view {
 | 
			
		||||
		err = ShowScript(script, name, style)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cont, err := YesNoPrompt("Would you still like to continue?", false, t)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !cont {
 | 
			
		||||
			log.Fatal(t.TranslateTo("User chose not to continue after reading script", config.Language)).Send()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ShowScript uses the built-in pager to display a script at a
 | 
			
		||||
// given path, in the given syntax highlighting style.
 | 
			
		||||
func ShowScript(path, name, style string) error {
 | 
			
		||||
	scriptFl, err := os.Open(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer scriptFl.Close()
 | 
			
		||||
 | 
			
		||||
	str, err := pager.SyntaxHighlightBash(scriptFl, style)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pgr := pager.New(name, str)
 | 
			
		||||
	return pgr.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FlattenPkgs attempts to flatten the a map of slices of packages into a single slice
 | 
			
		||||
// of packages by prompting the user if multiple packages match.
 | 
			
		||||
func FlattenPkgs(found map[string][]db.Package, verb string, t translate.Translator) []db.Package {
 | 
			
		||||
	var outPkgs []db.Package
 | 
			
		||||
	for _, pkgs := range found {
 | 
			
		||||
		if len(pkgs) > 1 {
 | 
			
		||||
			choices, err := PkgPrompt(pkgs, verb, t)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("Error prompting for choice of package").Send()
 | 
			
		||||
			}
 | 
			
		||||
			outPkgs = append(outPkgs, choices...)
 | 
			
		||||
		} else if len(pkgs) == 1 {
 | 
			
		||||
			outPkgs = append(outPkgs, pkgs[0])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return outPkgs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PkgPrompt asks the user to choose between multiple packages.
 | 
			
		||||
// The user may choose multiple packages.
 | 
			
		||||
func PkgPrompt(options []db.Package, verb string, t translate.Translator) ([]db.Package, error) {
 | 
			
		||||
	names := make([]string, len(options))
 | 
			
		||||
	for i, option := range options {
 | 
			
		||||
		names[i] = option.Repository + "/" + option.Name + " " + option.Version
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prompt := &survey.MultiSelect{
 | 
			
		||||
		Options: names,
 | 
			
		||||
		Message: t.TranslateTo("Choose which package(s) to "+verb, config.Language),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var choices []int
 | 
			
		||||
	err := survey.AskOne(prompt, &choices)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := make([]db.Package, len(choices))
 | 
			
		||||
	for i, choiceIndex := range choices {
 | 
			
		||||
		out[i] = options[choiceIndex]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/pelletier/go-toml/v2"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var defaultConfig = types.Config{
 | 
			
		||||
	RootCmd:          "sudo",
 | 
			
		||||
	PagerStyle:       "native",
 | 
			
		||||
	IgnorePkgUpdates: []string{},
 | 
			
		||||
	Repos: []types.Repo{
 | 
			
		||||
		{
 | 
			
		||||
			Name: "default",
 | 
			
		||||
			URL:  "https://github.com/Arsen6331/lure-repo.git",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Decode decodes the config file into the given
 | 
			
		||||
// pointer
 | 
			
		||||
func Decode(cfg *types.Config) error {
 | 
			
		||||
	cfgFl, err := os.Open(ConfigPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer cfgFl.Close()
 | 
			
		||||
 | 
			
		||||
	// Write defaults to pointer in case some values are not set in the config
 | 
			
		||||
	*cfg = defaultConfig
 | 
			
		||||
	// Set repos to nil so as to avoid a duplicate default
 | 
			
		||||
	cfg.Repos = nil
 | 
			
		||||
	return toml.NewDecoder(cfgFl).Decode(cfg)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								internal/config/dirs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								internal/config/dirs.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/pelletier/go-toml/v2"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ConfigDir  string
 | 
			
		||||
	ConfigPath string
 | 
			
		||||
	CacheDir   string
 | 
			
		||||
	RepoDir    string
 | 
			
		||||
	PkgsDir    string
 | 
			
		||||
	DBPath     string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DBPresent is true if the database
 | 
			
		||||
// was present when LURE was started
 | 
			
		||||
var DBPresent bool
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	cfgDir, err := os.UserConfigDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Unable to detect user config directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ConfigDir = filepath.Join(cfgDir, "lure")
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(ConfigDir, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Unable to create LURE config directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ConfigPath = filepath.Join(ConfigDir, "lure.toml")
 | 
			
		||||
 | 
			
		||||
	if _, err := os.Stat(ConfigPath); err != nil {
 | 
			
		||||
		cfgFl, err := os.Create(ConfigPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Unable to create LURE config file").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = toml.NewEncoder(cfgFl).Encode(&defaultConfig)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error encoding default configuration").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cfgFl.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cacheDir, err := os.UserCacheDir()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Unable to detect cache directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	CacheDir = filepath.Join(cacheDir, "lure")
 | 
			
		||||
	RepoDir = filepath.Join(CacheDir, "repo")
 | 
			
		||||
	PkgsDir = filepath.Join(CacheDir, "pkgs")
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(RepoDir, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Unable to create repo cache directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(PkgsDir, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Unable to create package cache directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	DBPath = filepath.Join(CacheDir, "db")
 | 
			
		||||
 | 
			
		||||
	fi, err := os.Stat(DBPath)
 | 
			
		||||
	DBPresent = err == nil && !fi.IsDir()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								internal/config/lang.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								internal/config/lang.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var Language language.Tag
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	lang := SystemLang()
 | 
			
		||||
	tag, err := language.Parse(lang)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error parsing system language").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	base, _ := tag.Base()
 | 
			
		||||
	Language = language.Make(base.String())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SystemLang() string {
 | 
			
		||||
	lang := os.Getenv("LANG")
 | 
			
		||||
	lang, _, _ = strings.Cut(lang, ".")
 | 
			
		||||
	if lang == "" {
 | 
			
		||||
		lang = "en"
 | 
			
		||||
	}
 | 
			
		||||
	return lang
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								internal/config/version.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/config/version.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 config
 | 
			
		||||
 | 
			
		||||
import _ "embed"
 | 
			
		||||
 | 
			
		||||
//go:embed version.txt
 | 
			
		||||
var Version string
 | 
			
		||||
@@ -1,7 +1,26 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 cpu
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/sys/cpu"
 | 
			
		||||
@@ -24,3 +43,12 @@ func ARMVariant() string {
 | 
			
		||||
		return "arm5"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Arch returns the canonical CPU architecture of the system
 | 
			
		||||
func Arch() string {
 | 
			
		||||
	arch := runtime.GOARCH
 | 
			
		||||
	if arch == "arm" {
 | 
			
		||||
		arch = ARMVariant()
 | 
			
		||||
	}
 | 
			
		||||
	return arch
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										299
									
								
								internal/db/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								internal/db/db.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,299 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 db
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"database/sql/driver"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"modernc.org/sqlite"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CurrentVersion = 1
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	sqlite.MustRegisterScalarFunction("json_array_contains", 2, JsonArrayContains)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Package is a LURE package's database representation
 | 
			
		||||
type Package struct {
 | 
			
		||||
	Name          string                    `sh:"name,required" db:"name"`
 | 
			
		||||
	Version       string                    `sh:"version,required" db:"version"`
 | 
			
		||||
	Release       int                       `sh:"release,required" db:"release"`
 | 
			
		||||
	Epoch         uint                      `sh:"epoch" db:"epoch"`
 | 
			
		||||
	Description   JSON[map[string]string]   `db:"description"`
 | 
			
		||||
	Homepage      JSON[map[string]string]   `db:"homepage"`
 | 
			
		||||
	Maintainer    JSON[map[string]string]   `db:"maintainer"`
 | 
			
		||||
	Architectures JSON[[]string]            `sh:"architectures" db:"architectures"`
 | 
			
		||||
	Licenses      JSON[[]string]            `sh:"license" db:"licenses"`
 | 
			
		||||
	Provides      JSON[[]string]            `sh:"provides" db:"provides"`
 | 
			
		||||
	Conflicts     JSON[[]string]            `sh:"conflicts" db:"conflicts"`
 | 
			
		||||
	Replaces      JSON[[]string]            `sh:"replaces" db:"replaces"`
 | 
			
		||||
	Depends       JSON[map[string][]string] `db:"depends"`
 | 
			
		||||
	BuildDepends  JSON[map[string][]string] `db:"builddepends"`
 | 
			
		||||
	Repository    string                    `db:"repository"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type version struct {
 | 
			
		||||
	Version int `db:"version"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Open(dsn string) (*sqlx.DB, error) {
 | 
			
		||||
	if dsn != ":memory:" {
 | 
			
		||||
		fi, err := os.Stat(config.DBPath)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			// TODO: This should be removed by the first stable release.
 | 
			
		||||
			if fi.IsDir() {
 | 
			
		||||
				log.Warn("Your database is using the old database engine; rebuilding").Send()
 | 
			
		||||
 | 
			
		||||
				err = os.RemoveAll(config.DBPath)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Fatal("Error removing old database").Err(err).Send()
 | 
			
		||||
				}
 | 
			
		||||
				config.DBPresent = false
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	db, err := sqlx.Open("sqlite", dsn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error opening database").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = Init(db, dsn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error initializing database").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return db, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Init initializes the database
 | 
			
		||||
func Init(db *sqlx.DB, dsn string) error {
 | 
			
		||||
	*db = *db.Unsafe()
 | 
			
		||||
	_, err := db.Exec(`
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS pkgs (
 | 
			
		||||
			name          TEXT NOT NULL,
 | 
			
		||||
			repository    TEXT NOT NULL,
 | 
			
		||||
			version       TEXT NOT NULL,
 | 
			
		||||
			release       INT  NOT NULL,
 | 
			
		||||
			epoch         INT,
 | 
			
		||||
			description   TEXT CHECK(description = 'null' OR (JSON_VALID(description) AND JSON_TYPE(description) = 'object')),
 | 
			
		||||
			homepage      TEXT CHECK(homepage = 'null' OR (JSON_VALID(homepage) AND JSON_TYPE(homepage) = 'object')),
 | 
			
		||||
			maintainer    TEXT CHECK(maintainer = 'null' OR (JSON_VALID(maintainer) AND JSON_TYPE(maintainer) = 'object')),
 | 
			
		||||
			architectures TEXT CHECK(architectures = 'null' OR (JSON_VALID(architectures) AND JSON_TYPE(architectures) = 'array')),
 | 
			
		||||
			licenses      TEXT CHECK(licenses = 'null' OR (JSON_VALID(licenses) AND JSON_TYPE(licenses) = 'array')),
 | 
			
		||||
			provides      TEXT CHECK(provides = 'null' OR (JSON_VALID(provides) AND JSON_TYPE(provides) = 'array')),
 | 
			
		||||
			conflicts     TEXT CHECK(conflicts = 'null' OR (JSON_VALID(conflicts) AND JSON_TYPE(conflicts) = 'array')),
 | 
			
		||||
			replaces      TEXT CHECK(replaces = 'null' OR (JSON_VALID(replaces) AND JSON_TYPE(replaces) = 'array')),
 | 
			
		||||
			depends       TEXT CHECK(depends = 'null' OR (JSON_VALID(depends) AND JSON_TYPE(depends) = 'object')),
 | 
			
		||||
			builddepends  TEXT CHECK(builddepends = 'null' OR (JSON_VALID(builddepends) AND JSON_TYPE(builddepends) = 'object')),
 | 
			
		||||
			UNIQUE(name, repository)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS lure_db_version (
 | 
			
		||||
			version INT NOT NULL
 | 
			
		||||
		);
 | 
			
		||||
	`)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ver, ok := GetVersion(db)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		log.Warn("Database version does not exist. Run lure fix if something isn't working.").Send()
 | 
			
		||||
		return addVersion(db, CurrentVersion)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ver != CurrentVersion {
 | 
			
		||||
		log.Warn("Database version mismatch; rebuilding").Int("version", ver).Int("expected", CurrentVersion).Send()
 | 
			
		||||
 | 
			
		||||
		db.Close()
 | 
			
		||||
		err = os.Remove(config.DBPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		config.DBPresent = false
 | 
			
		||||
 | 
			
		||||
		tdb, err := Open(dsn)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		*db = *tdb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetVersion(db *sqlx.DB) (int, bool) {
 | 
			
		||||
	var ver version
 | 
			
		||||
	err := db.Get(&ver, "SELECT * FROM lure_db_version LIMIT 1;")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, false
 | 
			
		||||
	}
 | 
			
		||||
	return ver.Version, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addVersion(db *sqlx.DB, ver int) error {
 | 
			
		||||
	_, err := db.Exec(`INSERT INTO lure_db_version(version) VALUES (?);`, ver)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InsertPackage adds a package to the database
 | 
			
		||||
func InsertPackage(db *sqlx.DB, pkg Package) error {
 | 
			
		||||
	_, err := db.NamedExec(`
 | 
			
		||||
		INSERT OR REPLACE INTO pkgs (
 | 
			
		||||
			name,
 | 
			
		||||
			repository,
 | 
			
		||||
			version,
 | 
			
		||||
			release,
 | 
			
		||||
			epoch,
 | 
			
		||||
			description,
 | 
			
		||||
			homepage,
 | 
			
		||||
			maintainer,
 | 
			
		||||
			architectures,
 | 
			
		||||
			licenses,
 | 
			
		||||
			provides,
 | 
			
		||||
			conflicts,
 | 
			
		||||
			replaces,
 | 
			
		||||
			depends,
 | 
			
		||||
			builddepends
 | 
			
		||||
		) VALUES (
 | 
			
		||||
			:name,
 | 
			
		||||
			:repository,
 | 
			
		||||
			:version,
 | 
			
		||||
			:release,
 | 
			
		||||
			:epoch,
 | 
			
		||||
			:description,
 | 
			
		||||
			:homepage,
 | 
			
		||||
			:maintainer,
 | 
			
		||||
			:architectures,
 | 
			
		||||
			:licenses,
 | 
			
		||||
			:provides,
 | 
			
		||||
			:conflicts,
 | 
			
		||||
			:replaces,
 | 
			
		||||
			:depends,
 | 
			
		||||
			:builddepends
 | 
			
		||||
		);
 | 
			
		||||
	`, pkg)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPkgs returns a result containing packages that match the where conditions
 | 
			
		||||
func GetPkgs(db *sqlx.DB, where string, args ...any) (*sqlx.Rows, error) {
 | 
			
		||||
	stream, err := db.Queryx("SELECT * FROM pkgs WHERE "+where, args...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return stream, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPkg returns a single package that match the where conditions
 | 
			
		||||
func GetPkg(db *sqlx.DB, where string, args ...any) (*Package, error) {
 | 
			
		||||
	out := &Package{}
 | 
			
		||||
	err := db.Get(out, "SELECT * FROM pkgs WHERE "+where+" LIMIT 1", args...)
 | 
			
		||||
	return out, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeletePkgs deletes all packages matching the where conditions
 | 
			
		||||
func DeletePkgs(db *sqlx.DB, where string, args ...any) error {
 | 
			
		||||
	_, err := db.Exec("DELETE FROM pkgs WHERE "+where, args...)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func JsonArrayContains(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
 | 
			
		||||
	value, ok := args[0].(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, errors.New("both arguments to json_array_contains must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	item, ok := args[1].(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, errors.New("both arguments to json_array_contains must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var array []string
 | 
			
		||||
	err := json.Unmarshal([]byte(value), &array)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return slices.Contains(array, item), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type JSON[T any] struct {
 | 
			
		||||
	Val T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewJSON[T any](v T) JSON[T] {
 | 
			
		||||
	return JSON[T]{Val: v}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *JSON[T]) Scan(val any) error {
 | 
			
		||||
	if val == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch val := val.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		err := json.Unmarshal([]byte(val), &s.Val)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	case sql.NullString:
 | 
			
		||||
		if val.Valid {
 | 
			
		||||
			err := json.Unmarshal([]byte(val.String), &s.Val)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("sqlite json types must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) Value() (driver.Value, error) {
 | 
			
		||||
	data, err := json.Marshal(s.Val)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return string(data), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) MarshalYAML() (any, error) {
 | 
			
		||||
	return s.Val, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) String() string {
 | 
			
		||||
	return fmt.Sprint(s.Val)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) GoString() string {
 | 
			
		||||
	return fmt.Sprintf("%#v", s.Val)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										255
									
								
								internal/db/db_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								internal/db/db_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,255 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 db_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var testPkg = db.Package{
 | 
			
		||||
	Name:    "test",
 | 
			
		||||
	Version: "0.0.1",
 | 
			
		||||
	Release: 1,
 | 
			
		||||
	Epoch:   2,
 | 
			
		||||
	Description: db.NewJSON(map[string]string{
 | 
			
		||||
		"en": "Test package",
 | 
			
		||||
		"ru": "Проверочный пакет",
 | 
			
		||||
	}),
 | 
			
		||||
	Homepage: db.NewJSON(map[string]string{
 | 
			
		||||
		"en": "https://lure.arsenm.dev",
 | 
			
		||||
	}),
 | 
			
		||||
	Maintainer: db.NewJSON(map[string]string{
 | 
			
		||||
		"en": "Arsen Musayelyan <arsen@arsenm.dev>",
 | 
			
		||||
		"ru": "Арсен Мусаелян <arsen@arsenm.dev>",
 | 
			
		||||
	}),
 | 
			
		||||
	Architectures: db.NewJSON([]string{"arm64", "amd64"}),
 | 
			
		||||
	Licenses:      db.NewJSON([]string{"GPL-3.0-or-later"}),
 | 
			
		||||
	Provides:      db.NewJSON([]string{"test"}),
 | 
			
		||||
	Conflicts:     db.NewJSON([]string{"test"}),
 | 
			
		||||
	Replaces:      db.NewJSON([]string{"test-old"}),
 | 
			
		||||
	Depends: db.NewJSON(map[string][]string{
 | 
			
		||||
		"": {"sudo"},
 | 
			
		||||
	}),
 | 
			
		||||
	BuildDepends: db.NewJSON(map[string][]string{
 | 
			
		||||
		"":     {"golang"},
 | 
			
		||||
		"arch": {"go"},
 | 
			
		||||
	}),
 | 
			
		||||
	Repository: "default",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInit(t *testing.T) {
 | 
			
		||||
	gdb, err := sqlx.Open("sqlite", ":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	err = db.Init(gdb, ":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = gdb.Exec("SELECT * FROM pkgs")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ver, ok := db.GetVersion(gdb)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Errorf("Expected version to be present")
 | 
			
		||||
	} else if ver != db.CurrentVersion {
 | 
			
		||||
		t.Errorf("Expected version %d, got %d", db.CurrentVersion, ver)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInsertPackage(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, testPkg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbPkg := db.Package{}
 | 
			
		||||
	err = sqlx.Get(gdb, &dbPkg, "SELECT * FROM pkgs WHERE name = 'test' AND repository = 'default'")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(testPkg, dbPkg) {
 | 
			
		||||
		t.Errorf("Expected test package to be the same as database package")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetPkgs(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := db.GetPkgs(gdb, "name LIKE 'x%'")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for result.Next() {
 | 
			
		||||
		var dbPkg db.Package
 | 
			
		||||
		err = result.StructScan(&dbPkg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !strings.HasPrefix(dbPkg.Name, "x") {
 | 
			
		||||
			t.Errorf("Expected package name to start with 'x', got %s", dbPkg.Name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetPkg(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pkg, err := db.GetPkg(gdb, "name LIKE 'x%' ORDER BY name")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pkg.Name != "x1" {
 | 
			
		||||
		t.Errorf("Expected x1 package, got %s", pkg.Name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(*pkg, x1) {
 | 
			
		||||
		t.Errorf("Expected x1 to be %v, got %v", x1, *pkg)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeletePkgs(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.DeletePkgs(gdb, "name = 'x1'")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var dbPkg db.Package
 | 
			
		||||
	err = gdb.Get(&dbPkg, "SELECT * FROM pkgs WHERE name LIKE 'x%' ORDER BY name LIMIT 1;")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if dbPkg.Name != "x2" {
 | 
			
		||||
		t.Errorf("Expected x2 package, got %s", dbPkg.Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestJsonArrayContains(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
	x2.Provides.Val = append(x2.Provides.Val, "x")
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var dbPkg db.Package
 | 
			
		||||
	err = gdb.Get(&dbPkg, "SELECT * FROM pkgs WHERE json_array_contains(provides, 'x');")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if dbPkg.Name != "x2" {
 | 
			
		||||
		t.Errorf("Expected x2 package, got %s", dbPkg.Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										325
									
								
								internal/dl/dl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								internal/dl/dl.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,325 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 dl contains abstractions for downloadingfiles and directories
 | 
			
		||||
// from various sources.
 | 
			
		||||
package dl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/PuerkitoBio/purell"
 | 
			
		||||
	"github.com/vmihailenco/msgpack/v5"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/dlcache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const manifestFileName = ".lure_cache_manifest"
 | 
			
		||||
 | 
			
		||||
// ErrChecksumMismatch occurs when the checksum of a downloaded file
 | 
			
		||||
// does not match the expected checksum provided in the Options struct.
 | 
			
		||||
var ErrChecksumMismatch = errors.New("dl: checksums did not match")
 | 
			
		||||
 | 
			
		||||
// Downloaders contains all the downloaders in the order in which
 | 
			
		||||
// they should be checked
 | 
			
		||||
var Downloaders = []Downloader{
 | 
			
		||||
	GitDownloader{},
 | 
			
		||||
	FileDownloader{},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Type represents the type of download (file or directory)
 | 
			
		||||
type Type uint8
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TypeFile Type = iota
 | 
			
		||||
	TypeDir
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (t Type) String() string {
 | 
			
		||||
	switch t {
 | 
			
		||||
	case TypeFile:
 | 
			
		||||
		return "file"
 | 
			
		||||
	case TypeDir:
 | 
			
		||||
		return "dir"
 | 
			
		||||
	}
 | 
			
		||||
	return "<unknown>"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Options contains the options for downloading
 | 
			
		||||
// files and directories
 | 
			
		||||
type Options struct {
 | 
			
		||||
	SHA256           []byte
 | 
			
		||||
	Name             string
 | 
			
		||||
	URL              string
 | 
			
		||||
	Destination      string
 | 
			
		||||
	CacheDisabled    bool
 | 
			
		||||
	PostprocDisabled bool
 | 
			
		||||
	Progress         io.Writer
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Manifest holds information about the type and name
 | 
			
		||||
// of a downloaded file or directory. It is stored inside
 | 
			
		||||
// each cache directory for later use.
 | 
			
		||||
type Manifest struct {
 | 
			
		||||
	Type Type
 | 
			
		||||
	Name string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Downloader interface {
 | 
			
		||||
	// Name returns the name of the downloader
 | 
			
		||||
	Name() string
 | 
			
		||||
	// MatchURL checks if the given URL matches
 | 
			
		||||
	// the downloader.
 | 
			
		||||
	MatchURL(string) bool
 | 
			
		||||
	// Download downloads the object at the URL
 | 
			
		||||
	// provided in the options, to the destination
 | 
			
		||||
	// given in the options. It returns a type,
 | 
			
		||||
	// a name for the downloaded object (this may be empty),
 | 
			
		||||
	// and an error.
 | 
			
		||||
	Download(Options) (Type, string, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatingDownloader extends the Downloader interface
 | 
			
		||||
// with an Update method for protocols such as git, which
 | 
			
		||||
// allow for incremental updates without changing the URL.
 | 
			
		||||
type UpdatingDownloader interface {
 | 
			
		||||
	Downloader
 | 
			
		||||
	// Update checks for and performs any
 | 
			
		||||
	// available updates for the object
 | 
			
		||||
	// described in the options. It returns
 | 
			
		||||
	// true if an update was performed, or
 | 
			
		||||
	// false if no update was required.
 | 
			
		||||
	Update(Options) (bool, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Download downloads a file or directory using the specified options.
 | 
			
		||||
// It first gets the appropriate downloader for the URL, then checks
 | 
			
		||||
// if caching is enabled. If caching is enabled, it attempts to get
 | 
			
		||||
// the cache directory for the URL and update it if necessary.
 | 
			
		||||
// If the source is found in the cache, it links it to the destination
 | 
			
		||||
// using hard links. If the source is not found in the cache,
 | 
			
		||||
// it downloads the source to a new cache directory and links it
 | 
			
		||||
// to the destination.
 | 
			
		||||
func Download(ctx context.Context, opts Options) (err error) {
 | 
			
		||||
	normalized, err := normalizeURL(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	opts.URL = normalized
 | 
			
		||||
 | 
			
		||||
	d := getDownloader(opts.URL)
 | 
			
		||||
 | 
			
		||||
	if opts.CacheDisabled {
 | 
			
		||||
		_, _, err = d.Download(opts)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var t Type
 | 
			
		||||
	cacheDir, ok := dlcache.Get(opts.URL)
 | 
			
		||||
	if ok {
 | 
			
		||||
		var updated bool
 | 
			
		||||
		if d, ok := d.(UpdatingDownloader); ok {
 | 
			
		||||
			log.Info("Source can be updated, updating if required").Str("source", opts.Name).Str("downloader", d.Name()).Send()
 | 
			
		||||
 | 
			
		||||
			updated, err = d.Update(Options{
 | 
			
		||||
				Name:        opts.Name,
 | 
			
		||||
				URL:         opts.URL,
 | 
			
		||||
				Destination: cacheDir,
 | 
			
		||||
				Progress:    opts.Progress,
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m, err := getManifest(cacheDir)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			t = m.Type
 | 
			
		||||
 | 
			
		||||
			dest := filepath.Join(opts.Destination, m.Name)
 | 
			
		||||
			ok, err := handleCache(cacheDir, dest, t)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if ok && !updated {
 | 
			
		||||
				log.Info("Source found in cache, linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
 | 
			
		||||
				return nil
 | 
			
		||||
			} else if ok {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// If we cannot read the manifest,
 | 
			
		||||
			// this cache entry is invalid and
 | 
			
		||||
			// the source must be re-downloaded.
 | 
			
		||||
			err = os.RemoveAll(cacheDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Downloading source").Str("source", opts.Name).Str("downloader", d.Name()).Send()
 | 
			
		||||
 | 
			
		||||
	cacheDir, err = dlcache.New(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t, name, err := d.Download(Options{
 | 
			
		||||
		Name:        opts.Name,
 | 
			
		||||
		URL:         opts.URL,
 | 
			
		||||
		Destination: cacheDir,
 | 
			
		||||
		Progress:    opts.Progress,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writeManifest(cacheDir, Manifest{t, name})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dest := filepath.Join(opts.Destination, name)
 | 
			
		||||
	_, err = handleCache(cacheDir, dest, t)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// writeManifest writes the manifest to the specified cache directory.
 | 
			
		||||
func writeManifest(cacheDir string, m Manifest) error {
 | 
			
		||||
	fl, err := os.Create(filepath.Join(cacheDir, manifestFileName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer fl.Close()
 | 
			
		||||
	return msgpack.NewEncoder(fl).Encode(m)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getManifest reads the manifest from the specified cache directory.
 | 
			
		||||
func getManifest(cacheDir string) (m Manifest, err error) {
 | 
			
		||||
	fl, err := os.Open(filepath.Join(cacheDir, manifestFileName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return Manifest{}, err
 | 
			
		||||
	}
 | 
			
		||||
	defer fl.Close()
 | 
			
		||||
 | 
			
		||||
	err = msgpack.NewDecoder(fl).Decode(&m)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleCache links the cache directory or a file within it to the destination
 | 
			
		||||
func handleCache(cacheDir, dest string, t Type) (bool, error) {
 | 
			
		||||
	switch t {
 | 
			
		||||
	case TypeFile:
 | 
			
		||||
		cd, err := os.Open(cacheDir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		names, err := cd.Readdirnames(2)
 | 
			
		||||
		if err == io.EOF {
 | 
			
		||||
			break
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cd.Close()
 | 
			
		||||
 | 
			
		||||
		for _, name := range names {
 | 
			
		||||
			if name == manifestFileName {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = os.Link(filepath.Join(cacheDir, names[0]), filepath.Join(dest, filepath.Base(names[0])))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return true, nil
 | 
			
		||||
	case TypeDir:
 | 
			
		||||
		err := linkDir(cacheDir, dest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// linkDir recursively walks through a directory, creating
 | 
			
		||||
// hard links for each file from the src directory to the
 | 
			
		||||
// dest directory. If it encounters a directory, it will
 | 
			
		||||
// create a directory with the same name and permissions
 | 
			
		||||
// in the dest directory, because hard links cannot be
 | 
			
		||||
// created for directories.
 | 
			
		||||
func linkDir(src, dest string) error {
 | 
			
		||||
	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if info.Name() == manifestFileName {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rel, err := filepath.Rel(src, path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newPath := filepath.Join(dest, rel)
 | 
			
		||||
		if info.IsDir() {
 | 
			
		||||
			return os.MkdirAll(newPath, info.Mode())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return os.Link(path, newPath)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDownloader(u string) Downloader {
 | 
			
		||||
	for _, d := range Downloaders {
 | 
			
		||||
		if d.MatchURL(u) {
 | 
			
		||||
			return d
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// normalizeURL normalizes a URL string, so that insignificant
 | 
			
		||||
// differences don't change the hash.
 | 
			
		||||
func normalizeURL(u string) (string, error) {
 | 
			
		||||
	const normalizationFlags = purell.FlagRemoveTrailingSlash |
 | 
			
		||||
		purell.FlagRemoveDefaultPort |
 | 
			
		||||
		purell.FlagLowercaseHost |
 | 
			
		||||
		purell.FlagLowercaseScheme |
 | 
			
		||||
		purell.FlagRemoveDuplicateSlashes |
 | 
			
		||||
		purell.FlagRemoveFragment |
 | 
			
		||||
		purell.FlagRemoveUnnecessaryHostDots |
 | 
			
		||||
		purell.FlagSortQuery |
 | 
			
		||||
		purell.FlagDecodeHexHost |
 | 
			
		||||
		purell.FlagDecodeOctalHost |
 | 
			
		||||
		purell.FlagDecodeUnnecessaryEscapes |
 | 
			
		||||
		purell.FlagRemoveEmptyPortSeparator
 | 
			
		||||
 | 
			
		||||
	return purell.NormalizeURLString(u, normalizationFlags)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										243
									
								
								internal/dl/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								internal/dl/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,243 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 dl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/mholt/archiver/v4"
 | 
			
		||||
	"github.com/schollz/progressbar/v3"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FileDownloader downloads files using HTTP
 | 
			
		||||
type FileDownloader struct{}
 | 
			
		||||
 | 
			
		||||
// Name always returns "file"
 | 
			
		||||
func (FileDownloader) Name() string {
 | 
			
		||||
	return "file"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MatchURL always returns true, as FileDownloader
 | 
			
		||||
// is used as a fallback if nothing else matches
 | 
			
		||||
func (FileDownloader) MatchURL(string) bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Download downloads a file using HTTP. If the file is
 | 
			
		||||
// compressed using a supported format, it will be extracted
 | 
			
		||||
func (FileDownloader) Download(opts Options) (Type, string, error) {
 | 
			
		||||
	u, err := url.Parse(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	query := u.Query()
 | 
			
		||||
 | 
			
		||||
	name := query.Get("~name")
 | 
			
		||||
	query.Del("~name")
 | 
			
		||||
 | 
			
		||||
	archive := query.Get("~archive")
 | 
			
		||||
	query.Del("~archive")
 | 
			
		||||
 | 
			
		||||
	u.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
	res, err := http.Get(u.String())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if name == "" {
 | 
			
		||||
		name = getFilename(res)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts.PostprocDisabled = archive == "false"
 | 
			
		||||
 | 
			
		||||
	path := filepath.Join(opts.Destination, name)
 | 
			
		||||
	fl, err := os.Create(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
	defer fl.Close()
 | 
			
		||||
 | 
			
		||||
	var bar io.WriteCloser
 | 
			
		||||
	if opts.Progress != nil {
 | 
			
		||||
		bar = progressbar.NewOptions64(
 | 
			
		||||
			res.ContentLength,
 | 
			
		||||
			progressbar.OptionSetDescription(name),
 | 
			
		||||
			progressbar.OptionSetWriter(opts.Progress),
 | 
			
		||||
			progressbar.OptionShowBytes(true),
 | 
			
		||||
			progressbar.OptionSetWidth(10),
 | 
			
		||||
			progressbar.OptionThrottle(65*time.Millisecond),
 | 
			
		||||
			progressbar.OptionShowCount(),
 | 
			
		||||
			progressbar.OptionOnCompletion(func() {
 | 
			
		||||
				_, _ = io.WriteString(opts.Progress, "\n")
 | 
			
		||||
			}),
 | 
			
		||||
			progressbar.OptionSpinnerType(14),
 | 
			
		||||
			progressbar.OptionFullWidth(),
 | 
			
		||||
			progressbar.OptionSetRenderBlankState(true),
 | 
			
		||||
		)
 | 
			
		||||
		defer bar.Close()
 | 
			
		||||
	} else {
 | 
			
		||||
		bar = shutils.NopRWC{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h := sha256.New()
 | 
			
		||||
 | 
			
		||||
	var w io.Writer
 | 
			
		||||
	if opts.SHA256 != nil {
 | 
			
		||||
		w = io.MultiWriter(fl, h, bar)
 | 
			
		||||
	} else {
 | 
			
		||||
		w = io.MultiWriter(fl, bar)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(w, res.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
	res.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if opts.SHA256 != nil {
 | 
			
		||||
		sum := h.Sum(nil)
 | 
			
		||||
		if !bytes.Equal(sum, opts.SHA256) {
 | 
			
		||||
			return 0, "", ErrChecksumMismatch
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.PostprocDisabled {
 | 
			
		||||
		return TypeFile, "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = fl.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	format, r, err := archiver.Identify(name, fl)
 | 
			
		||||
	if err == archiver.ErrNoMatch {
 | 
			
		||||
		return TypeFile, "", nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = extractFile(r, format, name, opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.Remove(path)
 | 
			
		||||
	return TypeDir, "", err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// extractFile extracts an archive or decompresses a file
 | 
			
		||||
func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) {
 | 
			
		||||
	fname := format.Name()
 | 
			
		||||
 | 
			
		||||
	switch format := format.(type) {
 | 
			
		||||
	case archiver.Extractor:
 | 
			
		||||
		err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error {
 | 
			
		||||
			fr, err := f.Open()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			defer fr.Close()
 | 
			
		||||
			fi, err := f.Stat()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			fm := fi.Mode()
 | 
			
		||||
 | 
			
		||||
			path := filepath.Join(opts.Destination, f.NameInArchive)
 | 
			
		||||
 | 
			
		||||
			err = os.MkdirAll(filepath.Dir(path), 0o755)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if f.IsDir() {
 | 
			
		||||
				err = os.Mkdir(path, 0o755)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				outFl, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fm.Perm())
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				defer outFl.Close()
 | 
			
		||||
 | 
			
		||||
				_, err = io.Copy(outFl, fr)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	case archiver.Decompressor:
 | 
			
		||||
		rc, err := format.OpenReader(r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer rc.Close()
 | 
			
		||||
 | 
			
		||||
		path := filepath.Join(opts.Destination, name)
 | 
			
		||||
		path = strings.TrimSuffix(path, fname)
 | 
			
		||||
 | 
			
		||||
		outFl, err := os.Create(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = io.Copy(outFl, rc)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var cdHeaderRgx = regexp.MustCompile(`filename="(.+)"`)
 | 
			
		||||
 | 
			
		||||
// getFilename attempts to parse the Content-Disposition
 | 
			
		||||
// HTTP response header and extract a filename. If the
 | 
			
		||||
// header does not exist, it will use the last element
 | 
			
		||||
// of the path.
 | 
			
		||||
func getFilename(res *http.Response) (name string) {
 | 
			
		||||
	cd := res.Header.Get("Content-Disposition")
 | 
			
		||||
	matches := cdHeaderRgx.FindStringSubmatch(cd)
 | 
			
		||||
	if len(matches) > 1 {
 | 
			
		||||
		name = matches[1]
 | 
			
		||||
	} else {
 | 
			
		||||
		name = path.Base(res.Request.URL.Path)
 | 
			
		||||
	}
 | 
			
		||||
	return name
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										180
									
								
								internal/dl/git.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								internal/dl/git.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 dl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GitDownloader downloads Git repositories
 | 
			
		||||
type GitDownloader struct{}
 | 
			
		||||
 | 
			
		||||
// Name always returns "git"
 | 
			
		||||
func (GitDownloader) Name() string {
 | 
			
		||||
	return "git"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MatchURL matches any URLs that start with "git+"
 | 
			
		||||
func (GitDownloader) MatchURL(u string) bool {
 | 
			
		||||
	return strings.HasPrefix(u, "git+")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Download uses git to clone the repository from the specified URL.
 | 
			
		||||
// It allows specifying the revision, depth and recursion options
 | 
			
		||||
// via query string
 | 
			
		||||
func (GitDownloader) Download(opts Options) (Type, string, error) {
 | 
			
		||||
	u, err := url.Parse(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
	u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
 | 
			
		||||
 | 
			
		||||
	query := u.Query()
 | 
			
		||||
 | 
			
		||||
	rev := query.Get("~rev")
 | 
			
		||||
	query.Del("~rev")
 | 
			
		||||
 | 
			
		||||
	name := query.Get("~name")
 | 
			
		||||
	query.Del("~name")
 | 
			
		||||
 | 
			
		||||
	depthStr := query.Get("~depth")
 | 
			
		||||
	query.Del("~depth")
 | 
			
		||||
 | 
			
		||||
	recursive := query.Get("~recursive")
 | 
			
		||||
	query.Del("~recursive")
 | 
			
		||||
 | 
			
		||||
	u.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
	depth := 0
 | 
			
		||||
	if depthStr != "" {
 | 
			
		||||
		depth, err = strconv.Atoi(depthStr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	co := &git.CloneOptions{
 | 
			
		||||
		URL:               u.String(),
 | 
			
		||||
		Depth:             depth,
 | 
			
		||||
		Progress:          opts.Progress,
 | 
			
		||||
		RecurseSubmodules: git.NoRecurseSubmodules,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if recursive == "true" {
 | 
			
		||||
		co.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := git.PlainClone(opts.Destination, false, co)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rev != "" {
 | 
			
		||||
		h, err := r.ResolveRevision(plumbing.Revision(rev))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		w, err := r.Worktree()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = w.Checkout(&git.CheckoutOptions{
 | 
			
		||||
			Hash: *h,
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if name == "" {
 | 
			
		||||
		name = strings.TrimSuffix(path.Base(u.Path), ".git")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return TypeDir, name, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update uses git to pull the repository and update it
 | 
			
		||||
// to the latest revision. It allows specifying the depth
 | 
			
		||||
// and recursion options via query string. It returns
 | 
			
		||||
// true if update was successful and false if the
 | 
			
		||||
// repository is already up-to-date
 | 
			
		||||
func (GitDownloader) Update(opts Options) (bool, error) {
 | 
			
		||||
	u, err := url.Parse(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
 | 
			
		||||
 | 
			
		||||
	query := u.Query()
 | 
			
		||||
	query.Del("~rev")
 | 
			
		||||
 | 
			
		||||
	depthStr := query.Get("~depth")
 | 
			
		||||
	query.Del("~depth")
 | 
			
		||||
 | 
			
		||||
	recursive := query.Get("~recursive")
 | 
			
		||||
	query.Del("~recursive")
 | 
			
		||||
 | 
			
		||||
	u.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
	r, err := git.PlainOpen(opts.Destination)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w, err := r.Worktree()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	depth := 0
 | 
			
		||||
	if depthStr != "" {
 | 
			
		||||
		depth, err = strconv.Atoi(depthStr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	po := &git.PullOptions{
 | 
			
		||||
		Depth:             depth,
 | 
			
		||||
		Progress:          opts.Progress,
 | 
			
		||||
		RecurseSubmodules: git.NoRecurseSubmodules,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if recursive == "true" {
 | 
			
		||||
		po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = w.Pull(po)
 | 
			
		||||
	if errors.Is(err, git.NoErrAlreadyUpToDate) {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								internal/dlcache/dlcache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								internal/dlcache/dlcache.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 dlcache
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BasePath stores the base path to the download cache
 | 
			
		||||
var BasePath = filepath.Join(config.CacheDir, "dl")
 | 
			
		||||
 | 
			
		||||
// New creates a new directory with the given ID in the cache.
 | 
			
		||||
// If a directory with the same ID already exists,
 | 
			
		||||
// it will be deleted before creating a new one.
 | 
			
		||||
func New(id string) (string, error) {
 | 
			
		||||
	h, err := hashID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	itemPath := filepath.Join(BasePath, h)
 | 
			
		||||
 | 
			
		||||
	fi, err := os.Stat(itemPath)
 | 
			
		||||
	if err == nil || (fi != nil && !fi.IsDir()) {
 | 
			
		||||
		err = os.RemoveAll(itemPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(itemPath, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return itemPath, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get checks if an entry with the given ID
 | 
			
		||||
// already exists in the cache, and if so,
 | 
			
		||||
// returns the directory and true. If it
 | 
			
		||||
// does not exist, it returns an empty string
 | 
			
		||||
// and false.
 | 
			
		||||
func Get(id string) (string, bool) {
 | 
			
		||||
	h, err := hashID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
	itemPath := filepath.Join(BasePath, h)
 | 
			
		||||
 | 
			
		||||
	_, err = os.Stat(itemPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return itemPath, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// hashID hashes the input ID with SHA1
 | 
			
		||||
// and returns the hex string of the hashed
 | 
			
		||||
// ID.
 | 
			
		||||
func hashID(id string) (string, error) {
 | 
			
		||||
	h := sha1.New()
 | 
			
		||||
	_, err := io.WriteString(h, id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return hex.EncodeToString(h.Sum(nil)), nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										74
									
								
								internal/dlcache/dlcache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								internal/dlcache/dlcache_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,74 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 dlcache_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/internal/dlcache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	dir, err := os.MkdirTemp("/tmp", "lure-dlcache-test.*")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	dlcache.BasePath = dir
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNew(t *testing.T) {
 | 
			
		||||
	const id = "https://example.com"
 | 
			
		||||
	dir, err := dlcache.New(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	exp := filepath.Join(dlcache.BasePath, sha1sum(id))
 | 
			
		||||
	if dir != exp {
 | 
			
		||||
		t.Errorf("Expected %s, got %s", exp, dir)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fi, err := os.Stat(dir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("stat: expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !fi.IsDir() {
 | 
			
		||||
		t.Errorf("Expected cache item to be a directory")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dir2, ok := dlcache.Get(id)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Errorf("Expected Get() to return valid value")
 | 
			
		||||
	}
 | 
			
		||||
	if dir2 != dir {
 | 
			
		||||
		t.Errorf("Expected %s from Get(), got %s", dir, dir2)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sha1sum(id string) string {
 | 
			
		||||
	h := sha1.New()
 | 
			
		||||
	_, _ = io.WriteString(h, id)
 | 
			
		||||
	return hex.EncodeToString(h.Sum(nil))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										235
									
								
								internal/overrides/overrides.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								internal/overrides/overrides.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,235 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 overrides
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/cpu"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Opts struct {
 | 
			
		||||
	Name         string
 | 
			
		||||
	Overrides    bool
 | 
			
		||||
	LikeDistros  bool
 | 
			
		||||
	Languages    []string
 | 
			
		||||
	LanguageTags []language.Tag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var DefaultOpts = &Opts{
 | 
			
		||||
	Overrides:   true,
 | 
			
		||||
	LikeDistros: true,
 | 
			
		||||
	Languages:   []string{"en"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Resolve generates a slice of possible override names in the order that they should be checked
 | 
			
		||||
func Resolve(info *distro.OSRelease, opts *Opts) ([]string, error) {
 | 
			
		||||
	if opts == nil {
 | 
			
		||||
		opts = DefaultOpts
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !opts.Overrides {
 | 
			
		||||
		return []string{opts.Name}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	langs, err := parseLangs(opts.Languages, opts.LanguageTags)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	architectures := []string{runtime.GOARCH}
 | 
			
		||||
 | 
			
		||||
	if runtime.GOARCH == "arm" {
 | 
			
		||||
		// More specific goes first
 | 
			
		||||
		architectures[0] = cpu.ARMVariant()
 | 
			
		||||
		architectures = append(architectures, "arm")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	distros := []string{info.ID}
 | 
			
		||||
	if opts.LikeDistros {
 | 
			
		||||
		distros = append(distros, info.Like...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var out []string
 | 
			
		||||
	for _, arch := range architectures {
 | 
			
		||||
		for _, distro := range distros {
 | 
			
		||||
			if opts.Name == "" {
 | 
			
		||||
				out = append(
 | 
			
		||||
					out,
 | 
			
		||||
					arch+"_"+distro,
 | 
			
		||||
					distro,
 | 
			
		||||
				)
 | 
			
		||||
			} else {
 | 
			
		||||
				out = append(
 | 
			
		||||
					out,
 | 
			
		||||
					opts.Name+"_"+arch+"_"+distro,
 | 
			
		||||
					opts.Name+"_"+distro,
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if opts.Name == "" {
 | 
			
		||||
			out = append(out, arch)
 | 
			
		||||
		} else {
 | 
			
		||||
			out = append(out, opts.Name+"_"+arch)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	out = append(out, opts.Name)
 | 
			
		||||
 | 
			
		||||
	for index, item := range out {
 | 
			
		||||
		out[index] = strings.ReplaceAll(item, "-", "_")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(langs) > 0 {
 | 
			
		||||
		tmp := out
 | 
			
		||||
		out = make([]string, 0, len(tmp)+(len(tmp)*len(langs)))
 | 
			
		||||
		for _, lang := range langs {
 | 
			
		||||
			for _, val := range tmp {
 | 
			
		||||
				if val == "" {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				out = append(out, val+"_"+lang)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		out = append(out, tmp...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithName(name string) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Name = name
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithOverrides(v bool) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Overrides = v
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithLikeDistros(v bool) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.LikeDistros = v
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithLanguages(langs []string) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Languages = langs
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithLanguageTags(langs []string) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Languages = langs
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ResolvedPackage is a LURE package after its overrides
 | 
			
		||||
// have been resolved
 | 
			
		||||
type ResolvedPackage struct {
 | 
			
		||||
	Name          string   `sh:"name"`
 | 
			
		||||
	Version       string   `sh:"version"`
 | 
			
		||||
	Release       int      `sh:"release"`
 | 
			
		||||
	Epoch         uint     `sh:"epoch"`
 | 
			
		||||
	Description   string   `db:"description"`
 | 
			
		||||
	Homepage      string   `db:"homepage"`
 | 
			
		||||
	Maintainer    string   `db:"maintainer"`
 | 
			
		||||
	Architectures []string `sh:"architectures"`
 | 
			
		||||
	Licenses      []string `sh:"license"`
 | 
			
		||||
	Provides      []string `sh:"provides"`
 | 
			
		||||
	Conflicts     []string `sh:"conflicts"`
 | 
			
		||||
	Replaces      []string `sh:"replaces"`
 | 
			
		||||
	Depends       []string `sh:"deps"`
 | 
			
		||||
	BuildDepends  []string `sh:"build_deps"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ResolvePackage(pkg *db.Package, overrides []string) *ResolvedPackage {
 | 
			
		||||
	out := &ResolvedPackage{}
 | 
			
		||||
	outVal := reflect.ValueOf(out).Elem()
 | 
			
		||||
	pkgVal := reflect.ValueOf(pkg).Elem()
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < outVal.NumField(); i++ {
 | 
			
		||||
		fieldVal := outVal.Field(i)
 | 
			
		||||
		fieldType := fieldVal.Type()
 | 
			
		||||
		pkgFieldVal := pkgVal.FieldByName(outVal.Type().Field(i).Name)
 | 
			
		||||
		pkgFieldType := pkgFieldVal.Type()
 | 
			
		||||
 | 
			
		||||
		if strings.HasPrefix(pkgFieldType.String(), "db.JSON") {
 | 
			
		||||
			pkgFieldVal = pkgFieldVal.FieldByName("Val")
 | 
			
		||||
			pkgFieldType = pkgFieldVal.Type()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if pkgFieldType.AssignableTo(fieldType) {
 | 
			
		||||
			fieldVal.Set(pkgFieldVal)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if pkgFieldVal.Kind() == reflect.Map && pkgFieldType.Elem().AssignableTo(fieldType) {
 | 
			
		||||
			for _, override := range overrides {
 | 
			
		||||
				overrideVal := pkgFieldVal.MapIndex(reflect.ValueOf(override))
 | 
			
		||||
				if !overrideVal.IsValid() {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				fieldVal.Set(overrideVal)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseLangs(langs []string, tags []language.Tag) ([]string, error) {
 | 
			
		||||
	out := make([]string, len(tags)+len(langs))
 | 
			
		||||
	for i, tag := range tags {
 | 
			
		||||
		base, _ := tag.Base()
 | 
			
		||||
		out[i] = base.String()
 | 
			
		||||
	}
 | 
			
		||||
	for i, lang := range langs {
 | 
			
		||||
		tag, err := language.Parse(lang)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		base, _ := tag.Base()
 | 
			
		||||
		out[len(tags)+i] = base.String()
 | 
			
		||||
	}
 | 
			
		||||
	slices.Sort(out)
 | 
			
		||||
	out = slices.Compact(out)
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										154
									
								
								internal/overrides/overrides_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								internal/overrides/overrides_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,154 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 overrides_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/overrides"
 | 
			
		||||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var info = &distro.OSRelease{
 | 
			
		||||
	ID:   "centos",
 | 
			
		||||
	Like: []string{"rhel", "fedora"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolve(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"amd64_centos_en",
 | 
			
		||||
		"centos_en",
 | 
			
		||||
		"amd64_rhel_en",
 | 
			
		||||
		"rhel_en",
 | 
			
		||||
		"amd64_fedora_en",
 | 
			
		||||
		"fedora_en",
 | 
			
		||||
		"amd64_en",
 | 
			
		||||
		"amd64_centos",
 | 
			
		||||
		"centos",
 | 
			
		||||
		"amd64_rhel",
 | 
			
		||||
		"rhel",
 | 
			
		||||
		"amd64_fedora",
 | 
			
		||||
		"fedora",
 | 
			
		||||
		"amd64",
 | 
			
		||||
		"",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveName(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Name:        "deps",
 | 
			
		||||
		Overrides:   true,
 | 
			
		||||
		LikeDistros: true,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"deps_amd64_centos",
 | 
			
		||||
		"deps_centos",
 | 
			
		||||
		"deps_amd64_rhel",
 | 
			
		||||
		"deps_rhel",
 | 
			
		||||
		"deps_amd64_fedora",
 | 
			
		||||
		"deps_fedora",
 | 
			
		||||
		"deps_amd64",
 | 
			
		||||
		"deps",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveNoLikeDistros(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Overrides:   true,
 | 
			
		||||
		LikeDistros: false,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"amd64_centos",
 | 
			
		||||
		"centos",
 | 
			
		||||
		"amd64",
 | 
			
		||||
		"",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveNoOverrides(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Name:        "deps",
 | 
			
		||||
		Overrides:   false,
 | 
			
		||||
		LikeDistros: false,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{"deps"}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveLangs(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Overrides:    true,
 | 
			
		||||
		Languages:    []string{"ru_RU", "en", "en_US"},
 | 
			
		||||
		LanguageTags: []language.Tag{language.BritishEnglish},
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"amd64_centos_en",
 | 
			
		||||
		"centos_en",
 | 
			
		||||
		"amd64_en",
 | 
			
		||||
		"amd64_centos_ru",
 | 
			
		||||
		"centos_ru",
 | 
			
		||||
		"amd64_ru",
 | 
			
		||||
		"amd64_centos",
 | 
			
		||||
		"centos",
 | 
			
		||||
		"amd64",
 | 
			
		||||
		"",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								internal/pager/highlighting.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/pager/highlighting.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 pager
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"github.com/alecthomas/chroma/v2/quick"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SyntaxHighlightBash(r io.Reader, style string) (string, error) {
 | 
			
		||||
	data, err := io.ReadAll(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w := &bytes.Buffer{}
 | 
			
		||||
	err = quick.Highlight(w, string(data), "bash", "terminal", style)
 | 
			
		||||
	return w.String(), err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										142
									
								
								internal/pager/pager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								internal/pager/pager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,142 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 pager
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/charmbracelet/bubbles/viewport"
 | 
			
		||||
	tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
	"github.com/muesli/reflow/wordwrap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	titleStyle lipgloss.Style
 | 
			
		||||
	infoStyle  lipgloss.Style
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	b := lipgloss.RoundedBorder()
 | 
			
		||||
	b.Right = "\u251C"
 | 
			
		||||
	titleStyle = lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
 | 
			
		||||
 | 
			
		||||
	b = lipgloss.RoundedBorder()
 | 
			
		||||
	b.Left = "\u2524"
 | 
			
		||||
	infoStyle = titleStyle.Copy().BorderStyle(b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Pager struct {
 | 
			
		||||
	model pagerModel
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(name, content string) *Pager {
 | 
			
		||||
	return &Pager{
 | 
			
		||||
		model: pagerModel{
 | 
			
		||||
			name:    name,
 | 
			
		||||
			content: content,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Pager) Run() error {
 | 
			
		||||
	prog := tea.NewProgram(
 | 
			
		||||
		p.model,
 | 
			
		||||
		tea.WithMouseCellMotion(),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	_, err := prog.Run()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type pagerModel struct {
 | 
			
		||||
	name     string
 | 
			
		||||
	content  string
 | 
			
		||||
	ready    bool
 | 
			
		||||
	viewport viewport.Model
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) Init() tea.Cmd {
 | 
			
		||||
	return tea.ClearScreen
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 | 
			
		||||
	var (
 | 
			
		||||
		cmd  tea.Cmd
 | 
			
		||||
		cmds []tea.Cmd
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	switch msg := msg.(type) {
 | 
			
		||||
	case tea.KeyMsg:
 | 
			
		||||
		k := msg.String()
 | 
			
		||||
		if k == "ctrl+c" || k == "q" || k == "esc" {
 | 
			
		||||
			return pm, tea.Quit
 | 
			
		||||
		}
 | 
			
		||||
	case tea.WindowSizeMsg:
 | 
			
		||||
		headerHeight := lipgloss.Height(pm.headerView())
 | 
			
		||||
		footerHeight := lipgloss.Height(pm.footerView())
 | 
			
		||||
		verticalMarginHeight := headerHeight + footerHeight
 | 
			
		||||
 | 
			
		||||
		if !pm.ready {
 | 
			
		||||
			pm.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
 | 
			
		||||
			pm.viewport.HighPerformanceRendering = true
 | 
			
		||||
			pm.viewport.YPosition = headerHeight + 1
 | 
			
		||||
			pm.viewport.SetContent(wordwrap.String(pm.content, msg.Width))
 | 
			
		||||
			pm.ready = true
 | 
			
		||||
		} else {
 | 
			
		||||
			pm.viewport.Width = msg.Width
 | 
			
		||||
			pm.viewport.Height = msg.Height - verticalMarginHeight
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cmds = append(cmds, viewport.Sync(pm.viewport))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle keyboard and mouse events in the viewport
 | 
			
		||||
	pm.viewport, cmd = pm.viewport.Update(msg)
 | 
			
		||||
	cmds = append(cmds, cmd)
 | 
			
		||||
 | 
			
		||||
	return pm, tea.Batch(cmds...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) View() string {
 | 
			
		||||
	if !pm.ready {
 | 
			
		||||
		return "\n  Initializing..."
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s\n%s\n%s", pm.headerView(), pm.viewport.View(), pm.footerView())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) headerView() string {
 | 
			
		||||
	title := titleStyle.Render(pm.name)
 | 
			
		||||
	line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(title)))
 | 
			
		||||
	return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) footerView() string {
 | 
			
		||||
	info := infoStyle.Render(fmt.Sprintf("%3.f%%", pm.viewport.ScrollPercent()*100))
 | 
			
		||||
	line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(info)))
 | 
			
		||||
	return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func max(a, b int) int {
 | 
			
		||||
	if a > b {
 | 
			
		||||
		return a
 | 
			
		||||
	}
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								internal/repos/find.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								internal/repos/find.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 repos
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FindPkgs looks for packages matching the inputs inside the database.
 | 
			
		||||
// It returns a map that maps the package name input to the packages found for it.
 | 
			
		||||
// It also returns a slice that contains the names of all packages that were not found.
 | 
			
		||||
func FindPkgs(gdb *sqlx.DB, pkgs []string) (map[string][]db.Package, []string, error) {
 | 
			
		||||
	found := map[string][]db.Package{}
 | 
			
		||||
	notFound := []string(nil)
 | 
			
		||||
 | 
			
		||||
	for _, pkgName := range pkgs {
 | 
			
		||||
		if pkgName == "" {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		result, err := db.GetPkgs(gdb, "name LIKE ?", pkgName)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		added := 0
 | 
			
		||||
		for result.Next() {
 | 
			
		||||
			var pkg db.Package
 | 
			
		||||
			err = result.StructScan(&pkg)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			added++
 | 
			
		||||
			found[pkgName] = append(found[pkgName], pkg)
 | 
			
		||||
		}
 | 
			
		||||
		result.Close()
 | 
			
		||||
 | 
			
		||||
		if added == 0 {
 | 
			
		||||
			result, err := db.GetPkgs(gdb, "json_array_contains(provides, ?)", pkgName)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, nil, err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for result.Next() {
 | 
			
		||||
				var pkg db.Package
 | 
			
		||||
				err = result.StructScan(&pkg)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return nil, nil, err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				added++
 | 
			
		||||
				found[pkgName] = append(found[pkgName], pkg)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			result.Close()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if added == 0 {
 | 
			
		||||
			notFound = append(notFound, pkgName)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return found, notFound, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										148
									
								
								internal/repos/find_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								internal/repos/find_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 repos_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestFindPkgs(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	setCfgDirs(t)
 | 
			
		||||
	defer removeCacheDir(t)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	err = repos.Pull(ctx, gdb, []types.Repo{
 | 
			
		||||
		{
 | 
			
		||||
			Name: "default",
 | 
			
		||||
			URL:  "https://github.com/Arsen6331/lure-repo.git",
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	found, notFound, err := repos.FindPkgs(gdb, []string{"itd", "nonexistentpackage1", "nonexistentpackage2"})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(notFound, []string{"nonexistentpackage1", "nonexistentpackage2"}) {
 | 
			
		||||
		t.Errorf("Expected 'nonexistentpackage{1,2} not to be found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(found) != 1 {
 | 
			
		||||
		t.Errorf("Expected 1 package found, got %d", len(found))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	itdPkgs, ok := found["itd"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatalf("Expected 'itd' packages to be found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(itdPkgs) < 2 {
 | 
			
		||||
		t.Errorf("Expected two 'itd' packages to be found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i, pkg := range itdPkgs {
 | 
			
		||||
		if !strings.HasPrefix(pkg.Name, "itd") {
 | 
			
		||||
			t.Errorf("Expected package name of all found packages to start with 'itd', got %s on element %d", pkg.Name, i)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestFindPkgsEmpty(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	setCfgDirs(t)
 | 
			
		||||
	defer removeCacheDir(t)
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, db.Package{
 | 
			
		||||
		Name:       "test1",
 | 
			
		||||
		Repository: "default",
 | 
			
		||||
		Version:    "0.0.1",
 | 
			
		||||
		Release:    1,
 | 
			
		||||
		Description: db.NewJSON(map[string]string{
 | 
			
		||||
			"en": "Test package 1",
 | 
			
		||||
			"ru": "Проверочный пакет 1",
 | 
			
		||||
		}),
 | 
			
		||||
		Provides: db.NewJSON([]string{""}),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(gdb, db.Package{
 | 
			
		||||
		Name:       "test2",
 | 
			
		||||
		Repository: "default",
 | 
			
		||||
		Version:    "0.0.1",
 | 
			
		||||
		Release:    1,
 | 
			
		||||
		Description: db.NewJSON(map[string]string{
 | 
			
		||||
			"en": "Test package 2",
 | 
			
		||||
			"ru": "Проверочный пакет 2",
 | 
			
		||||
		}),
 | 
			
		||||
		Provides: db.NewJSON([]string{"test"}),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	found, notFound, err := repos.FindPkgs(gdb, []string{"test", ""})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(notFound) != 0 {
 | 
			
		||||
		t.Errorf("Expected all packages to be found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(found) != 1 {
 | 
			
		||||
		t.Errorf("Expected 1 package found, got %d", len(found))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	testPkgs, ok := found["test"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatalf("Expected 'test' packages to be found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(testPkgs) != 1 {
 | 
			
		||||
		t.Errorf("Expected one 'test' package to be found, got %d", len(testPkgs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if testPkgs[0].Name != "test2" {
 | 
			
		||||
		t.Errorf("Expected 'test2' package, got '%s'", testPkgs[0].Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										431
									
								
								internal/repos/pull.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										431
									
								
								internal/repos/pull.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,431 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 repos
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-billy/v5"
 | 
			
		||||
	"github.com/go-git/go-billy/v5/osfs"
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/format/diff"
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"github.com/pelletier/go-toml/v2"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils/decoder"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/types"
 | 
			
		||||
	"go.arsenm.dev/lure/vercmp"
 | 
			
		||||
	"mvdan.cc/sh/v3/expand"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Pull pulls the provided repositories. If a repo doesn't exist, it will be cloned
 | 
			
		||||
// and its packages will be written to the DB. If it does exist, it will be pulled.
 | 
			
		||||
// In this case, only changed packages will be processed.
 | 
			
		||||
func Pull(ctx context.Context, gdb *sqlx.DB, repos []types.Repo) error {
 | 
			
		||||
	for _, repo := range repos {
 | 
			
		||||
		repoURL, err := url.Parse(repo.URL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Info("Pulling repository").Str("name", repo.Name).Send()
 | 
			
		||||
		repoDir := filepath.Join(config.RepoDir, repo.Name)
 | 
			
		||||
 | 
			
		||||
		var repoFS billy.Filesystem
 | 
			
		||||
		gitDir := filepath.Join(repoDir, ".git")
 | 
			
		||||
		// Only pull repos that contain valid git repos
 | 
			
		||||
		if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
 | 
			
		||||
			r, err := git.PlainOpen(repoDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			w, err := r.Worktree()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			old, err := r.Head()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = w.PullContext(ctx, &git.PullOptions{Progress: os.Stderr})
 | 
			
		||||
			if errors.Is(err, git.NoErrAlreadyUpToDate) {
 | 
			
		||||
				log.Info("Repository up to date").Str("name", repo.Name).Send()
 | 
			
		||||
			} else if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			repoFS = w.Filesystem
 | 
			
		||||
 | 
			
		||||
			// Make sure the DB is created even if the repo is up to date
 | 
			
		||||
			if !errors.Is(err, git.NoErrAlreadyUpToDate) || !config.DBPresent {
 | 
			
		||||
				new, err := r.Head()
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// If the DB was not present at startup, that means it's
 | 
			
		||||
				// empty. In this case, we need to update the DB fully
 | 
			
		||||
				// rather than just incrementally.
 | 
			
		||||
				if config.DBPresent {
 | 
			
		||||
					err = processRepoChanges(ctx, repo, r, w, old, new, gdb)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					err = processRepoFull(ctx, repo, repoDir, gdb)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						return err
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			err = os.RemoveAll(repoDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = os.MkdirAll(repoDir, 0o755)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			_, err = git.PlainCloneContext(ctx, repoDir, false, &git.CloneOptions{
 | 
			
		||||
				URL:      repoURL.String(),
 | 
			
		||||
				Progress: os.Stderr,
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = processRepoFull(ctx, repo, repoDir, gdb)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			repoFS = osfs.New(repoDir)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fl, err := repoFS.Open("lure-repo.toml")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Warn("Git repository does not appear to be a valid LURE repo").Str("repo", repo.Name).Send()
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var repoCfg types.RepoConfig
 | 
			
		||||
		err = toml.NewDecoder(fl).Decode(&repoCfg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		fl.Close()
 | 
			
		||||
 | 
			
		||||
		currentVer, _, _ := strings.Cut(config.Version, "-")
 | 
			
		||||
		if vercmp.Compare(currentVer, repoCfg.Repo.MinVersion) == -1 {
 | 
			
		||||
			log.Warn("LURE repo's minumum LURE version is greater than the current version. Try updating LURE if something doesn't work.").Str("repo", repo.Name).Send()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type actionType uint8
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	actionDelete actionType = iota
 | 
			
		||||
	actionUpdate
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type action struct {
 | 
			
		||||
	Type actionType
 | 
			
		||||
	File string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func processRepoChanges(ctx context.Context, repo types.Repo, r *git.Repository, w *git.Worktree, old, new *plumbing.Reference, gdb *sqlx.DB) error {
 | 
			
		||||
	oldCommit, err := r.CommitObject(old.Hash())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newCommit, err := r.CommitObject(new.Hash())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	patch, err := oldCommit.Patch(newCommit)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var actions []action
 | 
			
		||||
	for _, fp := range patch.FilePatches() {
 | 
			
		||||
		from, to := fp.Files()
 | 
			
		||||
 | 
			
		||||
		if !isValid(from, to) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if to == nil {
 | 
			
		||||
			actions = append(actions, action{
 | 
			
		||||
				Type: actionDelete,
 | 
			
		||||
				File: from.Path(),
 | 
			
		||||
			})
 | 
			
		||||
		} else if from == nil {
 | 
			
		||||
			actions = append(actions, action{
 | 
			
		||||
				Type: actionUpdate,
 | 
			
		||||
				File: to.Path(),
 | 
			
		||||
			})
 | 
			
		||||
		} else {
 | 
			
		||||
			if from.Path() != to.Path() {
 | 
			
		||||
				actions = append(actions,
 | 
			
		||||
					action{
 | 
			
		||||
						Type: actionDelete,
 | 
			
		||||
						File: from.Path(),
 | 
			
		||||
					},
 | 
			
		||||
					action{
 | 
			
		||||
						Type: actionUpdate,
 | 
			
		||||
						File: to.Path(),
 | 
			
		||||
					},
 | 
			
		||||
				)
 | 
			
		||||
			} else {
 | 
			
		||||
				actions = append(actions, action{
 | 
			
		||||
					Type: actionUpdate,
 | 
			
		||||
					File: to.Path(),
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	repoDir := w.Filesystem.Root()
 | 
			
		||||
	parser := syntax.NewParser()
 | 
			
		||||
 | 
			
		||||
	for _, action := range actions {
 | 
			
		||||
		env := append(os.Environ(), "scriptdir="+filepath.Dir(filepath.Join(repoDir, action.File)))
 | 
			
		||||
		runner, err := interp.New(
 | 
			
		||||
			interp.Env(expand.ListEnviron(env...)),
 | 
			
		||||
			interp.ExecHandler(shutils.NopExec),
 | 
			
		||||
			interp.ReadDirHandler(shutils.RestrictedReadDir(repoDir)),
 | 
			
		||||
			interp.StatHandler(shutils.RestrictedStat(repoDir)),
 | 
			
		||||
			interp.OpenHandler(shutils.RestrictedOpen(repoDir)),
 | 
			
		||||
			interp.StdIO(shutils.NopRWC{}, shutils.NopRWC{}, shutils.NopRWC{}),
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch action.Type {
 | 
			
		||||
		case actionDelete:
 | 
			
		||||
			if filepath.Base(action.File) != "lure.sh" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			scriptFl, err := oldCommit.File(action.File)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			r, err := scriptFl.Reader()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var pkg db.Package
 | 
			
		||||
			err = parseScript(ctx, parser, runner, r, &pkg)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = db.DeletePkgs(gdb, "name = ? AND repository = ?", pkg.Name, repo.Name)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		case actionUpdate:
 | 
			
		||||
			if filepath.Base(action.File) != "lure.sh" {
 | 
			
		||||
				action.File = filepath.Join(filepath.Dir(action.File), "lure.sh")
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			scriptFl, err := newCommit.File(action.File)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			r, err := scriptFl.Reader()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			pkg := db.Package{
 | 
			
		||||
				Description:  db.NewJSON(map[string]string{}),
 | 
			
		||||
				Homepage:     db.NewJSON(map[string]string{}),
 | 
			
		||||
				Maintainer:   db.NewJSON(map[string]string{}),
 | 
			
		||||
				Depends:      db.NewJSON(map[string][]string{}),
 | 
			
		||||
				BuildDepends: db.NewJSON(map[string][]string{}),
 | 
			
		||||
				Repository:   repo.Name,
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = parseScript(ctx, parser, runner, r, &pkg)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			resolveOverrides(runner, &pkg)
 | 
			
		||||
 | 
			
		||||
			err = db.InsertPackage(gdb, pkg)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isValid makes sure the path of the file being updated is valid.
 | 
			
		||||
// It checks to make sure the file is not within a nested directory
 | 
			
		||||
// and that it is called lure.sh.
 | 
			
		||||
func isValid(from, to diff.File) bool {
 | 
			
		||||
	var path string
 | 
			
		||||
	if from != nil {
 | 
			
		||||
		path = from.Path()
 | 
			
		||||
	}
 | 
			
		||||
	if to != nil {
 | 
			
		||||
		path = to.Path()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	match, _ := filepath.Match("*/*.sh", path)
 | 
			
		||||
	return match
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func processRepoFull(ctx context.Context, repo types.Repo, repoDir string, gdb *sqlx.DB) error {
 | 
			
		||||
	glob := filepath.Join(repoDir, "/*/lure.sh")
 | 
			
		||||
	matches, err := filepath.Glob(glob)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parser := syntax.NewParser()
 | 
			
		||||
 | 
			
		||||
	for _, match := range matches {
 | 
			
		||||
		env := append(os.Environ(), "scriptdir="+filepath.Dir(match))
 | 
			
		||||
		runner, err := interp.New(
 | 
			
		||||
			interp.Env(expand.ListEnviron(env...)),
 | 
			
		||||
			interp.ExecHandler(shutils.NopExec),
 | 
			
		||||
			interp.ReadDirHandler(shutils.RestrictedReadDir(repoDir)),
 | 
			
		||||
			interp.StatHandler(shutils.RestrictedStat(repoDir)),
 | 
			
		||||
			interp.OpenHandler(shutils.RestrictedOpen(repoDir)),
 | 
			
		||||
			interp.StdIO(shutils.NopRWC{}, shutils.NopRWC{}, shutils.NopRWC{}),
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		scriptFl, err := os.Open(match)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		pkg := db.Package{
 | 
			
		||||
			Description:  db.NewJSON(map[string]string{}),
 | 
			
		||||
			Homepage:     db.NewJSON(map[string]string{}),
 | 
			
		||||
			Maintainer:   db.NewJSON(map[string]string{}),
 | 
			
		||||
			Depends:      db.NewJSON(map[string][]string{}),
 | 
			
		||||
			BuildDepends: db.NewJSON(map[string][]string{}),
 | 
			
		||||
			Repository:   repo.Name,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = parseScript(ctx, parser, runner, scriptFl, &pkg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		resolveOverrides(runner, &pkg)
 | 
			
		||||
 | 
			
		||||
		err = db.InsertPackage(gdb, pkg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseScript(ctx context.Context, parser *syntax.Parser, runner *interp.Runner, r io.ReadCloser, pkg *db.Package) error {
 | 
			
		||||
	defer r.Close()
 | 
			
		||||
	fl, err := parser.Parse(r, "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner.Reset()
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	d := decoder.New(&distro.OSRelease{}, runner)
 | 
			
		||||
	d.Overrides = false
 | 
			
		||||
	d.LikeDistros = false
 | 
			
		||||
	return d.DecodeVars(pkg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var overridable = map[string]string{
 | 
			
		||||
	"deps":       "Depends",
 | 
			
		||||
	"build_deps": "BuildDepends",
 | 
			
		||||
	"desc":       "Description",
 | 
			
		||||
	"homepage":   "Homepage",
 | 
			
		||||
	"maintainer": "Maintainer",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func resolveOverrides(runner *interp.Runner, pkg *db.Package) {
 | 
			
		||||
	pkgVal := reflect.ValueOf(pkg).Elem()
 | 
			
		||||
	for name, val := range runner.Vars {
 | 
			
		||||
		for prefix, field := range overridable {
 | 
			
		||||
			if strings.HasPrefix(name, prefix) {
 | 
			
		||||
				override := strings.TrimPrefix(name, prefix)
 | 
			
		||||
				override = strings.TrimPrefix(override, "_")
 | 
			
		||||
 | 
			
		||||
				field := pkgVal.FieldByName(field)
 | 
			
		||||
				varVal := field.FieldByName("Val")
 | 
			
		||||
				varType := varVal.Type()
 | 
			
		||||
 | 
			
		||||
				switch varType.Elem().String() {
 | 
			
		||||
				case "[]string":
 | 
			
		||||
					varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.List))
 | 
			
		||||
				case "string":
 | 
			
		||||
					varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.Str))
 | 
			
		||||
				}
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										107
									
								
								internal/repos/pull_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								internal/repos/pull_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 repos_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func setCfgDirs(t *testing.T) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
 | 
			
		||||
	var err error
 | 
			
		||||
	config.CacheDir, err = os.MkdirTemp("/tmp", "lure-pull-test.*")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config.RepoDir = filepath.Join(config.CacheDir, "repo")
 | 
			
		||||
	config.PkgsDir = filepath.Join(config.CacheDir, "pkgs")
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(config.RepoDir, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(config.PkgsDir, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config.DBPath = filepath.Join(config.CacheDir, "db")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func removeCacheDir(t *testing.T) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
 | 
			
		||||
	err := os.RemoveAll(config.CacheDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPull(t *testing.T) {
 | 
			
		||||
	gdb, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer gdb.Close()
 | 
			
		||||
 | 
			
		||||
	setCfgDirs(t)
 | 
			
		||||
	defer removeCacheDir(t)
 | 
			
		||||
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	err = repos.Pull(ctx, gdb, []types.Repo{
 | 
			
		||||
		{
 | 
			
		||||
			Name: "default",
 | 
			
		||||
			URL:  "https://github.com/Arsen6331/lure-repo.git",
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := db.GetPkgs(gdb, "name LIKE 'itd%'")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var pkgAmt int
 | 
			
		||||
	for result.Next() {
 | 
			
		||||
		var dbPkg db.Package
 | 
			
		||||
		err = result.StructScan(&dbPkg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
		}
 | 
			
		||||
		pkgAmt++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pkgAmt < 2 {
 | 
			
		||||
		t.Errorf("Expected 2 packages to match, got %d", pkgAmt)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -21,21 +21,19 @@ package decoder
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/mitchellh/mapstructure"
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/cpu"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/overrides"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"mvdan.cc/sh/v3/expand"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrInvalidType = errors.New("val must be a pointer to a struct")
 | 
			
		||||
var ErrNotPointerToStruct = errors.New("val must be a pointer to a struct")
 | 
			
		||||
 | 
			
		||||
type VarNotFoundError struct {
 | 
			
		||||
	name string
 | 
			
		||||
@@ -45,16 +43,29 @@ func (nfe VarNotFoundError) Error() string {
 | 
			
		||||
	return "required variable '" + nfe.name + "' could not be found"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InvalidTypeError struct {
 | 
			
		||||
	name    string
 | 
			
		||||
	vartype string
 | 
			
		||||
	exptype string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ite InvalidTypeError) Error() string {
 | 
			
		||||
	return "variable '" + ite.name + "' is of type " + ite.vartype + ", but " + ite.exptype + " is expected"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Decoder provides methods for decoding variable values
 | 
			
		||||
type Decoder struct {
 | 
			
		||||
	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.
 | 
			
		||||
@@ -67,6 +78,18 @@ func (d *Decoder) DecodeVar(name string, val any) error {
 | 
			
		||||
 | 
			
		||||
	dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
 | 
			
		||||
		WeaklyTypedInput: true,
 | 
			
		||||
		DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
 | 
			
		||||
			if strings.Contains(to.Type().String(), "db.JSON") {
 | 
			
		||||
				valType := to.FieldByName("Val").Type()
 | 
			
		||||
				if !from.Type().AssignableTo(valType) {
 | 
			
		||||
					return nil, InvalidTypeError{name, from.Type().String(), valType.String()}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				to.FieldByName("Val").Set(from)
 | 
			
		||||
				return to, nil
 | 
			
		||||
			}
 | 
			
		||||
			return from.Interface(), nil
 | 
			
		||||
		}),
 | 
			
		||||
		Result:  val,
 | 
			
		||||
		TagName: "sh",
 | 
			
		||||
	})
 | 
			
		||||
@@ -89,11 +112,11 @@ func (d *Decoder) DecodeVar(name string, val any) error {
 | 
			
		||||
func (d *Decoder) DecodeVars(val any) error {
 | 
			
		||||
	valKind := reflect.TypeOf(val).Kind()
 | 
			
		||||
	if valKind != reflect.Pointer {
 | 
			
		||||
		return ErrInvalidType
 | 
			
		||||
		return ErrNotPointerToStruct
 | 
			
		||||
	} else {
 | 
			
		||||
		elemKind := reflect.TypeOf(val).Elem().Kind()
 | 
			
		||||
		if elemKind != reflect.Struct {
 | 
			
		||||
			return ErrInvalidType
 | 
			
		||||
			return ErrNotPointerToStruct
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -139,7 +162,7 @@ func (d *Decoder) DecodeVars(val any) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ScriptFunc func(ctx context.Context, sir string, args ...string) error
 | 
			
		||||
type ScriptFunc func(ctx context.Context, opts ...interp.RunnerOption) error
 | 
			
		||||
 | 
			
		||||
// GetFunc returns a function corresponding to a bash function
 | 
			
		||||
// with the given name
 | 
			
		||||
@@ -149,16 +172,21 @@ func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) {
 | 
			
		||||
		return nil, false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return func(ctx context.Context, dir string, args ...string) error {
 | 
			
		||||
	return func(ctx context.Context, opts ...interp.RunnerOption) error {
 | 
			
		||||
		sub := d.runner.Subshell()
 | 
			
		||||
		interp.Params(args...)(sub)
 | 
			
		||||
		interp.Dir(dir)(sub)
 | 
			
		||||
		for _, opt := range opts {
 | 
			
		||||
			opt(sub)
 | 
			
		||||
		}
 | 
			
		||||
		return sub.Run(ctx, fn)
 | 
			
		||||
	}, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Decoder) getFunc(name string) *syntax.Stmt {
 | 
			
		||||
	names := d.genPossibleNames(name)
 | 
			
		||||
	names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, fnName := range names {
 | 
			
		||||
		fn, ok := d.runner.Funcs[fnName]
 | 
			
		||||
		if ok {
 | 
			
		||||
@@ -171,7 +199,11 @@ func (d *Decoder) getFunc(name string) *syntax.Stmt {
 | 
			
		||||
// getVar gets a variable based on its name, taking into account
 | 
			
		||||
// override variables and nameref variables.
 | 
			
		||||
func (d *Decoder) getVar(name string) *expand.Variable {
 | 
			
		||||
	names := d.genPossibleNames(name)
 | 
			
		||||
	names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, varName := range names {
 | 
			
		||||
		val, ok := d.runner.Vars[varName]
 | 
			
		||||
		if ok {
 | 
			
		||||
@@ -189,41 +221,3 @@ func (d *Decoder) getVar(name string) *expand.Variable {
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// genPossibleNames generates a slice of the possible names that
 | 
			
		||||
// could be used in the order that they should be checked
 | 
			
		||||
func (d *Decoder) genPossibleNames(name string) []string {
 | 
			
		||||
	if !d.Overrides {
 | 
			
		||||
		return []string{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}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										224
									
								
								internal/shutils/decoder/decoder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								internal/shutils/decoder/decoder_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,224 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 decoder_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils/decoder"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const testScript = `
 | 
			
		||||
	name='test'
 | 
			
		||||
	version='0.0.1'
 | 
			
		||||
	release=1
 | 
			
		||||
	epoch=2
 | 
			
		||||
	desc="Test package"
 | 
			
		||||
	homepage='https://lure.arsenm.dev'
 | 
			
		||||
	maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
	architectures=('arm64' 'amd64')
 | 
			
		||||
	license=('GPL-3.0-or-later')
 | 
			
		||||
	provides=('test')
 | 
			
		||||
	conflicts=('test')
 | 
			
		||||
	replaces=('test-old')
 | 
			
		||||
	replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
	deps=('sudo')
 | 
			
		||||
 | 
			
		||||
	build_deps=('golang')
 | 
			
		||||
	build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
	test() {
 | 
			
		||||
		echo "Test"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	package() {
 | 
			
		||||
		install-binary test
 | 
			
		||||
	}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
var osRelease = &distro.OSRelease{
 | 
			
		||||
	ID:   "test_os",
 | 
			
		||||
	Like: []string{"arch"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDecodeVars(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
 | 
			
		||||
	var bv BuildVars
 | 
			
		||||
	err = dec.DecodeVars(&bv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := BuildVars{
 | 
			
		||||
		Name:          "test",
 | 
			
		||||
		Version:       "0.0.1",
 | 
			
		||||
		Release:       1,
 | 
			
		||||
		Epoch:         2,
 | 
			
		||||
		Description:   "Test package",
 | 
			
		||||
		Homepage:      "https://lure.arsenm.dev",
 | 
			
		||||
		Maintainer:    "Arsen Musayelyan <arsen@arsenm.dev>",
 | 
			
		||||
		Architectures: []string{"arm64", "amd64"},
 | 
			
		||||
		Licenses:      []string{"GPL-3.0-or-later"},
 | 
			
		||||
		Provides:      []string{"test"},
 | 
			
		||||
		Conflicts:     []string{"test"},
 | 
			
		||||
		Replaces:      []string{"test-legacy"},
 | 
			
		||||
		Depends:       []string{"sudo"},
 | 
			
		||||
		BuildDepends:  []string{"go"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(bv, expected) {
 | 
			
		||||
		t.Errorf("Expected %v, got %v", expected, bv)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDecodeVarsMissing(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	const testScript = `
 | 
			
		||||
		name='test'
 | 
			
		||||
		epoch=2
 | 
			
		||||
		desc="Test package"
 | 
			
		||||
		homepage='https://lure.arsenm.dev'
 | 
			
		||||
		maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
		architectures=('arm64' 'amd64')
 | 
			
		||||
		license=('GPL-3.0-or-later')
 | 
			
		||||
		provides=('test')
 | 
			
		||||
		conflicts=('test')
 | 
			
		||||
		replaces=('test-old')
 | 
			
		||||
		replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
		deps=('sudo')
 | 
			
		||||
 | 
			
		||||
		build_deps=('golang')
 | 
			
		||||
		build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
		test() {
 | 
			
		||||
			echo "Test"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		package() {
 | 
			
		||||
			install-binary test
 | 
			
		||||
		}
 | 
			
		||||
	`
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
 | 
			
		||||
	var bv BuildVars
 | 
			
		||||
	err = dec.DecodeVars(&bv)
 | 
			
		||||
 | 
			
		||||
	var notFoundErr decoder.VarNotFoundError
 | 
			
		||||
	if !errors.As(err, ¬FoundErr) {
 | 
			
		||||
		t.Fatalf("Expected VarNotFoundError, got %T %v", err, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetFunc(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
	fn, ok := dec.GetFunc("test")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatalf("Expected test() function to exist")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	err = fn(ctx, interp.StdIO(os.Stdin, buf, buf))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if buf.String() != "Test\n" {
 | 
			
		||||
		t.Fatalf(`Expected "Test\n", got %#v`, buf.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -20,24 +20,44 @@ package shutils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ExecFuncs map[string]func(interp.HandlerContext, []string) uint8
 | 
			
		||||
func InsufficientArgsError(cmd string, exp, got int) error {
 | 
			
		||||
	argsWord := "arguments"
 | 
			
		||||
	if exp == 1 {
 | 
			
		||||
		argsWord = "argument"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func (ef ExecFuncs) ExecHandler(ctx context.Context, args []string) error {
 | 
			
		||||
	return fmt.Errorf("%s: command requires at least %d %s, got %d", cmd, exp, argsWord, got)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ExecFunc func(hc interp.HandlerContext, name string, args []string) error
 | 
			
		||||
 | 
			
		||||
type ExecFuncs map[string]ExecFunc
 | 
			
		||||
 | 
			
		||||
// ExecHandler returns a new ExecHandlerFunc that falls back to fallback
 | 
			
		||||
// if the command cannot be found in the map. If fallback is nil, the default
 | 
			
		||||
// handler is used.
 | 
			
		||||
func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		name := args[0]
 | 
			
		||||
 | 
			
		||||
		if fn, ok := ef[name]; ok {
 | 
			
		||||
			hctx := interp.HandlerCtx(ctx)
 | 
			
		||||
		ec := fn(hctx, args)
 | 
			
		||||
		if ec != 0 {
 | 
			
		||||
			return interp.NewExitStatus(ec)
 | 
			
		||||
			if len(args) > 1 {
 | 
			
		||||
				return fn(hctx, args[0], args[1:])
 | 
			
		||||
			} else {
 | 
			
		||||
				return fn(hctx, args[0], nil)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	defExec := interp.DefaultExecHandler(2 * time.Second)
 | 
			
		||||
	return defExec(ctx, args)
 | 
			
		||||
		if fallback == nil {
 | 
			
		||||
			fallback = interp.DefaultExecHandler(2 * time.Second)
 | 
			
		||||
		}
 | 
			
		||||
		return fallback(ctx, args)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										124
									
								
								internal/shutils/exec_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								internal/shutils/exec_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 shutils_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils/decoder"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const testScript = `
 | 
			
		||||
	name='test'
 | 
			
		||||
	version='0.0.1'
 | 
			
		||||
	release=1
 | 
			
		||||
	epoch=2
 | 
			
		||||
	desc="Test package"
 | 
			
		||||
	homepage='https://lure.arsenm.dev'
 | 
			
		||||
	maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
	architectures=('arm64' 'amd64')
 | 
			
		||||
	license=('GPL-3.0-or-later')
 | 
			
		||||
	provides=('test')
 | 
			
		||||
	conflicts=('test')
 | 
			
		||||
	replaces=('test-old')
 | 
			
		||||
	replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
	deps=('sudo')
 | 
			
		||||
 | 
			
		||||
	build_deps=('golang')
 | 
			
		||||
	build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
	test() {
 | 
			
		||||
		test-cmd "Hello, World"
 | 
			
		||||
		test-fb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	package() {
 | 
			
		||||
		install-binary test
 | 
			
		||||
	}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
var osRelease = &distro.OSRelease{
 | 
			
		||||
	ID:   "test_os",
 | 
			
		||||
	Like: []string{"arch"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExecFuncs(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
	fn, ok := dec.GetFunc("test")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatalf("Expected test() function to exist")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	eh := shutils.ExecFuncs{
 | 
			
		||||
		"test-cmd": func(hc interp.HandlerContext, name string, args []string) error {
 | 
			
		||||
			if name != "test-cmd" {
 | 
			
		||||
				t.Errorf("Expected name to be 'test-cmd', got '%s'", name)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(args) < 1 {
 | 
			
		||||
				t.Fatalf("Expected at least one argument, got %d", len(args))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if args[0] != "Hello, World" {
 | 
			
		||||
				t.Errorf("Expected first argument to be 'Hello, World', got '%s'", args[0])
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fbInvoked := false
 | 
			
		||||
	fbHandler := func(context.Context, []string) error {
 | 
			
		||||
		fbInvoked = true
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = fn(ctx, interp.ExecHandler(eh.ExecHandler(fbHandler)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !fbInvoked {
 | 
			
		||||
		t.Errorf("Expected fallback handler to be invoked")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -46,8 +46,8 @@ func (NopRWC) Read([]byte) (int, error) {
 | 
			
		||||
	return 0, io.EOF
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Write([]byte) (int, error) {
 | 
			
		||||
	return 0, io.EOF
 | 
			
		||||
func (NopRWC) Write(b []byte) (int, error) {
 | 
			
		||||
	return len(b), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Close() error {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								internal/shutils/nop_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/shutils/nop_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 shutils_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"go.arsenm.dev/lure/internal/shutils"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestNopExec(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(`/bin/echo test`), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	runner, err := interp.New(
 | 
			
		||||
		interp.ExecHandler(shutils.NopExec),
 | 
			
		||||
		interp.StdIO(os.Stdin, buf, buf),
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if buf.String() != "" {
 | 
			
		||||
		t.Fatalf("Expected empty string, got %#v", buf.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								internal/shutils/restricted.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								internal/shutils/restricted.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 shutils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string) ([]os.FileInfo, error) {
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(s, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultReadDirHandler()(ctx, s)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil, os.ErrNotExist
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string, b bool) (os.FileInfo, error) {
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(s, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultStatHandler()(ctx, s, b)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil, os.ErrNotExist
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string, i int, fm os.FileMode) (io.ReadWriteCloser, error) {
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(s, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultOpenHandler()(ctx, s, i, fm)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return NopRWC{}, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		if slices.Contains(allowedCmds, args[0]) {
 | 
			
		||||
			return interp.DefaultExecHandler(2*time.Second)(ctx, args)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								internal/types/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								internal/types/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 types
 | 
			
		||||
 | 
			
		||||
// Config represents the LURE configuration file
 | 
			
		||||
type Config struct {
 | 
			
		||||
	RootCmd          string   `toml:"rootCmd"`
 | 
			
		||||
	PagerStyle       string   `toml:"pagerStyle"`
 | 
			
		||||
	IgnorePkgUpdates []string `toml:"ignorePkgUpdates"`
 | 
			
		||||
	Repos            []Repo   `toml:"repo"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Repo represents a LURE repo within a configuration file
 | 
			
		||||
type Repo struct {
 | 
			
		||||
	Name string `toml:"name"`
 | 
			
		||||
	URL  string `toml:"url"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								internal/types/repo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/types/repo.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 types
 | 
			
		||||
 | 
			
		||||
// RepoConfig represents a LURE repo's lure-repo.toml file.
 | 
			
		||||
type RepoConfig struct {
 | 
			
		||||
	Repo struct {
 | 
			
		||||
		MinVersion string `toml:"minVersion"`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										77
									
								
								list.go
									
									
									
									
									
								
							
							
						
						
									
										77
									
								
								list.go
									
									
									
									
									
								
							@@ -1,30 +1,87 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
	"go.arsenm.dev/lure/manager"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func listCmd(c *cli.Context) error {
 | 
			
		||||
	info, err := distro.ParseOSRelease(c.Context)
 | 
			
		||||
	err := repos.Pull(c.Context, gdb, cfg.Repos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error parsing os-release").Err(err).Send()
 | 
			
		||||
		log.Fatal("Error pulling repositories").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pkgs, err := findPkg("*")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error finding packages").Err(err).Send()
 | 
			
		||||
	where := "true"
 | 
			
		||||
	args := []any(nil)
 | 
			
		||||
	if c.NArg() > 0 {
 | 
			
		||||
		where = "name LIKE ? OR json_array_contains(provides, ?)"
 | 
			
		||||
		args = []any{c.Args().First(), c.Args().First()}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, script := range pkgs {
 | 
			
		||||
		vars, err := getBuildVars(c.Context, script, info)
 | 
			
		||||
	result, err := db.GetPkgs(gdb, where, args...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
			log.Fatal("Error getting build variables").Err(err).Send()
 | 
			
		||||
		log.Fatal("Error getting packages").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	defer result.Close()
 | 
			
		||||
 | 
			
		||||
	var installed map[string]string
 | 
			
		||||
	if c.Bool("installed") {
 | 
			
		||||
		mgr := manager.Detect()
 | 
			
		||||
		if mgr == nil {
 | 
			
		||||
			log.Fatal("Unable to detect supported package manager on system").Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(vars.Name, vars.Version)
 | 
			
		||||
		installed, err = mgr.ListInstalled(&manager.Opts{AsRoot: false})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error listing installed packages").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for result.Next() {
 | 
			
		||||
		var pkg db.Package
 | 
			
		||||
		err := result.StructScan(&pkg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		version := pkg.Version
 | 
			
		||||
		if c.Bool("installed") {
 | 
			
		||||
			instVersion, ok := installed[pkg.Name]
 | 
			
		||||
			if !ok {
 | 
			
		||||
				return nil
 | 
			
		||||
			} else {
 | 
			
		||||
				version = instVersion
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Printf("%s/%s %s\n", pkg.Repository, pkg.Name, version)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error iterating over packages").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										116
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										116
									
								
								main.go
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -20,16 +20,41 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"go.arsenm.dev/logger"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/manager"
 | 
			
		||||
	"go.arsenm.dev/translate"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var log = logger.NewPretty(os.Stderr)
 | 
			
		||||
//go:generate scripts/gen-version.sh
 | 
			
		||||
 | 
			
		||||
//go:embed translations
 | 
			
		||||
var translationFS embed.FS
 | 
			
		||||
 | 
			
		||||
var translator translate.Translator
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	logger := logger.NewCLI(os.Stderr)
 | 
			
		||||
 | 
			
		||||
	t, err := translate.NewFromFS(translationFS)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Fatal("Error creating new translator").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	translator = t
 | 
			
		||||
 | 
			
		||||
	log.Logger = translate.NewLogger(logger, t, config.Language)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
@@ -39,18 +64,34 @@ func main() {
 | 
			
		||||
		<-ctx.Done()
 | 
			
		||||
		// Exit the program after a maximum of 200ms
 | 
			
		||||
		time.Sleep(200 * time.Millisecond)
 | 
			
		||||
		gdb.Close()
 | 
			
		||||
		os.Exit(0)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	app := &cli.App{
 | 
			
		||||
		Name:  "lure",
 | 
			
		||||
		Usage: "Linux User REpository",
 | 
			
		||||
		Flags: []cli.Flag{
 | 
			
		||||
			&cli.StringFlag{
 | 
			
		||||
				Name:    "pm-args",
 | 
			
		||||
				Aliases: []string{"P"},
 | 
			
		||||
				Usage:   "Arguments to be passed on to the package manager",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Commands: []*cli.Command{
 | 
			
		||||
			{
 | 
			
		||||
				Flags: []cli.Flag{
 | 
			
		||||
					&cli.BoolFlag{
 | 
			
		||||
						Name:    "clean",
 | 
			
		||||
						Aliases: []string{"c"},
 | 
			
		||||
						Usage:   "Build package from scratch even if there's an already built package available",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Name:         "install",
 | 
			
		||||
				Usage:        "Install a new package",
 | 
			
		||||
				Aliases:      []string{"in"},
 | 
			
		||||
				Action:       installCmd,
 | 
			
		||||
				BashComplete: completionInstall,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Name:    "remove",
 | 
			
		||||
@@ -59,17 +100,37 @@ func main() {
 | 
			
		||||
				Action:  removeCmd,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Flags: []cli.Flag{
 | 
			
		||||
					&cli.BoolFlag{
 | 
			
		||||
						Name:    "clean",
 | 
			
		||||
						Aliases: []string{"c"},
 | 
			
		||||
						Usage:   "Build package from scratch even if there's an already built package available",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Name:    "upgrade",
 | 
			
		||||
				Usage:   "Upgrade all installed packages",
 | 
			
		||||
				Aliases: []string{"up"},
 | 
			
		||||
				Action:  upgradeCmd,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Flags: []cli.Flag{
 | 
			
		||||
					&cli.BoolFlag{
 | 
			
		||||
						Name:    "all",
 | 
			
		||||
						Aliases: []string{"a"},
 | 
			
		||||
						Usage:   "Show all information, not just for the current distro",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Name:   "info",
 | 
			
		||||
				Usage:  "Print information about a package",
 | 
			
		||||
				Action: infoCmd,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Flags: []cli.Flag{
 | 
			
		||||
					&cli.BoolFlag{
 | 
			
		||||
						Name:    "installed",
 | 
			
		||||
						Aliases: []string{"I"},
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Name:    "list",
 | 
			
		||||
				Usage:   "List LURE repo packages",
 | 
			
		||||
				Aliases: []string{"ls"},
 | 
			
		||||
@@ -83,6 +144,11 @@ func main() {
 | 
			
		||||
						Value:   "lure.sh",
 | 
			
		||||
						Usage:   "Path to the build script",
 | 
			
		||||
					},
 | 
			
		||||
					&cli.BoolFlag{
 | 
			
		||||
						Name:    "clean",
 | 
			
		||||
						Aliases: []string{"c"},
 | 
			
		||||
						Usage:   "Build package from scratch even if there's an already built package available",
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
				Name:   "build",
 | 
			
		||||
				Usage:  "Build a local package",
 | 
			
		||||
@@ -128,7 +194,30 @@ func main() {
 | 
			
		||||
				Aliases: []string{"ref"},
 | 
			
		||||
				Action:  refreshCmd,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Name:   "fix",
 | 
			
		||||
				Usage:  "Attempt to fix problems with LURE",
 | 
			
		||||
				Action: fixCmd,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				Name:   "version",
 | 
			
		||||
				Usage:  "Display the current LURE version and exit",
 | 
			
		||||
				Action: displayVersion,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Before: func(c *cli.Context) error {
 | 
			
		||||
			args := strings.Split(c.String("pm-args"), " ")
 | 
			
		||||
			if len(args) == 1 && args[0] == "" {
 | 
			
		||||
				args = nil
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			manager.Args = append(manager.Args, args...)
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		After: func(ctx *cli.Context) error {
 | 
			
		||||
			return gdb.Close()
 | 
			
		||||
		},
 | 
			
		||||
		EnableBashCompletion: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := app.RunContext(ctx, os.Args)
 | 
			
		||||
@@ -136,3 +225,26 @@ func main() {
 | 
			
		||||
		log.Error("Error while running app").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func displayVersion(c *cli.Context) error {
 | 
			
		||||
	print(config.Version)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func completionInstall(c *cli.Context) {
 | 
			
		||||
	result, err := db.GetPkgs(gdb, "true")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error getting packages").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	defer result.Close()
 | 
			
		||||
 | 
			
		||||
	for result.Next() {
 | 
			
		||||
		var pkg db.Package
 | 
			
		||||
		err = result.StructScan(&pkg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error iterating over packages").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		fmt.Println(pkg.Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -112,15 +112,8 @@ func (a *APK) UpgradeAll(opts *Opts) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *APK) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	out := map[string]string{}
 | 
			
		||||
 | 
			
		||||
	var cmd *exec.Cmd
 | 
			
		||||
	if opts.AsRoot {
 | 
			
		||||
		cmd = exec.Command(getRootCmd(a.rootCmd), "apk", "list", "-I")
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command("apk", "list", "-I")
 | 
			
		||||
	}
 | 
			
		||||
	cmd := exec.Command("apk", "list", "-I")
 | 
			
		||||
 | 
			
		||||
	stdout, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -159,6 +152,7 @@ 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, opts.Args...)
 | 
			
		||||
		cmd.Args = append(cmd.Args, args...)
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command(mgrCmd, args...)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -104,15 +104,8 @@ func (a *APT) UpgradeAll(opts *Opts) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *APT) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	out := map[string]string{}
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	}
 | 
			
		||||
	cmd := exec.Command("dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
 | 
			
		||||
 | 
			
		||||
	stdout, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -145,6 +138,7 @@ 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, opts.Args...)
 | 
			
		||||
		cmd.Args = append(cmd.Args, args...)
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command(mgrCmd, args...)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -60,7 +60,7 @@ func (d *DNF) Sync(opts *Opts) error {
 | 
			
		||||
 | 
			
		||||
func (d *DNF) Install(opts *Opts, pkgs ...string) error {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	cmd := d.getCmd(opts, "dnf", "install")
 | 
			
		||||
	cmd := d.getCmd(opts, "dnf", "install", "--allowerasing")
 | 
			
		||||
	cmd.Args = append(cmd.Args, pkgs...)
 | 
			
		||||
	setCmdEnv(cmd)
 | 
			
		||||
	err := cmd.Run()
 | 
			
		||||
@@ -111,15 +111,8 @@ func (d *DNF) UpgradeAll(opts *Opts) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *DNF) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	out := map[string]string{}
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	}
 | 
			
		||||
	cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
 | 
			
		||||
 | 
			
		||||
	stdout, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -137,6 +130,7 @@ func (d *DNF) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		version = strings.TrimPrefix(version, "0:")
 | 
			
		||||
		out[name] = version
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -152,6 +146,7 @@ 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, opts.Args...)
 | 
			
		||||
		cmd.Args = append(cmd.Args, args...)
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command(mgrCmd, args...)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -23,9 +23,12 @@ import (
 | 
			
		||||
	"os/exec"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var Args []string
 | 
			
		||||
 | 
			
		||||
type Opts struct {
 | 
			
		||||
	AsRoot    bool
 | 
			
		||||
	NoConfirm bool
 | 
			
		||||
	Args      []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var DefaultOpts = &Opts{
 | 
			
		||||
@@ -114,7 +117,8 @@ func setCmdEnv(cmd *exec.Cmd) {
 | 
			
		||||
 | 
			
		||||
func ensureOpts(opts *Opts) *Opts {
 | 
			
		||||
	if opts == nil {
 | 
			
		||||
		return DefaultOpts
 | 
			
		||||
		opts = DefaultOpts
 | 
			
		||||
	}
 | 
			
		||||
	opts.Args = append(opts.Args, Args...)
 | 
			
		||||
	return opts
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -60,7 +60,7 @@ func (p *Pacman) Sync(opts *Opts) error {
 | 
			
		||||
 | 
			
		||||
func (p *Pacman) Install(opts *Opts, pkgs ...string) error {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	cmd := p.getCmd(opts, "pacman", "-S")
 | 
			
		||||
	cmd := p.getCmd(opts, "pacman", "-S", "--needed")
 | 
			
		||||
	cmd.Args = append(cmd.Args, pkgs...)
 | 
			
		||||
	setCmdEnv(cmd)
 | 
			
		||||
	err := cmd.Run()
 | 
			
		||||
@@ -72,7 +72,7 @@ func (p *Pacman) Install(opts *Opts, pkgs ...string) error {
 | 
			
		||||
 | 
			
		||||
func (p *Pacman) InstallLocal(opts *Opts, pkgs ...string) error {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	cmd := p.getCmd(opts, "pacman", "-U")
 | 
			
		||||
	cmd := p.getCmd(opts, "pacman", "-U", "--needed")
 | 
			
		||||
	cmd.Args = append(cmd.Args, pkgs...)
 | 
			
		||||
	setCmdEnv(cmd)
 | 
			
		||||
	err := cmd.Run()
 | 
			
		||||
@@ -111,15 +111,8 @@ func (p *Pacman) UpgradeAll(opts *Opts) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Pacman) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	out := map[string]string{}
 | 
			
		||||
 | 
			
		||||
	var cmd *exec.Cmd
 | 
			
		||||
	if opts.AsRoot {
 | 
			
		||||
		cmd = exec.Command(getRootCmd(p.rootCmd), "pacman", "-Q")
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command("pacman", "-Q")
 | 
			
		||||
	}
 | 
			
		||||
	cmd := exec.Command("pacman", "-Q")
 | 
			
		||||
 | 
			
		||||
	stdout, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -152,6 +145,7 @@ 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, opts.Args...)
 | 
			
		||||
		cmd.Args = append(cmd.Args, args...)
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command(mgrCmd, args...)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -60,7 +60,7 @@ func (y *YUM) Sync(opts *Opts) error {
 | 
			
		||||
 | 
			
		||||
func (y *YUM) Install(opts *Opts, pkgs ...string) error {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	cmd := y.getCmd(opts, "yum", "install")
 | 
			
		||||
	cmd := y.getCmd(opts, "yum", "install", "--allowerasing")
 | 
			
		||||
	cmd.Args = append(cmd.Args, pkgs...)
 | 
			
		||||
	setCmdEnv(cmd)
 | 
			
		||||
	err := cmd.Run()
 | 
			
		||||
@@ -111,15 +111,8 @@ func (y *YUM) UpgradeAll(opts *Opts) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (y *YUM) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	out := map[string]string{}
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	}
 | 
			
		||||
	cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
 | 
			
		||||
 | 
			
		||||
	stdout, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -137,6 +130,7 @@ func (y *YUM) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		version = strings.TrimPrefix(version, "0:")
 | 
			
		||||
		out[name] = version
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -152,6 +146,7 @@ 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, opts.Args...)
 | 
			
		||||
		cmd.Args = append(cmd.Args, args...)
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command(mgrCmd, args...)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -111,15 +111,8 @@ func (z *Zypper) UpgradeAll(opts *Opts) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (z *Zypper) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
	opts = ensureOpts(opts)
 | 
			
		||||
	out := map[string]string{}
 | 
			
		||||
 | 
			
		||||
	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")
 | 
			
		||||
	}
 | 
			
		||||
	cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
 | 
			
		||||
 | 
			
		||||
	stdout, err := cmd.StdoutPipe()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -137,6 +130,7 @@ func (z *Zypper) ListInstalled(opts *Opts) (map[string]string, error) {
 | 
			
		||||
		if !ok {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		version = strings.TrimPrefix(version, "0:")
 | 
			
		||||
		out[name] = version
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -152,6 +146,7 @@ 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, opts.Args...)
 | 
			
		||||
		cmd.Args = append(cmd.Args, args...)
 | 
			
		||||
	} else {
 | 
			
		||||
		cmd = exec.Command(mgrCmd, args...)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										188
									
								
								repo.go
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								repo.go
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -19,53 +19,49 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/pelletier/go-toml/v2"
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"go.arsenm.dev/lure/download"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/config"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/types"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PkgNotFoundError struct {
 | 
			
		||||
	pkgName string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p PkgNotFoundError) Error() string {
 | 
			
		||||
	return "package '" + p.pkgName + "' could not be found in any repository"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addrepoCmd(c *cli.Context) error {
 | 
			
		||||
	name := c.String("name")
 | 
			
		||||
	repoURL := c.String("url")
 | 
			
		||||
 | 
			
		||||
	for _, repo := range config.Repos {
 | 
			
		||||
	for _, repo := range cfg.Repos {
 | 
			
		||||
		if repo.URL == repoURL {
 | 
			
		||||
			log.Fatal("Repo already exists").Str("name", repo.Name).Send()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config.Repos = append(config.Repos, Repo{
 | 
			
		||||
	cfg.Repos = append(cfg.Repos, types.Repo{
 | 
			
		||||
		Name: name,
 | 
			
		||||
		URL:  repoURL,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	cfgFl, err := os.Create(cfgPath)
 | 
			
		||||
	cfgFl, err := os.Create(config.ConfigPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error opening config file").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = toml.NewEncoder(cfgFl).Encode(&config)
 | 
			
		||||
	err = toml.NewEncoder(cfgFl).Encode(&cfg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error encoding config").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = repos.Pull(c.Context, gdb, cfg.Repos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error pulling repos").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -74,7 +70,7 @@ func removerepoCmd(c *cli.Context) error {
 | 
			
		||||
 | 
			
		||||
	found := false
 | 
			
		||||
	index := 0
 | 
			
		||||
	for i, repo := range config.Repos {
 | 
			
		||||
	for i, repo := range cfg.Repos {
 | 
			
		||||
		if repo.Name == name {
 | 
			
		||||
			index = i
 | 
			
		||||
			found = true
 | 
			
		||||
@@ -84,167 +80,35 @@ func removerepoCmd(c *cli.Context) error {
 | 
			
		||||
		log.Fatal("Repo does not exist").Str("name", name).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config.Repos = slices.Delete(config.Repos, index, index+1)
 | 
			
		||||
	cfg.Repos = slices.Delete(cfg.Repos, index, index+1)
 | 
			
		||||
 | 
			
		||||
	cfgFl, err := os.Create(cfgPath)
 | 
			
		||||
	cfgFl, err := os.Create(config.ConfigPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error opening config file").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = toml.NewEncoder(cfgFl).Encode(&config)
 | 
			
		||||
	err = toml.NewEncoder(cfgFl).Encode(&cfg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error encoding config").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.RemoveAll(filepath.Join(cacheDir, "repo", name))
 | 
			
		||||
	err = os.RemoveAll(filepath.Join(config.RepoDir, name))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error removing repo directory").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.DeletePkgs(gdb, "repository = ?", name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error removing packages from database").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func refreshCmd(c *cli.Context) error {
 | 
			
		||||
	err := pullRepos(c.Context)
 | 
			
		||||
	err := repos.Pull(c.Context, gdb, cfg.Repos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error pulling repos").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func findPkg(pkg string) ([]string, error) {
 | 
			
		||||
	baseRepoDir := filepath.Join(cacheDir, "repo")
 | 
			
		||||
 | 
			
		||||
	var out []string
 | 
			
		||||
	for _, repo := range config.Repos {
 | 
			
		||||
		repoDir := filepath.Join(baseRepoDir, repo.Name)
 | 
			
		||||
		err := os.MkdirAll(repoDir, 0o755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		glob := filepath.Join(repoDir, pkg, "lure.sh")
 | 
			
		||||
		matches, err := filepath.Glob(glob)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(matches) == 0 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, matches...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(out) == 0 {
 | 
			
		||||
		return nil, PkgNotFoundError{pkgName: pkg}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func pkgPrompt(options []string) ([]string, error) {
 | 
			
		||||
	names := make([]string, len(options))
 | 
			
		||||
	for i, option := range options {
 | 
			
		||||
		names[i] = filepath.Base(filepath.Dir(option))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prompt := &survey.MultiSelect{
 | 
			
		||||
		Options: names,
 | 
			
		||||
		Message: "Choose which package(s) to install",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var choices []int
 | 
			
		||||
	err := survey.AskOne(prompt, &choices)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := make([]string, len(choices))
 | 
			
		||||
	for i, choiceIndex := range choices {
 | 
			
		||||
		out[i] = options[choiceIndex]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func findPkgs(pkgs []string) (scripts, notFound []string) {
 | 
			
		||||
	for _, pkg := range pkgs {
 | 
			
		||||
		found, err := findPkg(pkg)
 | 
			
		||||
		if _, ok := err.(PkgNotFoundError); ok {
 | 
			
		||||
			notFound = append(notFound, pkg)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(found) == 1 {
 | 
			
		||||
			scripts = append(scripts, found...)
 | 
			
		||||
		} else {
 | 
			
		||||
			choices, err := pkgPrompt(found)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("Error prompting for package choices").Err(err).Send()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			scripts = append(scripts, choices...)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func pullRepos(ctx context.Context) error {
 | 
			
		||||
	baseRepoDir := filepath.Join(cacheDir, "repo")
 | 
			
		||||
 | 
			
		||||
	for _, repo := range config.Repos {
 | 
			
		||||
		repoURL, err := url.Parse(repo.URL)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		log.Info("Pulling repository").Str("name", repo.Name).Send()
 | 
			
		||||
		repoDir := filepath.Join(baseRepoDir, repo.Name)
 | 
			
		||||
 | 
			
		||||
		gitDir := filepath.Join(repoDir, ".git")
 | 
			
		||||
		if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
 | 
			
		||||
			r, err := git.PlainOpen(repoDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			w, err := r.Worktree()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = w.PullContext(ctx, &git.PullOptions{Progress: os.Stderr})
 | 
			
		||||
			if err == git.NoErrAlreadyUpToDate {
 | 
			
		||||
				log.Info("Repository up to date").Str("name", repo.Name).Send()
 | 
			
		||||
				continue
 | 
			
		||||
			} else if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = os.RemoveAll(repoDir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = os.MkdirAll(repoDir, 0o755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !strings.HasPrefix(repoURL.Scheme, "git+") {
 | 
			
		||||
			repoURL.Scheme = "git+" + repoURL.Scheme
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = download.Get(ctx, download.GetOptions{
 | 
			
		||||
			SourceURL:   repoURL.String(),
 | 
			
		||||
			Destination: repoDir,
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								scripts/completion/bash
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								scripts/completion/bash
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
#! /bin/bash
 | 
			
		||||
 | 
			
		||||
: ${PROG:=$(basename ${BASH_SOURCE})}
 | 
			
		||||
 | 
			
		||||
_cli_bash_autocomplete() {
 | 
			
		||||
  if [[ "${COMP_WORDS[0]}" != "source" ]]; then
 | 
			
		||||
    local cur opts base
 | 
			
		||||
    COMPREPLY=()
 | 
			
		||||
    cur="${COMP_WORDS[COMP_CWORD]}"
 | 
			
		||||
    if [[ "$cur" == "-"* ]]; then
 | 
			
		||||
      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
 | 
			
		||||
    else
 | 
			
		||||
      opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
 | 
			
		||||
    fi
 | 
			
		||||
    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
 | 
			
		||||
    return 0
 | 
			
		||||
  fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
 | 
			
		||||
unset PROG
 | 
			
		||||
							
								
								
									
										20
									
								
								scripts/completion/zsh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								scripts/completion/zsh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
#compdef lure
 | 
			
		||||
 | 
			
		||||
_cli_zsh_autocomplete() {
 | 
			
		||||
  local -a opts
 | 
			
		||||
  local cur
 | 
			
		||||
  cur=${words[-1]}
 | 
			
		||||
  if [[ "$cur" == "-"* ]]; then
 | 
			
		||||
    opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
 | 
			
		||||
  else
 | 
			
		||||
    opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}")
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
  if [[ "${opts[1]}" != "" ]]; then
 | 
			
		||||
    _describe 'values' opts
 | 
			
		||||
  else
 | 
			
		||||
    _files
 | 
			
		||||
  fi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
compdef _cli_zsh_autocomplete lure
 | 
			
		||||
							
								
								
									
										3
									
								
								scripts/gen-version.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										3
									
								
								scripts/gen-version.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
git describe --tags > internal/config/version.txt
 | 
			
		||||
							
								
								
									
										82
									
								
								scripts/install.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								scripts/install.sh
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
info() {
 | 
			
		||||
  echo $'\x1b[32m[INFO]\x1b[0m' $@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
warn() {
 | 
			
		||||
  echo $'\x1b[31m[WARN]\x1b[0m' $@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
error() {
 | 
			
		||||
  echo $'\x1b[31;1m[ERR]\x1b[0m' $@
 | 
			
		||||
  exit 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
installPkg() {
 | 
			
		||||
  rootCmd=""
 | 
			
		||||
  if command -v doas &>/dev/null; then
 | 
			
		||||
    rootCmd="doas"
 | 
			
		||||
  elif command -v sudo &>/dev/null; then
 | 
			
		||||
    rootCmd="sudo"
 | 
			
		||||
  else
 | 
			
		||||
    warn "No privilege elevation command (e.g. sudo, doas) detected"
 | 
			
		||||
  fi
 | 
			
		||||
  
 | 
			
		||||
  case $1 in
 | 
			
		||||
  pacman) $rootCmd pacman --noconfirm -U ${@:2} ;;
 | 
			
		||||
  apk) $rootCmd apk add --allow-untrusted ${@:2} ;;
 | 
			
		||||
  *) $rootCmd $1 install -y ${@:2} ;;
 | 
			
		||||
  esac
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if ! command -v curl &>/dev/null; then
 | 
			
		||||
  error "This script requires the curl command. Please install it and run again."
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
pkgFormat=""
 | 
			
		||||
pkgMgr=""
 | 
			
		||||
if command -v pacman &>/dev/null; then
 | 
			
		||||
  info "Detected pacman"
 | 
			
		||||
  pkgFormat="pkg.tar.zst"
 | 
			
		||||
  pkgMgr="pacman"
 | 
			
		||||
elif command -v apt &>/dev/null; then
 | 
			
		||||
  info "Detected apt"
 | 
			
		||||
  pkgFormat="deb"
 | 
			
		||||
  pkgMgr="apt"
 | 
			
		||||
elif command -v dnf &>/dev/null; then
 | 
			
		||||
  info "Detected dnf"
 | 
			
		||||
  pkgFormat="rpm"
 | 
			
		||||
  pkgMgr="dnf"
 | 
			
		||||
elif command -v yum &>/dev/null; then
 | 
			
		||||
  info "Detected yum"
 | 
			
		||||
  pkgFormat="rpm"
 | 
			
		||||
  pkgMgr="yum"
 | 
			
		||||
elif command -v zypper &>/dev/null; then
 | 
			
		||||
  info "Detected zypper"
 | 
			
		||||
  pkgFormat="rpm"
 | 
			
		||||
  pkgMgr="zypper"
 | 
			
		||||
elif command -v apk &>/dev/null; then
 | 
			
		||||
  info "Detected apk"
 | 
			
		||||
  pkgFormat="apk"
 | 
			
		||||
  pkgMgr="apk"
 | 
			
		||||
else
 | 
			
		||||
  error "No supported package manager detected!"
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
latestVersion=$(curl -sI 'https://gitea.arsenm.dev/Arsen6331/lure/releases/latest' | grep -o 'location: .*' | rev | cut -d '/' -f1 | rev | tr -d '[:space:]')
 | 
			
		||||
info "Found latest LURE version:" $latestVersion
 | 
			
		||||
 | 
			
		||||
fname="$(mktemp -u -p /tmp "lure.XXXXXXXXXX").${pkgFormat}"
 | 
			
		||||
url="https://gitea.arsenm.dev/Arsen6331/lure/releases/download/${latestVersion}/linux-user-repository-${latestVersion#v}-linux-$(uname -m).${pkgFormat}"
 | 
			
		||||
 | 
			
		||||
info "Downloading LURE package" 
 | 
			
		||||
curl -L $url -o $fname
 | 
			
		||||
 | 
			
		||||
info "Installing LURE package"
 | 
			
		||||
installPkg $pkgMgr $fname
 | 
			
		||||
 | 
			
		||||
info "Cleaning up"
 | 
			
		||||
rm $fname
 | 
			
		||||
 | 
			
		||||
info "Done!"
 | 
			
		||||
							
								
								
									
										143
									
								
								translations/lure.en.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								translations/lure.en.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1228660974
 | 
			
		||||
value = 'Pulling repository'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2779805870
 | 
			
		||||
value = 'Repository up to date'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1433222829
 | 
			
		||||
value = 'Would you like to view the build script for'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2470847050
 | 
			
		||||
value = 'Failed to prompt user to view build script'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 855659503
 | 
			
		||||
value = 'Would you still like to continue?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1997041569
 | 
			
		||||
value = 'User chose not to continue after reading script'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2347700990
 | 
			
		||||
value = 'Building package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2105058868
 | 
			
		||||
value = 'Downloading sources'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1884485082
 | 
			
		||||
value = 'Downloading source'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1519177982
 | 
			
		||||
value = 'Error building package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2125220917
 | 
			
		||||
value = 'Choose which package(s) to install'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 812531604
 | 
			
		||||
value = 'Error prompting for choice of package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1040982801
 | 
			
		||||
value = 'Updating version'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1014897988
 | 
			
		||||
value = 'Remove build dependencies?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2205430948
 | 
			
		||||
value = 'Installing build dependencies'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2522710805
 | 
			
		||||
value = 'Installing dependencies'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3602138206
 | 
			
		||||
value = 'Error installing package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2235794125
 | 
			
		||||
value = 'Would you like to remove build dependencies?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2562049386
 | 
			
		||||
value = "Your system's CPU architecture doesn't match this package. Do you want to build anyway?"
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 4006393493
 | 
			
		||||
value = 'The checksums array must be the same length as sources'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3759891273
 | 
			
		||||
value = 'The package() function is required'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1057080231
 | 
			
		||||
value = 'Executing package()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2687735200
 | 
			
		||||
value = 'Executing prepare()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 535572372
 | 
			
		||||
value = 'Executing version()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 436644691
 | 
			
		||||
value = 'Executing build()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1393316459
 | 
			
		||||
value = 'This package is already installed'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1267660189
 | 
			
		||||
value = 'Source can be updated, updating if required'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 21753247
 | 
			
		||||
value = 'Source found in cache, linked to destination'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 257354570
 | 
			
		||||
value = 'Compressing package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2952487371
 | 
			
		||||
value = 'Building package metadata'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1579384326
 | 
			
		||||
value = 'name'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3206337475
 | 
			
		||||
value = 'version'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1810056261
 | 
			
		||||
value = 'new'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1602912115
 | 
			
		||||
value = 'source'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2363381545
 | 
			
		||||
value = 'type'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3419504365
 | 
			
		||||
value = 'downloader'
 | 
			
		||||
							
								
								
									
										139
									
								
								translations/lure.ru.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								translations/lure.ru.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1228660974
 | 
			
		||||
value = 'Скачивание репозитория'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2779805870
 | 
			
		||||
value = 'Репозиторий уже обновлен'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1433222829
 | 
			
		||||
value = 'Показать скрипт для пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2470847050
 | 
			
		||||
value = 'Не удалось предложить просмотреть скрипт'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 855659503
 | 
			
		||||
value = 'Продолжить?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1997041569
 | 
			
		||||
value = 'Пользователь решил не продолжать после просмотра скрипта'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2347700990
 | 
			
		||||
value = 'Сборка пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2105058868
 | 
			
		||||
value = 'Скачивание файлов'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1884485082
 | 
			
		||||
value = 'Скачивание источника'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1519177982
 | 
			
		||||
value = 'Ошибка при сборке пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2125220917
 | 
			
		||||
value = 'Выберите, какие пакеты установить'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 812531604
 | 
			
		||||
value = 'Ошибка при запросе выбора пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1040982801
 | 
			
		||||
value = 'Обновление версии'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2235794125
 | 
			
		||||
value = 'Удалить зависимости сборки?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2205430948
 | 
			
		||||
value = 'Установка зависимостей сборки'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2522710805
 | 
			
		||||
value = 'Установка зависимостей'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3602138206
 | 
			
		||||
value = 'Ошибка при установке пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1057080231
 | 
			
		||||
value = 'Вызов функции package()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2687735200
 | 
			
		||||
value = 'Вызов функции prepare()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 535572372
 | 
			
		||||
value = 'Вызов функции version()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 436644691
 | 
			
		||||
value = 'Вызов функции build()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2562049386
 | 
			
		||||
value = "Архитектура процессора вашей системы не соответствует этому пакету. Продолжать несмотря на это?"
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3759891273
 | 
			
		||||
value = 'Функция package() необходима'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 4006393493
 | 
			
		||||
value = 'Массив checksums должен быть той же длины, что и sources'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1393316459
 | 
			
		||||
value = 'Этот пакет уже установлен'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1267660189
 | 
			
		||||
value = 'Источник может быть обновлен, если требуется, обновляем'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 21753247
 | 
			
		||||
value = 'Источник найден в кэше'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 257354570
 | 
			
		||||
value = 'Сжатие пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2952487371
 | 
			
		||||
value = 'Создание метаданных пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1579384326
 | 
			
		||||
value = 'название'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3206337475
 | 
			
		||||
value = 'версия'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1810056261
 | 
			
		||||
value = 'новая'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1602912115
 | 
			
		||||
value = 'источник'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2363381545
 | 
			
		||||
value = 'вид'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3419504365
 | 
			
		||||
value = 'протокол-скачивание'
 | 
			
		||||
							
								
								
									
										57
									
								
								upgrade.go
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								upgrade.go
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -23,8 +23,14 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/urfave/cli/v2"
 | 
			
		||||
	"go.arsenm.dev/logger/log"
 | 
			
		||||
	"go.arsenm.dev/lure/distro"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/db"
 | 
			
		||||
	"go.arsenm.dev/lure/internal/repos"
 | 
			
		||||
	"go.arsenm.dev/lure/manager"
 | 
			
		||||
	"go.arsenm.dev/lure/vercmp"
 | 
			
		||||
	"golang.org/x/exp/maps"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func upgradeCmd(c *cli.Context) error {
 | 
			
		||||
@@ -38,13 +44,18 @@ func upgradeCmd(c *cli.Context) error {
 | 
			
		||||
		log.Fatal("Unable to detect supported package manager on system").Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = repos.Pull(c.Context, gdb, cfg.Repos)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error pulling repos").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	updates, err := checkForUpdates(c.Context, mgr, info)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error checking for updates").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(updates) > 0 {
 | 
			
		||||
		installPkgs(c.Context, updates, mgr)
 | 
			
		||||
		installPkgs(c.Context, updates, nil, mgr, c.Bool("clean"))
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Info("There is nothing to do.").Send()
 | 
			
		||||
	}
 | 
			
		||||
@@ -52,41 +63,47 @@ func upgradeCmd(c *cli.Context) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]string, error) {
 | 
			
		||||
func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]db.Package, error) {
 | 
			
		||||
	installed, err := mgr.ListInstalled(nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var out []string
 | 
			
		||||
	for name, version := range installed {
 | 
			
		||||
		scripts, err := findPkg(name)
 | 
			
		||||
	pkgNames := maps.Keys(installed)
 | 
			
		||||
	found, _, err := repos.FindPkgs(gdb, pkgNames)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var out []db.Package
 | 
			
		||||
	for pkgName, pkgs := range found {
 | 
			
		||||
		if slices.Contains(cfg.IgnorePkgUpdates, pkgName) {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// since we're not using a glob, we can assume a single item
 | 
			
		||||
		script := scripts[0]
 | 
			
		||||
 | 
			
		||||
		vars, err := getBuildVars(ctx, script, info)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error getting build variables").Err(err).Send()
 | 
			
		||||
		if len(pkgs) > 1 {
 | 
			
		||||
			// Puts the element with the highest version first
 | 
			
		||||
			slices.SortFunc(pkgs, func(a, b db.Package) bool {
 | 
			
		||||
				return vercmp.Compare(a.Version, b.Version) == 1
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		repoVer := vars.Version
 | 
			
		||||
		if vars.Release != 0 && vars.Epoch == 0 {
 | 
			
		||||
			repoVer = fmt.Sprintf("%s-%d", vars.Version, vars.Release)
 | 
			
		||||
		} else if vars.Release != 0 && vars.Epoch != 0 {
 | 
			
		||||
			repoVer = fmt.Sprintf("%d:%s-%d", vars.Epoch, vars.Version, vars.Release)
 | 
			
		||||
		// First element is the package we want to install
 | 
			
		||||
		pkg := pkgs[0]
 | 
			
		||||
 | 
			
		||||
		repoVer := pkg.Version
 | 
			
		||||
		if pkg.Release != 0 && pkg.Epoch == 0 {
 | 
			
		||||
			repoVer = fmt.Sprintf("%s-%d", pkg.Version, pkg.Release)
 | 
			
		||||
		} else if pkg.Release != 0 && pkg.Epoch != 0 {
 | 
			
		||||
			repoVer = fmt.Sprintf("%d:%s-%d", pkg.Epoch, pkg.Version, pkg.Release)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c := vercmp(repoVer, version)
 | 
			
		||||
		c := vercmp.Compare(repoVer, installed[pkgName])
 | 
			
		||||
		if c == 0 || c == -1 {
 | 
			
		||||
			continue
 | 
			
		||||
		} else if c == 1 {
 | 
			
		||||
			out = append(out, name)
 | 
			
		||||
			out = append(out, pkg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -16,20 +16,21 @@
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package main
 | 
			
		||||
package vercmp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	_ "embed"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// vercmp compares two version strings.
 | 
			
		||||
// Compare compares two version strings.
 | 
			
		||||
// It returns 1 if v1 is greater,
 | 
			
		||||
// 0 if the versions are equal,
 | 
			
		||||
// and -1 if v2 is greater
 | 
			
		||||
func vercmp(v1, v2 string) int {
 | 
			
		||||
func Compare(v1, v2 string) int {
 | 
			
		||||
	if v1 == v2 {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2022 Arsen Musayelyan
 | 
			
		||||
 * 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
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package main
 | 
			
		||||
package vercmp
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
@@ -67,13 +67,13 @@ func TestVerCmp(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	for _, it := range table {
 | 
			
		||||
		t.Run(it.v1+"/"+it.v2, func(t *testing.T) {
 | 
			
		||||
			c := vercmp(it.v1, it.v2)
 | 
			
		||||
			c := Compare(it.v1, it.v2)
 | 
			
		||||
			if c != it.expected {
 | 
			
		||||
				t.Errorf("Expected %d, got %d", it.expected, c)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// Ensure opposite comparison gives opposite value
 | 
			
		||||
			c = -vercmp(it.v2, it.v1)
 | 
			
		||||
			c = -Compare(it.v2, it.v1)
 | 
			
		||||
			if c != it.expected {
 | 
			
		||||
				t.Errorf("Expected %d, got %d (opposite)", it.expected, c)
 | 
			
		||||
			}
 | 
			
		||||
		Reference in New Issue
	
	Block a user