125 Commits
Author SHA1 Message Date
EnumDev 3950c5f0d6 Dependency resolution improvements 2026-05-12 20:03:40 +03:00
EnumDev f3c2b9f0f6 Make 'ignore_packages' and 'ignore_paths' config options only work without rootdir 2026-05-10 19:45:07 +03:00
EnumDev dc8959c2f8 Add 'ignore_paths' config option 2026-05-10 17:26:38 +03:00
EnumDev 9122ffb9ca Improve hook system and add pre-transaction hooks 2026-05-10 16:43:58 +03:00
EnumDev a4e20b7d88 Add dependency version requirement resolution 2026-05-09 19:06:20 +03:00
EnumDev a03c9ad9ce Do not add fetch action for missing dependency again if it already exists 2026-04-26 15:13:25 +03:00
EnumDev 3f205ec412 Update GetEntryDependants function 2026-04-26 15:07:23 +03:00
EnumDev 2f9487affe Sort packages by name in error messages 2026-04-24 20:41:40 +03:00
EnumDev 596d8753a7 Allow removal of packages that depend on virtual packages with more than 1 provider 2026-04-24 20:41:16 +03:00
EnumDev 2de4eea7a4 Improve ignore_package functionality 2026-04-24 18:40:08 +03:00
EnumDev 5d0312856d Install missing dependencies on update 2026-04-20 17:17:54 +03:00
EnumDev 248359c02f Add 'GetPathOwners' function 2026-03-24 18:09:54 +02:00
EnumDev 1d81690bff Readd download size to database entry readable info 2026-03-14 11:08:42 +02:00
EnumDev 618916cd22 Remove flag to install all optional dependencies 2026-03-14 09:24:07 +02:00
EnumDev 8e4177c899 Show make dependant packages in database entry readable info 2026-03-14 09:18:47 +02:00
EnumDev 3f23580d16 Add sorting by installation date and last update date in 'list' subcommand 2026-02-24 20:51:04 +02:00
EnumDev 95dee0560a Add GPG signature verification 2026-02-24 18:50:50 +02:00
EnumDev 45721cfc43 Changes in printUsage function output 2026-02-07 18:05:15 +02:00
EnumDev 2aceb9efe8 Remove 'reinstall-all' flag 2026-02-07 15:07:10 +02:00
EnumDev bc3df83936 Reimplement runtime dependency resolution toggle 2026-02-07 15:03:38 +02:00
EnumDev f18c837bc2 Remove obsolete functions 2026-02-07 14:59:25 +02:00
EnumDev 76ad8e77c5 Greatly simplify dependency resolution 2026-02-07 14:52:14 +02:00
EnumDev 9ae2d3f893 Return virtual package providers in a sorted array 2026-02-07 11:51:05 +02:00
EnumDev 89c0c0892f Cleanup make and check dependencies only if package type is 'source' 2026-02-06 13:50:01 +02:00
EnumDev ad89650dc4 Sort packages by name correctly in 'list' subcommand 2026-02-05 18:08:34 +02:00
EnumDev 3a7adb27ab Replace 'human-readable' flag with 'show-bytes' 2026-02-05 18:02:02 +02:00
EnumDev f6aa01339e Show array length in readable info 2026-02-05 17:57:11 +02:00
EnumDev a29de2e52d Show virtual package providers in readable info 2026-02-05 17:52:40 +02:00
EnumDev 619335bbe6 Replace ResolveVirtualPackage function with GetDatabaseVirtualPackageEntry 2026-02-05 14:54:08 +02:00
EnumDev 8579c688d7 Use BFS for finding unused dependencies 2026-02-05 14:22:16 +02:00
EnumDev 48cec35f0c Cache installed virtual packages 2026-02-05 13:21:50 +02:00
EnumDev e19d64bf04 Replace an instance of GetPackage with GetPackageInfo 2026-02-04 14:50:25 +02:00
EnumDev ee80640153 Move installation reason to package local info 2026-02-04 14:48:52 +02:00
EnumDev e4d94e378f Move bpm locks to var/cache/bpm 2026-02-04 13:03:52 +02:00
EnumDev b37360ed82 Add package installation and last update dates 2026-02-03 21:25:33 +02:00
EnumDev ba0d288e45 Add check dependencies 2026-01-28 18:50:25 +02:00
EnumDev c1fa9d9474 Allow skipping tests during compilation in 'install' and 'update' subcommands 2026-01-28 13:02:37 +02:00
EnumDev 0742c81f2f Fix file ownership issues with package compilation as the root user 2026-01-28 12:33:26 +02:00
EnumDev dd20574470 More improvements for the readable info functions 2026-01-27 12:47:25 +02:00
EnumDev b3dc6e3e34 Add optional dependency descriptions 2026-01-26 13:25:25 +02:00
EnumDev 0a06808303 Improve readable info functions 2026-01-25 19:55:09 +02:00
EnumDev 9f8a20c671 Show arrays in multiple lines in readable info 2026-01-25 19:23:39 +02:00
EnumDev 2072e3856f Add 'maintainers' field to pkg.info 2026-01-25 17:23:53 +02:00
EnumDev 72f574209f Keep track of resolved virtual packages during dependency resolution 2026-01-22 12:35:08 +02:00
EnumDev 9c2d4595c7 Add 'jobs' flag to the 'compile' and 'install' subcommands 2026-01-19 17:42:47 +02:00
EnumDev 72f17f8954 Rework runtime dependency system 2026-01-06 19:35:42 +02:00
EnumDev fd646a820d Add 'runtime_depends' field to package info 2026-01-04 14:40:55 +02:00
EnumDev f23537ba7d Fix typo 2026-01-03 13:46:43 +02:00
EnumDev 6e9f16837c Fix split packages with the same name as the main package being removed from the database 2026-01-02 20:08:10 +02:00
EnumDev 271e87cb77 Exit with exit code 1 if path is not owned by any package 2026-01-02 18:54:09 +02:00
EnumDev 25ccc208d7 Change module names 2026-01-01 10:20:33 +02:00
EnumDev 9c7e20d27d Remove need for config module 2026-01-01 10:15:23 +02:00
EnumDev 6206fdbf43 Add 'BPM_PKG_URL' variable in package compilation 2025-12-31 09:10:36 +02:00
EnumDev 9e48d97dc8 Add 'allow-downgrades' flag to 'update' subcommand 2025-12-28 18:30:17 +02:00
EnumDev 08a4e24889 Add confirmation prompt for syncing in 'update' subcomamnd 2025-12-26 13:05:25 +02:00
EnumDev 4a32df6335 Fix possible panic when updating packages 2025-12-26 13:02:58 +02:00
EnumDev 548b2476bd Reduce memory usage for the 'list' subcommand 2025-12-26 12:34:08 +02:00
EnumDev a660039be1 Use package info instead of BPMPackage for the local package information map 2025-12-26 11:41:07 +02:00
EnumDev 44c2678ef9 Show optional dependencies after install or update operation 2025-12-17 15:36:08 +02:00
EnumDev 2a7c6ce5d9 Count size in signed 64-bit integers 2025-12-17 13:43:23 +02:00
EnumDev f98760d1dd Rearrange function order in utils.go 2025-12-17 12:18:07 +02:00
EnumDev 2ba78f16db Remove duplicate strings in packages slice from install and remove operations 2025-12-17 12:16:45 +02:00
EnumDev 04a71b573b Improve conflict checking 2025-12-17 12:07:44 +02:00
EnumDev aa229af8df Exit with exit code 1 if no valid subcommand is specified 2025-12-12 22:02:44 +02:00
EnumDev 9912b173ff Move pre-* package script execution into start of 'installPackage' function 2025-12-08 21:44:29 +02:00
EnumDev 879c832f60 Set BPM version using git 2025-12-08 17:04:15 +02:00
EnumDev 2c91a9332b Add 'vercmp' subcommand for package version comparisons in scripts 2025-12-08 16:08:55 +02:00
EnumDev 1895fabfb0 Do not strip files that use ELF as a container format 2025-12-05 20:16:11 +02:00
EnumDev 0dfb66c6c0 Bind mount etc/resolv.conf in package scripts for internet access 2025-12-05 18:02:54 +02:00
EnumDev dfdf38bad8 Replace variables in download filepath during compilation 2025-11-08 18:49:03 +02:00
EnumDev 52f25c8f3f Skip stripping if 'strip' binary is not in path 2025-11-01 12:08:26 +02:00
EnumDev 7e372eef91 Fix progress bar still overflowing 2025-10-30 13:57:40 +02:00
EnumDev b07a7f8778 Change printf message 2025-10-29 18:40:07 +02:00
EnumDev b2fc963389 Make progress bars look nicer 2025-10-29 18:38:52 +02:00
EnumDev b27e9fb0df Show optionally dependant packages in readable info 2025-10-29 18:02:42 +02:00
EnumDev 089bc61d58 Add more functionality to ignored packages 2025-10-28 19:50:46 +02:00
EnumDev fb821b3190 Remove bufio writer in databases.go 2025-10-28 19:20:36 +02:00
EnumDev 444b2032e2 Add automatic ELF binary stripping 2025-10-27 22:11:02 +02:00
EnumDev 70ba64ca6b Change file download message 2025-10-27 17:53:10 +02:00
EnumDev ed172f2805 Add progress bars 2025-10-26 20:06:55 +02:00
EnumDev f0ea57d500 Switch to fuzzy matching for searching through databases 2025-10-25 16:06:50 +03:00
EnumDev e923aefff9 Add 'sort' and 'reverse' flags to the 'list' subcommand 2025-10-25 15:20:23 +03:00
EnumDev c2d420d854 Show installed size for binary packages 2025-10-25 13:14:33 +03:00
EnumDev 2db01fb0d4 Disable tar verbose mode by default 2025-10-23 19:42:09 +03:00
EnumDev b451c5c320 Add shorthand for 'version' subcommand 2025-10-19 16:28:52 +03:00
EnumDev 27427d5654 Update version number to 0.6.1 2025-10-19 16:28:04 +03:00
EnumDev 04e966fef5 Hide URL and license from readable info if empty 2025-10-19 16:26:57 +03:00
EnumDev 8a7519628f Show output architecture in readable info 2025-10-19 16:24:05 +03:00
EnumDev 8986e24334 Add 'database' flag to the 'list' subcommand 2025-10-19 14:53:37 +03:00
EnumDev f066e26dc2 Add new flags to the 'list' subcommand 2025-10-19 14:28:14 +03:00
EnumDev bee23e2f56 Improve local package querying 2025-10-18 09:51:36 +03:00
EnumDev d56005e63e Improve database entry querying 2025-10-18 09:16:15 +03:00
EnumDev c9e16d6df1 Add 'keep' flag 2025-10-17 21:58:26 +03:00
EnumDev 45c536b326 Allow additional GOFLAGS in makefile 2025-10-09 16:42:03 +03:00
EnumDev 705aec8e6e Resolve virtual packages in remove subcommand 2025-10-09 14:14:14 +03:00
EnumDev 34fb7ce1d6 Ensure virtual packages are sorted alphabetically 2025-10-09 14:08:22 +03:00
EnumDev fee0e6a96f Make cleanup flag in the remove subcommand use the config option for
make dependencies
2025-10-09 12:37:51 +03:00
EnumDev c5f48becf8 Change remove subcommand behaviour to show error when breaking installed package dependencies 2025-10-09 12:35:58 +03:00
EnumDev ac1ba2ada3 Improve confirmation prompts 2025-10-08 20:46:44 +03:00
EnumDev 5fb8211933 Validate package names using regex 2025-10-07 20:50:08 +03:00
EnumDev 4110ad9c5c Create different output directory for each split package during
compilation
2025-10-07 19:00:17 +03:00
EnumDev dcff59d560 Add 'all' flag to cleanup subcommand 2025-10-07 18:24:14 +03:00
EnumDev 1594eec5ce Remove subcommand will now run without specifying any packages 2025-10-07 18:20:48 +03:00
EnumDev 0c30d85589 The 'cleanup' flag for the remove subcommand will now use 'n' for its
shorthand form similar to the 'cleanup' subcommand
2025-10-07 18:19:30 +03:00
EnumDev 768c75c9bd Cleanup subcommand will now remove optional dependencies. Optional dependencies will now be installed as manual when passing the 'optional' flag to the install subcommand 2025-10-07 18:17:42 +03:00
EnumDev abc6674b97 Run 'cleanup' subcommand when 'compile' subcommand encounters an error 2025-10-06 20:10:20 +03:00
EnumDev 1d858076fd Add 'yes' flag to cleanup subcommand 2025-10-06 20:09:36 +03:00
EnumDev 5909370407 Improve operation summary 2025-10-06 16:38:03 +03:00
EnumDev 5a78fd3002 Show source package contents during package installation 2025-10-06 13:33:18 +03:00
EnumDev a7c03c272e Add back file descriptor flag for 'compile' subcommand 2025-10-05 19:59:27 +03:00
EnumDev 8ba209cf35 Improve 'search' subcommand 2025-10-05 19:46:57 +03:00
EnumDev 6f27f39065 Rename 'file' subcommand to 'owner' and improve it 2025-10-05 18:00:44 +03:00
EnumDev 91479e58f1 Rename 'info' subcommand to 'query' and add shorthand for all
subcommands
2025-10-05 16:56:04 +03:00
EnumDev b6d055a38d Redo bpm flag and subcommand system 2025-10-05 16:38:59 +03:00
EnumDev 0c754a7e0f Change 'extract_to_bpm_source' field to 'extract_to' and 'clone_to' 2025-10-05 12:14:05 +03:00
EnumDev eeb9a01e31 Add file locks to prevent multiple processes at the same time 2025-10-04 17:24:20 +03:00
EnumDev 53e08ce7f9 Ensure package downloads stay in the temporary directory 2025-10-04 09:43:44 +03:00
EnumDev 6f124e0586 Improve package info parsing 2025-09-29 10:34:47 +03:00
EnumDev 93921d4093 Use drone/envsubst module for bash variable expansion 2025-09-28 12:38:47 +03:00
EnumDev bc89db8cef Add 'downloads' field in pkg.info 2025-09-26 16:10:07 +03:00
EnumDev ea6d722fa8 Fix IDE warnings 2025-09-26 10:23:51 +03:00
EnumDev 264a0f87ba Replace .compilation-options 'ARCH' field with 'output_architecture' field in pkg.info 2025-09-26 10:10:48 +03:00
EnumDev 926b0e35fc Update version number to 0.6.0 2025-09-25 14:08:46 +03:00
EnumDev a4d1365e55 Change to yaml only database format 2025-09-04 19:14:30 +03:00
EnumDev bc3fe6fd7e Allow writing output package paths to file descriptor after compilation 2025-09-04 11:58:30 +03:00
20 changed files with 4410 additions and 1755 deletions
+2 -1
View File
@@ -7,12 +7,13 @@ SYSCONFDIR := $(PREFIX)/etc
GO ?= go GO ?= go
# Build-time variables # Build-time variables
VERSION ?= $(shell git describe --tags --dirty)
ROOT_COMPILATION_UID ?= 65534 ROOT_COMPILATION_UID ?= 65534
ROOT_COMPILATION_GID ?= 65534 ROOT_COMPILATION_GID ?= 65534
build: build:
mkdir -p build mkdir -p build
cd src/bpm; $(GO) build -ldflags "-w -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationUID=$(ROOT_COMPILATION_UID)' -X 'git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib.rootCompilationGID=$(ROOT_COMPILATION_GID)'" -o ../../build/bpm git.enumerated.dev/bubble-package-manager/bpm/src/bpm cd src/bpm; $(GO) build $(GOFLAGS) -ldflags "-w -X 'main.BpmVersion=$(VERSION)' -X 'github.com/EnumeratedDev/bpm/src/bpmlib.rootCompilationUID=$(ROOT_COMPILATION_UID)' -X 'github.com/EnumeratedDev/bpm/src/bpmlib.rootCompilationGID=$(ROOT_COMPILATION_GID)'" -o ../../build/bpm bpm
install: build/bpm config/ install: build/bpm config/
# Create directory # Create directory
+1
View File
@@ -1,4 +1,5 @@
ignore_packages: [] ignore_packages: []
show_source_package_contents: always
cleanup_make_dependencies: true cleanup_make_dependencies: true
databases: databases:
- name: example-database - name: example-database
+17 -8
View File
@@ -1,14 +1,23 @@
module git.enumerated.dev/bubble-package-manager/bpm/src/bpm module bpm
go 1.23 go 1.24.0
toolchain go1.23.7
require git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib v0.5.0
replace git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib => ../bpmlib
require ( require (
github.com/EnumeratedDev/bpm/src/bpmlib v0.0.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/spf13/pflag v1.0.10
)
replace github.com/EnumeratedDev/bpm/src/bpmlib => ../bpmlib
require (
github.com/drone/envsubst v1.0.3 // indirect
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/schollz/progressbar/v3 v3.18.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.9.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+60
View File
@@ -1,5 +1,65 @@
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
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/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g= github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+1288 -548
View File
File diff suppressed because it is too large Load Diff
+452 -77
View File
@@ -1,22 +1,35 @@
package bpmlib package bpmlib
import ( import (
"crypto/sha256"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io"
"io/fs"
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"runtime"
"slices"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"github.com/drone/envsubst"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
var rootCompilationUID = "65534" var rootCompilationUID = "65534"
var rootCompilationGID = "65534" var rootCompilationGID = "65534"
func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bool) (outputBpmPackages map[string]string, err error) { func CompileSourcePackage(archiveFilename, outputDirectory string, compilationJobs int, skipChecks, keepCompilationFiles, verbose bool) (outputBpmPackages map[string]string, err error) {
// Set compilation jobs
if compilationJobs <= 0 || compilationJobs > runtime.NumCPU() {
compilationJobs = runtime.NumCPU()
}
// Initialize map // Initialize map
outputBpmPackages = make(map[string]string) outputBpmPackages = make(map[string]string)
@@ -31,12 +44,6 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
return nil, errors.New("cannot compile a non-source package") return nil, errors.New("cannot compile a non-source package")
} }
// Read compilation options file in current directory
compilationOptions, err := readCompilationOptionsFile()
if err != nil {
return nil, err
}
// Get HOME directory // Get HOME directory
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
@@ -122,30 +129,50 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
return nil, err return nil, err
} }
// Download files
err = downloadPackageFiles(bpmpkg.PkgInfo, tempDirectory, verbose)
if err != nil {
return nil, err
}
// Setup environment for commands // Setup environment for commands
env := os.Environ() env := os.Environ()
env = append(env, "HOME="+tempDirectory) env = append(env, "HOME="+tempDirectory)
env = append(env, "BPM_WORKDIR="+tempDirectory) env = append(env, "BPM_WORKDIR="+tempDirectory)
env = append(env, "BPM_SOURCE="+path.Join(tempDirectory, "source")) env = append(env, "BPM_SOURCE="+path.Join(tempDirectory, "source"))
env = append(env, "BPM_OUTPUT="+path.Join(tempDirectory, "output"))
env = append(env, "BPM_PKG_NAME="+bpmpkg.PkgInfo.Name) env = append(env, "BPM_PKG_NAME="+bpmpkg.PkgInfo.Name)
env = append(env, "BPM_PKG_VERSION="+bpmpkg.PkgInfo.Version) env = append(env, "BPM_PKG_VERSION="+bpmpkg.PkgInfo.Version)
env = append(env, "BPM_PKG_REVISION="+strconv.Itoa(bpmpkg.PkgInfo.Revision)) env = append(env, "BPM_PKG_REVISION="+strconv.Itoa(bpmpkg.PkgInfo.Revision))
// Check for architecture override in compilation options env = append(env, "BPM_PKG_URL="+bpmpkg.PkgInfo.Url)
if val, ok := compilationOptions["ARCH"]; ok { env = append(env, "BPM_PKG_ARCH="+bpmpkg.PkgInfo.OutputArch)
env = append(env, "BPM_PKG_ARCH="+val) env = append(env, "BPM_JOBS="+strconv.Itoa(compilationJobs))
} else {
env = append(env, "BPM_PKG_ARCH="+GetArch())
}
env = append(env, CompilationBPMConfig.CompilationEnvironment...) env = append(env, CompilationBPMConfig.CompilationEnvironment...)
// Set common flags used for limiting job count
makeflags := ""
env = slices.DeleteFunc(env, func(s string) bool {
if strings.HasPrefix(s, "MAKEFLAGS=") {
makeflags = s + " "
return true
} else if strings.HasPrefix(s, "CMAKE_BUILD_PARALLEL_LEVEL=") {
return true
} else if strings.HasPrefix(s, "CARGO_BUILD_JOBS=") {
return true
}
return false
})
env = append(env, makeflags+"-j"+strconv.Itoa(compilationJobs))
env = append(env, "CMAKE_BUILD_PARALLEL_LEVEL="+strconv.Itoa(compilationJobs))
env = append(env, "CARGO_BUILD_JOBS="+strconv.Itoa(compilationJobs))
// Execute prepare and build functions in source.sh script // Execute prepare and build functions in source.sh script
cmd := exec.Command("bash", "-c", cmd := exec.Command("bash", "-c",
"set -a\n"+ // Source and export functions and variables in source.sh script "set -a\n"+ // Source and export functions and variables in source.sh script
". \"${BPM_WORKDIR}\"/source.sh\n"+ ". \"${BPM_WORKDIR}\"/source.sh\n"+
"set +a\n"+ "set +a\n"+
"[[ $(type -t prepare) == \"function\" ]] && { echo \"Running prepare() function\"; bash -e -c 'cd \"$BPM_WORKDIR\" && prepare' || exit 1; }\n"+ // Run prepare() function if it exists "[[ $(type -t prepare) == \"function\" ]] && { echo \"Running prepare() function...\"; bash -e -c 'cd \"$BPM_WORKDIR\" && prepare' || exit 1; }\n"+ // Run prepare() function if it exists
"[[ $(type -t build) == \"function\" ]] && { echo \"Running build() function\"; bash -e -c 'cd \"$BPM_SOURCE\" && build' || exit 1; }\n"+ // Run build() function if it exists "[[ $(type -t build) == \"function\" ]] && { echo \"Running build() function...\"; bash -e -c 'cd \"$BPM_SOURCE\" && build' || exit 1; }\n"+ // Run build() function if it exists
"exit 0") "exit 0")
cmd.Dir = tempDirectory cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -166,7 +193,7 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
"set -a\n"+ // Source and export functions and variables in source.sh script "set -a\n"+ // Source and export functions and variables in source.sh script
". \"${BPM_WORKDIR}\"/source.sh\n"+ ". \"${BPM_WORKDIR}\"/source.sh\n"+
"set +a\n"+ "set +a\n"+
"[[ $(type -t check) == \"function\" ]] && { echo \"Running check() function\"; bash -e -c 'cd \"$BPM_SOURCE\" && check' || exit 1; }\n"+ // Run check() function if it exists "[[ $(type -t check) == \"function\" ]] && { echo \"Running check() function...\"; bash -e -c 'cd \"$BPM_SOURCE\" && check' || exit 1; }\n"+ // Run check() function if it exists
"exit 0") "exit 0")
cmd.Dir = tempDirectory cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -188,6 +215,21 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
packagesToCompile = append(packagesToCompile, bpmpkg.PkgInfo) packagesToCompile = append(packagesToCompile, bpmpkg.PkgInfo)
} }
// Create output directories for each package
for _, pkg := range packagesToCompile {
// Create new output directory
err = os.Mkdir(path.Join(tempDirectory, "output_"+pkg.Name), 0755)
if err != nil {
return nil, err
}
// Change output directory owner
err = os.Chown(path.Join(tempDirectory, "output_"+pkg.Name), uid, gid)
if err != nil {
return nil, err
}
}
// Compile each package // Compile each package
for _, pkg := range packagesToCompile { for _, pkg := range packagesToCompile {
// Get package function name // Get package function name
@@ -196,38 +238,102 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
packageFunctionName = "package_" + pkg.Name packageFunctionName = "package_" + pkg.Name
} }
// Remove output directory if it already exists
if _, err := os.Stat(path.Join(tempDirectory, "output")); err == nil {
err := os.RemoveAll(path.Join(tempDirectory, "output"))
if err != nil {
return nil, err
}
}
// Create new output directory
err = os.Mkdir(path.Join(tempDirectory, "output"), 0755)
if err != nil {
return nil, err
}
// Change output directory owner
err = os.Chown(path.Join(tempDirectory, "output"), uid, gid)
if err != nil {
return nil, err
}
// Execute package function in source.sh script and generate package file list // Execute package function in source.sh script and generate package file list
cmd = exec.Command("bash", "-c", cmd = exec.Command("bash", "-c",
"set -a\n"+ // Source and export functions and variables in source.sh script "set -a\n"+ // Source and export functions and variables in source.sh script
". \"${BPM_WORKDIR}\"/source.sh\n"+ ". \"${BPM_WORKDIR}\"/source.sh\n"+
"set +a\n"+ "set +a\n"+
"echo \"Running "+packageFunctionName+"() function\"\n"+ "echo \"Running "+packageFunctionName+"() function...\"\n"+
"( cd \"$BPM_SOURCE\" && fakeroot -s \"$BPM_WORKDIR\"/fakeroot_file bash -e -c '"+packageFunctionName+"' ) || exit 1\n"+ // Run package() function "( cd \"$BPM_SOURCE\" && fakeroot -s \"$BPM_WORKDIR\"/fakeroot_file bash -e -c '"+packageFunctionName+"' ) || exit 1\n") // Run package() function
"fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file find \"$BPM_OUTPUT\" -mindepth 1 -printf \"%P %#m %U %G %s\\n\" > \"$BPM_WORKDIR\"/pkg.files") // Create package file list
cmd.Dir = tempDirectory cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = env cmd.Env = append(env, "BPM_OUTPUT="+path.Join(tempDirectory, "output_"+pkg.Name))
if os.Getuid() == 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}
err = cmd.Run()
if err != nil {
return nil, err
}
// Strip all ELF binaries
if !slices.Contains(pkg.Options, "nostrip") {
if _, err := exec.LookPath("strip"); err != nil {
fmt.Println("Skip stripping ELF binaries. 'strip' not in path")
} else {
fmt.Println("Stripping ELF binaries...")
err = filepath.WalkDir(path.Join(tempDirectory, "output_"+pkg.Name), func(path string, d fs.DirEntry, err error) error {
cmd = exec.Command("file", "-bi", path)
cmd.Stderr = os.Stderr
output, err := cmd.Output()
if err != nil {
return err
}
mimetype := strings.Split(string(output), ";")[0]
if strings.HasPrefix(mimetype, "application/x-executable") {
cmd := exec.Command("strip", path)
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return err
}
if verbose {
fmt.Printf("Stripped %s (%s)\n", path, mimetype)
}
} else if strings.HasPrefix(mimetype, "application/x-pie-executable") || strings.HasPrefix(mimetype, "application/x-sharedlib") {
cmd = exec.Command("file", "-b", path)
cmd.Stderr = os.Stderr
output, err = cmd.Output()
if err != nil {
return err
}
// Skip files that use ELF as a container format
if strings.Contains(string(output), "no machine") {
return nil
}
cmd := exec.Command("strip", "--strip-unneeded", path)
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return err
}
if verbose {
fmt.Printf("Stripped %s (%s)\n", path, mimetype)
}
} else if strings.HasPrefix(mimetype, "application/x-archive") {
cmd := exec.Command("strip", "--strip-debug", path)
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return err
}
if verbose {
fmt.Printf("Stripped %s (%s)\n", path, mimetype)
}
}
return nil
})
if err != nil {
return nil, err
}
}
}
// Generate package file list
fmt.Println("Generating package file list...")
cmd = exec.Command("bash", "-c", "fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file find \"$BPM_OUTPUT\" -mindepth 1 -printf \"%P %#m %U %G %s\\n\" > \"$BPM_WORKDIR\"/pkg.files")
cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(env, "BPM_OUTPUT="+path.Join(tempDirectory, "output_"+pkg.Name))
if os.Getuid() == 0 { if os.Getuid() == 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)} cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
@@ -238,7 +344,8 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
} }
// Create gzip-compressed archive for the package files // Create gzip-compressed archive for the package files
cmd = exec.Command("bash", "-c", "find output -printf \"%P\\n\" | fakeroot -i \"$BPM_WORKDIR\"/fakeroot_file tar -czf files.tar.gz --no-recursion -C output -T -") fmt.Println("Generating compressed file archive...")
cmd = exec.Command("bash", "-c", fmt.Sprintf("find %s -printf \"%%P\\n\" | fakeroot -i %s/fakeroot_file tar -czf files.tar.gz --no-recursion -C %s -T -", "output_"+pkg.Name, tempDirectory, "output_"+pkg.Name))
cmd.Dir = tempDirectory cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
@@ -259,14 +366,12 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
pkgInfo.Type = "binary" pkgInfo.Type = "binary"
// Set package architecture // Set package architecture
if val, ok := compilationOptions["ARCH"]; ok { pkgInfo.Arch = pkg.OutputArch
pkgInfo.Arch = val pkgInfo.OutputArch = ""
} else {
pkgInfo.Arch = GetArch()
}
// Remove split package field // Remove split package and downloads fields
pkgInfo.SplitPackages = nil pkgInfo.SplitPackages = nil
pkgInfo.Downloads = nil
// Marshal package info // Marshal package info
pkgInfoBytes, err := yaml.Marshal(pkgInfo) pkgInfoBytes, err := yaml.Marshal(pkgInfo)
@@ -293,6 +398,7 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
bpmArchiveFiles = append(bpmArchiveFiles, packageScripts...) // Package scripts bpmArchiveFiles = append(bpmArchiveFiles, packageScripts...) // Package scripts
// Create final BPM archive // Create final BPM archive
fmt.Println("Generating final BPM archive...")
cmd = exec.Command("bash", "-c", "tar -cf final-archive.bpm --owner=0 --group=0 -C \"$BPM_WORKDIR\" "+strings.Join(bpmArchiveFiles, " ")) cmd = exec.Command("bash", "-c", "tar -cf final-archive.bpm --owner=0 --group=0 -C \"$BPM_WORKDIR\" "+strings.Join(bpmArchiveFiles, " "))
cmd.Dir = tempDirectory cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
@@ -332,52 +438,321 @@ func CompileSourcePackage(archiveFilename, outputDirectory string, skipChecks bo
return nil, err return nil, err
} }
// Remove output directory
err = os.RemoveAll(path.Join(tempDirectory, "output_"+pkg.Name))
if err != nil {
return nil, err
}
outputBpmPackages[pkgInfo.Name] = outputFilename outputBpmPackages[pkgInfo.Name] = outputFilename
} }
// Delete temporary directory
if !keepCompilationFiles {
os.RemoveAll(tempDirectory)
}
return outputBpmPackages, nil return outputBpmPackages, nil
} }
func readCompilationOptionsFile() (options map[string]string, err error) { func downloadPackageFiles(pkgInfo *PackageInfo, tempDirectory string, verbose bool) error {
// Initialize options map // Get UID and GID to use for compilation
options = make(map[string]string) var uid, gid int
if os.Getuid() == 0 {
// Check if file compilation options file exists _uid, err := strconv.ParseInt(rootCompilationUID, 10, 32)
stat, err := os.Stat(".compilation-options")
if err != nil { if err != nil {
return nil, nil return fmt.Errorf("could not convert UID '%s' to int", rootCompilationUID)
} }
_gid, err := strconv.ParseInt(rootCompilationGID, 10, 32)
// Ensure it is a regular file
if !stat.Mode().IsRegular() {
return nil, fmt.Errorf("%s is not a regular file", stat.Name())
}
// Read file data
data, err := os.ReadFile(stat.Name())
if err != nil { if err != nil {
return nil, err return fmt.Errorf("could not convert GID '%s' to int", rootCompilationGID)
}
uid = int(_uid)
gid = int(_gid)
} else {
uid = os.Getuid()
gid = os.Getgid()
} }
for _, line := range strings.Split(string(data), "\n") { for _, download := range pkgInfo.Downloads {
// Trim line // Replace variables
line = strings.TrimSpace(line) replaceVars := func(s string) string {
switch s {
case "BPM_PKG_NAME":
return pkgInfo.Name
case "BPM_PKG_VERSION":
return pkgInfo.Version
case "BPM_PKG_URL":
return pkgInfo.Url
case "BPM_SOURCE":
return path.Join(tempDirectory, "source/")
default:
return ""
}
}
// Skip empty lines downloadUrl, err := envsubst.Eval(strings.TrimSpace(download.Url), replaceVars)
if line == "" { if err != nil {
return err
}
filepath, err := envsubst.Eval(strings.TrimSpace(download.Filepath), replaceVars)
if err != nil {
return err
}
extractTo, err := envsubst.Eval(strings.TrimSpace(download.ExtractTo), replaceVars)
if err != nil {
return err
}
cloneTo, err := envsubst.Eval(strings.TrimSpace(download.CloneTo), replaceVars)
if err != nil {
return err
}
// Make relative paths absolute
if extractTo != "" && extractTo[0] != '/' {
extractTo = path.Join(tempDirectory, extractTo)
}
// Make relative paths absolute
if cloneTo != "" && cloneTo[0] != '/' {
cloneTo = path.Join(tempDirectory, cloneTo)
}
switch download.Type {
case "", "file":
if filepath == "" {
filepath = path.Join(tempDirectory, path.Base(downloadUrl))
}
if filepath[0] != '/' {
filepath = path.Join(tempDirectory, filepath)
}
err := downloadFile("Downloading file "+path.Base(filepath), downloadUrl, filepath, 0644)
if err != nil {
return err
}
if download.Checksum != "skip" {
f, err := os.Open(filepath)
if err != nil {
return err
}
defer f.Close()
h := sha256.New()
_, err = io.Copy(h, f)
if err != nil {
return err
}
if hex.EncodeToString(h.Sum(nil)) != download.Checksum {
fmt.Printf("Downloaded file checksum: %x\n", h.Sum(nil))
return fmt.Errorf("downloaded file checksums did not match")
}
} else {
fmt.Println("Skipping checksum checking...")
}
// Change downloaded file ownership
err = os.Chown(filepath, uid, gid)
if err != nil {
return err
}
if !download.NoExtract && (strings.Contains(filepath, ".tar") || strings.HasSuffix(filepath, ".tgz")) {
cmd := exec.Command("tar", "xf", filepath, "--strip-components="+strconv.Itoa(download.ExtractStripComponents))
if verbose {
cmd.Args[1] = "xvf"
}
cmd.Dir = tempDirectory
if os.Getuid() == 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}
if extractTo != "" {
err := os.MkdirAll(extractTo, 0755)
if err != nil {
return err
}
cmd.Args = append(cmd.Args, "-C", extractTo)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
} else if !download.NoExtract && strings.HasSuffix(filepath, ".zip") {
cmd := exec.Command("unzip", filepath)
cmd.Dir = tempDirectory
if os.Getuid() == 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}
if extractTo != "" {
err := os.MkdirAll(extractTo, 0755)
if err != nil {
return err
}
cmd.Args = append(cmd.Args, "-d", extractTo)
} else {
err := os.Mkdir(path.Join(tempDirectory, strings.TrimSuffix(path.Base(filepath), ".zip")), 0755)
if err != nil {
return err
}
cmd.Args = append(cmd.Args, "-d", path.Join(tempDirectory, strings.TrimSuffix(path.Base(filepath), ".zip")))
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
}
case "git":
// Replace variables in git branch
gitBranch := download.GitBranch
gitBranch, err := envsubst.Eval(gitBranch, func(s string) string {
switch s {
case "BPM_PKG_NAME":
return pkgInfo.Name
case "BPM_PKG_VERSION":
return pkgInfo.Version
case "BPM_PKG_URL":
return pkgInfo.Url
default:
return ""
}
})
if err != nil {
return err
}
cmd := exec.Command("git", "clone", "--depth=1", downloadUrl)
cmd.Dir = tempDirectory
if os.Getuid() == 0 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}
if gitBranch != "" {
cmd.Args = slices.Insert(cmd.Args, len(cmd.Args)-1, "--branch="+gitBranch)
}
if cloneTo != "" {
err := os.MkdirAll(cloneTo, 0755)
if err != nil {
return err
}
cmd.Args = append(cmd.Args, cloneTo)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return err
}
if download.Checksum != "skip" {
cmd := exec.Command("git", "rev-parse", "HEAD")
if cloneTo != "" {
cmd.Dir = cloneTo
} else {
cmd.Dir = path.Join(tempDirectory, strings.TrimSuffix(path.Base(downloadUrl), ".git"))
}
branchChecksum, err := cmd.Output()
if err != nil {
return err
}
if strings.TrimSpace(string(branchChecksum)) != download.Checksum {
fmt.Printf("Git branch checksum: %s\n", strings.TrimSpace(string(branchChecksum)))
return fmt.Errorf("Cloned git repository checksum did not match")
}
} else {
fmt.Println("Skipping checksum checking...")
}
default:
return fmt.Errorf("unknown download type (%s)", download.Type)
}
}
return nil
}
func showPackageFiles(archiveFilename string) error {
// Read BPM archive
bpmpkg, err := ReadPackage(archiveFilename)
if err != nil {
return err
}
// Ensure package type is 'source'
if bpmpkg.PkgInfo.Type != "source" {
return fmt.Errorf("cannot compile a non-source package")
}
printTarballContent := func(filename string) error {
// Get tarball reader for file
reader, err := readTarballFile(archiveFilename, filename)
if err != nil {
return err
}
// Read file content
data, err := io.ReadAll(reader.tarReader)
if err != nil {
return err
}
// Print file name and content
fmt.Println("==============================")
fmt.Printf("%s: %s\n", bpmpkg.PkgInfo.Name, filename)
fmt.Println("==============================")
fmt.Printf("%s\n", data)
// Close reader
err = reader.file.Close()
if err != nil {
return err
}
return nil
}
// Print pkg.info content
err = printTarballContent("pkg.info")
if err != nil {
return err
}
// Print source files contents
files, err := listTarballContent(archiveFilename)
if err != nil {
return err
}
for _, file := range files {
if !strings.HasPrefix(file, "source-files/") {
continue continue
} }
// Split line // Print source file content
split := strings.SplitN(line, "=", 2) err = printTarballContent(file)
if err != nil {
// Throw error if line isn't valid return err
if len(split) < 2 { }
return nil, fmt.Errorf("invalid line in compilation-options file: '%s'", line)
} }
options[split[0]] = split[1] // Print source.sh content
err = printTarballContent("source.sh")
if err != nil {
return err
} }
return options, nil return nil
} }
+12 -1
View File
@@ -8,12 +8,22 @@ import (
type MainBPMConfigStruct struct { type MainBPMConfigStruct struct {
IgnorePackages []string `yaml:"ignore_packages"` IgnorePackages []string `yaml:"ignore_packages"`
IgnorePaths []string `yaml:"ignore_paths"`
ShowSourcePackageContents string `yaml:"show_source_package_contents"`
CleanupMakeDependencies bool `yaml:"cleanup_make_dependencies"` CleanupMakeDependencies bool `yaml:"cleanup_make_dependencies"`
Databases []*BPMDatabase `yaml:"databases"` Databases []configDatabase `yaml:"databases"`
}
type configDatabase struct {
Name string `yaml:"name"`
Source string `yaml:"source"`
VerificationLevel string `yaml:"verification_level"`
Disabled *bool `yaml:"disabled"`
} }
type CompilationBPMConfigStruct struct { type CompilationBPMConfigStruct struct {
PrivilegeEscalatorCmd string `yaml:"privilege_escalator_cmd"` PrivilegeEscalatorCmd string `yaml:"privilege_escalator_cmd"`
CompilationJobs int `yaml:"compilation_jobs"`
CompilationEnvironment []string `yaml:"compilation_env"` CompilationEnvironment []string `yaml:"compilation_env"`
} }
@@ -25,6 +35,7 @@ func ReadConfig() (err error) {
// Set default config options // Set default config options
MainBPMConfig = MainBPMConfigStruct{ MainBPMConfig = MainBPMConfigStruct{
ShowSourcePackageContents: "always",
CleanupMakeDependencies: true, CleanupMakeDependencies: true,
} }
+403 -88
View File
@@ -1,40 +1,55 @@
package bpmlib package bpmlib
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path" "path"
"slices"
"strconv"
"strings" "strings"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
type VerificationLevel int
const (
VerificationLevelNone VerificationLevel = iota
VerificationLevelAll
VerificationLevelTrusted
)
type BPMDatabase struct { type BPMDatabase struct {
Name string `yaml:"name"` DatabaseVersion int `yaml:"database_version"`
Source string `yaml:"source"` Entries map[string]*BPMDatabaseEntry `yaml:"entries"`
Disabled *bool `yaml:"disabled"` VirtualPackages map[string][]*BPMDatabaseEntry
Entries map[string]*BPMDatabaseEntry Name string
VirtualPackages map[string][]string VerificationLevel VerificationLevel
Source string
} }
type BPMDatabaseEntry struct { type BPMDatabaseEntry struct {
Info *PackageInfo `yaml:"info"` Info *PackageInfo `yaml:"info"`
Download string `yaml:"download"` Filepath string `yaml:"filepath"`
DownloadSize uint64 `yaml:"download_size"` DownloadSize int64 `yaml:"download_size"`
InstalledSize uint64 `yaml:"installed_size"` InstalledSize int64 `yaml:"installed_size"`
Database *BPMDatabase Database *BPMDatabase
} }
var BPMDatabases = make(map[string]*BPMDatabase)
func (db *BPMDatabase) ContainsPackage(pkg string) bool { func (db *BPMDatabase) ContainsPackage(pkg string) bool {
_, ok := db.Entries[pkg] _, ok := db.Entries[pkg]
return ok return ok
} }
func (db *BPMDatabase) ReadLocalDatabase() error { func (db *configDatabase) ReadLocalDatabase() error {
dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb" dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb"
if _, err := os.Stat(dbFile); err != nil { if _, err := os.Stat(dbFile); err != nil {
return nil return nil
@@ -45,37 +60,35 @@ func (db *BPMDatabase) ReadLocalDatabase() error {
return err return err
} }
data := string(bytes) // Unmarshal yaml
for _, b := range strings.Split(data, "---") { database := &BPMDatabase{}
entry := BPMDatabaseEntry{ err = yaml.Unmarshal(bytes, database)
Info: &PackageInfo{
Name: "",
Description: "",
Version: "",
Revision: 1,
Url: "",
License: "",
Arch: "",
Type: "",
Keep: make([]string, 0),
Depends: make([]string, 0),
MakeDepends: make([]string, 0),
OptionalDepends: make([]string, 0),
Conflicts: make([]string, 0),
Provides: make([]string, 0),
},
Download: "",
DownloadSize: 0,
InstalledSize: 0,
Database: db,
}
err := yaml.Unmarshal([]byte(b), &entry)
if err != nil { if err != nil {
return err return err
} }
// Create database entries // Initialize struct values
database.VirtualPackages = make(map[string][]*BPMDatabaseEntry)
database.Name = db.Name
switch db.VerificationLevel {
case "0", "none":
database.VerificationLevel = VerificationLevelNone
case "1", "all":
database.VerificationLevel = VerificationLevelAll
case "2", "trusted":
database.VerificationLevel = VerificationLevelTrusted
default:
database.VerificationLevel = VerificationLevelAll
}
database.Source = db.Source
for entryName, entry := range database.Entries {
entry.Database = database
if entry.Info.IsSplitPackage() { if entry.Info.IsSplitPackage() {
delete(database.Entries, entryName)
// Handle split packages
for _, splitPkg := range entry.Info.SplitPackages { for _, splitPkg := range entry.Info.SplitPackages {
// Turn split package into json data // Turn split package into json data
splitPkgJson, err := yaml.Marshal(splitPkg) splitPkgJson, err := yaml.Marshal(splitPkg)
@@ -101,35 +114,33 @@ func (db *BPMDatabase) ReadLocalDatabase() error {
splitPkgClone.Url = entry.Info.Url splitPkgClone.Url = entry.Info.Url
// Create entry for split package // Create entry for split package
db.Entries[splitPkg.Name] = &BPMDatabaseEntry{ database.Entries[splitPkg.Name] = &BPMDatabaseEntry{
Info: &splitPkgClone, Info: &splitPkgClone,
Download: entry.Download, Filepath: entry.Filepath,
DownloadSize: entry.DownloadSize, DownloadSize: entry.DownloadSize,
InstalledSize: 0, InstalledSize: 0,
Database: db, Database: database,
} }
// Add virtual packages to database // Add virtual packages to database
for _, p := range splitPkg.Provides { for _, p := range splitPkg.Provides {
db.VirtualPackages[p] = append(db.VirtualPackages[p], splitPkg.Name) database.VirtualPackages[p] = append(database.VirtualPackages[p], database.Entries[splitPkg.Name])
} }
} }
} else { } else {
// Create entry for package
db.Entries[entry.Info.Name] = &entry
// Add virtual packages to database // Add virtual packages to database
for _, p := range entry.Info.Provides { for _, p := range entry.Info.Provides {
db.VirtualPackages[p] = append(db.VirtualPackages[p], entry.Info.Name) database.VirtualPackages[p] = append(database.VirtualPackages[p], entry)
}
} }
} }
} BPMDatabases[db.Name] = database
return nil return nil
} }
func (db *BPMDatabase) SyncLocalDatabaseFile() error { func (db *configDatabase) SyncLocalDatabaseFile() error {
dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb" dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb"
// Get URL to database // Get URL to database
@@ -139,17 +150,29 @@ func (db *BPMDatabase) SyncLocalDatabaseFile() error {
} }
// Retrieve data from URL // Retrieve data from URL
resp, err := http.Get(u) req, err := http.NewRequest("GET", u, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
// Load data into byte buffer // Create progress bar
buffer, err := io.ReadAll(resp.Body) bar := createProgressBar(resp.ContentLength, "Syncing "+db.Name, false)
// Copy data
var buffer bytes.Buffer
_, err = io.Copy(io.MultiWriter(&buffer, bar), resp.Body)
if err != nil {
return err
}
// Unmarshal data to ensure it is a valid BPM database // Unmarshal data to ensure it is a valid BPM database
err = yaml.Unmarshal(buffer, &BPMDatabase{}) err = yaml.Unmarshal(buffer.Bytes(), &BPMDatabase{})
if err != nil { if err != nil {
return fmt.Errorf("could not decode database: %s", err) return fmt.Errorf("could not decode database: %s", err)
} }
@@ -167,17 +190,13 @@ func (db *BPMDatabase) SyncLocalDatabaseFile() error {
} }
defer out.Close() defer out.Close()
_, err = out.Write(buffer) _, err = out.Write(buffer.Bytes())
return nil return nil
} }
func ReadLocalDatabaseFiles() (err error) { func ReadLocalDatabaseFiles() (err error) {
for _, db := range MainBPMConfig.Databases { for _, db := range MainBPMConfig.Databases {
// Initialize struct values
db.Entries = make(map[string]*BPMDatabaseEntry)
db.VirtualPackages = make(map[string][]string)
// Read database // Read database
err = db.ReadLocalDatabase() err = db.ReadLocalDatabase()
if err != nil { if err != nil {
@@ -188,15 +207,6 @@ func ReadLocalDatabaseFiles() (err error) {
return nil return nil
} }
func GetDatabase(name string) *BPMDatabase {
for _, db := range MainBPMConfig.Databases {
if db.Name == name {
return db
}
}
return nil
}
func GetDatabaseEntry(str string) (*BPMDatabaseEntry, *BPMDatabase, error) { func GetDatabaseEntry(str string) (*BPMDatabaseEntry, *BPMDatabase, error) {
split := strings.Split(str, "/") split := strings.Split(str, "/")
if len(split) == 1 { if len(split) == 1 {
@@ -204,7 +214,7 @@ func GetDatabaseEntry(str string) (*BPMDatabaseEntry, *BPMDatabase, error) {
if pkgName == "" { if pkgName == "" {
return nil, nil, errors.New("could not find database entry for this package") return nil, nil, errors.New("could not find database entry for this package")
} }
for _, db := range MainBPMConfig.Databases { for _, db := range BPMDatabases {
if db.ContainsPackage(pkgName) { if db.ContainsPackage(pkgName) {
return db.Entries[pkgName], db, nil return db.Entries[pkgName], db, nil
} }
@@ -216,7 +226,7 @@ func GetDatabaseEntry(str string) (*BPMDatabaseEntry, *BPMDatabase, error) {
if dbName == "" || pkgName == "" { if dbName == "" || pkgName == "" {
return nil, nil, errors.New("could not find database entry for this package") return nil, nil, errors.New("could not find database entry for this package")
} }
db := GetDatabase(dbName) db := BPMDatabases[dbName]
if db == nil || !db.ContainsPackage(pkgName) { if db == nil || !db.ContainsPackage(pkgName) {
return nil, nil, errors.New("could not find database entry for this package") return nil, nil, errors.New("could not find database entry for this package")
} }
@@ -227,7 +237,7 @@ func GetDatabaseEntry(str string) (*BPMDatabaseEntry, *BPMDatabase, error) {
} }
func FindReplacement(pkg string) *BPMDatabaseEntry { func FindReplacement(pkg string) *BPMDatabaseEntry {
for _, db := range MainBPMConfig.Databases { for _, db := range BPMDatabases {
for _, entry := range db.Entries { for _, entry := range db.Entries {
for _, replaced := range entry.Info.Replaces { for _, replaced := range entry.Info.Replaces {
if replaced == pkg { if replaced == pkg {
@@ -240,43 +250,348 @@ func FindReplacement(pkg string) *BPMDatabaseEntry {
return nil return nil
} }
func ResolveVirtualPackage(vpkg string) *BPMDatabaseEntry { func GetDatabaseVirtualPackageEntry(vpkg string) (providers []*BPMDatabaseEntry) {
for _, db := range MainBPMConfig.Databases { for _, db := range BPMDatabases {
if v, ok := db.VirtualPackages[vpkg]; ok { providers = append(providers, db.VirtualPackages[vpkg]...)
for _, pkg := range v {
return db.Entries[pkg]
}
}
} }
return nil slices.SortFunc(providers, func(a, b *BPMDatabaseEntry) int {
return strings.Compare(a.Info.Name, b.Info.Name)
})
return providers
} }
func (db *BPMDatabase) FetchPackage(pkg string) (string, error) { func (db *BPMDatabase) FetchPackage(pkg string) (string, error) {
// Check if package exists in database
if !db.ContainsPackage(pkg) { if !db.ContainsPackage(pkg) {
return "", errors.New("could not fetch package '" + pkg + "'") return "", errors.New("could not fetch package '" + pkg + "'")
} }
// Get package url from database
entry := db.Entries[pkg] entry := db.Entries[pkg]
URL, err := url.JoinPath(db.Source, entry.Download) u, err := url.JoinPath(db.Source, entry.Filepath)
if err != nil { if err != nil {
return "", err return "", err
} }
resp, err := http.Get(URL)
if err != nil {
return "", err
}
defer resp.Body.Close()
err = os.MkdirAll("/var/cache/bpm/fetched/", 0755) // Download package from url
filepath := path.Join("/var/cache/bpm/fetched/", path.Base(entry.Filepath))
err = downloadFile("Downloading "+entry.Info.Name, u, filepath, 0644)
if err != nil { if err != nil {
return "", err return "", err
} }
out, err := os.Create("/var/cache/bpm/fetched/" + path.Base(entry.Download))
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, resp.Body) // Download and verify signature if required
return "/var/cache/bpm/fetched/" + path.Base(entry.Download), nil if db.VerificationLevel != VerificationLevelNone {
err = downloadFile("", u+".sig", filepath+".sig", 0644)
if err != nil {
return "", err
}
err := VerifySignature(filepath, filepath+".sig", db.VerificationLevel == VerificationLevelTrusted, "/")
if err != nil {
return "", fmt.Errorf("Could not verify signature for %s: %s", filepath, err)
}
}
return filepath, nil
}
func (entry *BPMDatabaseEntry) GetEntryDependants() (dependants []string) {
dependantsMap := make(map[string][]string)
// Loop through all entries
for _, db := range BPMDatabases {
for _, e := range db.Entries {
// Skip iteration if comparing the same packages
if e.Info.Name == entry.Info.Name {
continue
}
// Add installed package to list if its dependencies include pkgName
if slices.ContainsFunc(e.Info.Depends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == entry.Info.Name
}) {
dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name)
continue
}
// Add installed package to list if its runtime dependencies include pkgName
if slices.ContainsFunc(e.Info.RuntimeDepends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == entry.Info.Name
}) {
dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name)
continue
}
// Loop through each virtual package
for _, vpkg := range entry.Info.Provides {
// Add installed package to list if its dependencies contain a provided virtual package
if slices.ContainsFunc(e.Info.Depends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == vpkg
}) {
dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name)
break
}
// Add installed package to list if its runtime dependencies contain a provided virtual package
if slices.ContainsFunc(e.Info.RuntimeDepends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == vpkg
}) {
dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], db.Name)
break
}
}
}
}
// Get keys
keySlice := slices.Collect(maps.Keys(dependantsMap))
slices.Sort(keySlice)
// Add all dependant entries to slice in alphabetical order
for _, entryName := range keySlice {
dbs := dependantsMap[entryName]
if len(dbs) > 1 {
for _, db := range dbs {
dependants = append(dependants, db+"/"+entryName)
}
} else {
dependants = append(dependants, entryName)
}
}
return dependants
}
func (entry *BPMDatabaseEntry) GetEntryOptionalDependants() (dependants []string) {
dependantsMap := make(map[string][]string)
for _, db := range BPMDatabases {
for _, e := range db.Entries {
if slices.ContainsFunc(e.Info.OptionalDepends, func(n string) bool {
// Remove optional dependency comment
n = strings.SplitN(n, ":", 2)[0]
// Remove required version
n, _, _ = SplitPkgNameAndVersion(n)
return n == entry.Info.Name
}) {
dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], e.Database.Name)
}
}
}
// Get keys
keySlice := slices.Collect(maps.Keys(dependantsMap))
slices.Sort(keySlice)
// Add all dependant entries to slice in alphabetical order
for _, entryName := range keySlice {
dbs := dependantsMap[entryName]
if len(dbs) > 1 {
for _, db := range dbs {
dependants = append(dependants, db+"/"+entryName)
}
} else {
dependants = append(dependants, entryName)
}
}
return dependants
}
func (entry *BPMDatabaseEntry) GetEntryMakeDependants() (dependants []string) {
dependantsMap := make(map[string][]string)
for _, db := range BPMDatabases {
for _, e := range db.Entries {
if slices.ContainsFunc(e.Info.MakeDepends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == entry.Info.Name
}) {
dependantsMap[e.Info.Name] = append(dependantsMap[e.Info.Name], e.Database.Name)
}
}
}
// Get keys
keySlice := slices.Collect(maps.Keys(dependantsMap))
slices.Sort(keySlice)
// Add all dependant entries to slice in alphabetical order
for _, entryName := range keySlice {
dbs := dependantsMap[entryName]
if len(dbs) > 1 {
for _, db := range dbs {
dependants = append(dependants, db+"/"+entryName)
}
} else {
dependants = append(dependants, entryName)
}
}
return dependants
}
func (entry *BPMDatabaseEntry) CreateReadableInfo(rootDir string, showBytes bool) string {
builder := strings.Builder{}
builderWriteStringNotEmpty := func(label string, value string) {
if value != "" {
builder.WriteString(label + ": " + value + "\n")
}
}
builderWriteArray := func(label string, array []string, sort bool) {
if len(array) == 0 {
return
}
// Sort array
if sort {
slices.Sort(array)
}
builder.WriteString(label + " (" + strconv.Itoa(len(array)) + "):\n")
for _, val := range array {
builder.WriteString(" - " + val + "\n")
}
}
builderWriteDependencyArray := func(label string, depends []string) {
if len(depends) == 0 {
return
}
// Sort array
slices.Sort(depends)
builder.WriteString(label + " (" + strconv.Itoa(len(depends)) + "):\n")
for _, val := range depends {
builder.WriteString(" - " + val)
// Show virtual package providers
if providers := GetDatabaseVirtualPackageEntry(val); len(providers) > 0 {
builder.WriteString(" (")
for i, vpkg := range providers {
if i == len(providers)-1 {
builder.WriteString(vpkg.Info.Name)
} else {
builder.WriteString(vpkg.Info.Name + ", ")
}
}
builder.WriteString(")")
}
builder.WriteString("\n")
}
}
// Main information
builder.WriteString("Name: " + entry.Info.Name + "\n")
builder.WriteString("Database: " + entry.Database.Name + "\n")
builder.WriteString("Description: " + entry.Info.Description + "\n")
builder.WriteString("Version: " + entry.Info.GetFullVersion() + "\n")
builderWriteStringNotEmpty("URL", entry.Info.Url)
builderWriteStringNotEmpty("License", entry.Info.License)
builderWriteArray("Maintainers", entry.Info.Maintainers, false)
builder.WriteString("Architecture: " + entry.Info.Arch + "\n")
if entry.Info.Type == "source" && entry.Info.OutputArch != "" && entry.Info.OutputArch != GetArch() {
builder.WriteString("Output architecture: " + entry.Info.OutputArch + "\n")
}
builder.WriteString("Type: " + entry.Info.Type + "\n")
// Dependencies
builderWriteDependencyArray("Dependencies", entry.Info.Depends)
if entry.Info.Type == "source" {
builderWriteDependencyArray("Make dependencies", entry.Info.MakeDepends)
builderWriteDependencyArray("Check dependencies", entry.Info.CheckDepends)
}
builderWriteDependencyArray("Runtime dependencies", entry.Info.RuntimeDepends)
if len(entry.Info.OptionalDepends) > 0 {
builder.WriteString("Optional dependencies: (" + strconv.Itoa(len(entry.Info.OptionalDepends)) + ")\n")
for _, depend := range entry.Info.OptionalDepends {
dependSplit := strings.SplitN(depend, ":", 2)
if len(dependSplit) == 2 {
builder.WriteString(fmt.Sprintf(" - %s (%s)", dependSplit[0], dependSplit[1]))
} else {
builder.WriteString(" - " + dependSplit[0])
}
// Show virtual package providers
if providers := GetDatabaseVirtualPackageEntry(dependSplit[0]); len(providers) > 0 {
builder.WriteString(" (")
for i, vpkg := range providers {
if i == len(providers)-1 {
builder.WriteString(vpkg.Info.Name)
} else {
builder.WriteString(vpkg.Info.Name + ", ")
}
}
builder.WriteString(")")
}
builder.WriteString("\n")
}
}
builderWriteArray("Dependant packages", entry.GetEntryDependants(), true)
builderWriteArray("Optionally dependant packages", entry.GetEntryOptionalDependants(), true)
builderWriteArray("Make dependant packages", entry.GetEntryMakeDependants(), true)
// Other package relations
builderWriteArray("Conflicting packages", entry.Info.Conflicts, true)
builderWriteArray("Provided packages", entry.Info.Provides, true)
builderWriteArray("Replaces packages", entry.Info.Replaces, true)
// Split packages
if entry.Info.Type == "source" && len(entry.Info.SplitPackages) != 0 {
splitPkgs := make([]string, len(entry.Info.SplitPackages))
for i, splitPkgInfo := range entry.Info.SplitPackages {
splitPkgs[i] = splitPkgInfo.Name
}
builderWriteArray("Split packages", splitPkgs, true)
}
// Installation reason
if rootDir != "" && IsPackageInstalled(entry.Info.Name, rootDir) {
installationReason := GetPackage(entry.Info.Name, rootDir).LocalInfo.GetInstallationReason()
var installationReasonString string
switch installationReason {
case InstallationReasonManual:
installationReasonString = "Manual"
case InstallationReasonDependency:
installationReasonString = "Dependency"
case InstallationReasonMakeDependency:
installationReasonString = "Make dependency"
default:
installationReasonString = "Unknown"
}
builder.WriteString("Installation reason: " + installationReasonString + "\n")
}
// Download size
downloadSize := entry.DownloadSize
var downloadSizeStr string
if showBytes {
downloadSizeStr = strconv.FormatInt(downloadSize, 10)
} else {
downloadSizeStr = BytesToHumanReadable(downloadSize)
}
builder.WriteString("Download size: " + downloadSizeStr + "\n")
// Installed size
if entry.Info.Type == "binary" {
installedSize := entry.InstalledSize
var installedSizeStr string
if showBytes {
installedSizeStr = strconv.FormatInt(installedSize, 10)
} else {
installedSizeStr = BytesToHumanReadable(installedSize)
}
builder.WriteString("Installed size: " + installedSizeStr + "\n")
}
return strings.TrimSpace(builder.String())
} }
+219 -197
View File
@@ -1,225 +1,247 @@
package bpmlib package bpmlib
import ( import (
"errors"
"fmt"
"slices" "slices"
"strings"
) )
type pkgInstallationReason struct { func (pkgInfo *PackageInfo) GetPackageDependants(rootDir string, skipMultipleProviders bool) (dependants []string) {
PkgName string
InstallationReason InstallationReason
}
func (pkgInfo *PackageInfo) GetDependencies(includeMakeDepends, includeOptionalDepends bool) []pkgInstallationReason {
allDepends := make([]pkgInstallationReason, 0)
for _, depend := range pkgInfo.Depends {
if !slices.ContainsFunc(allDepends, func(p pkgInstallationReason) bool {
return p.PkgName == depend
}) {
allDepends = append(allDepends, pkgInstallationReason{
PkgName: depend,
InstallationReason: InstallationReasonDependency,
})
}
}
if includeOptionalDepends {
for _, depend := range pkgInfo.OptionalDepends {
if !slices.ContainsFunc(allDepends, func(p pkgInstallationReason) bool {
return p.PkgName == depend
}) {
allDepends = append(allDepends, pkgInstallationReason{
PkgName: depend,
InstallationReason: InstallationReasonDependency,
})
}
}
}
if includeMakeDepends {
for _, depend := range pkgInfo.MakeDepends {
if !slices.ContainsFunc(allDepends, func(p pkgInstallationReason) bool {
return p.PkgName == depend
}) {
allDepends = append(allDepends, pkgInstallationReason{
PkgName: depend,
InstallationReason: InstallationReasonMakeDependency,
})
}
}
}
return allDepends
}
func (pkgInfo *PackageInfo) GetAllDependencies(includeMakeDepends, includeOptionalDepends bool, rootDir string) (resolved []string) {
// Initialize slices
resolved = make([]string, 0)
unresolved := make([]string, 0)
// Call unexported function
pkgInfo.getAllDependencies(&resolved, &unresolved, includeMakeDepends, includeOptionalDepends, rootDir)
return resolved
}
func (pkgInfo *PackageInfo) getAllDependencies(resolved *[]string, unresolved *[]string, includeMakeDepends, includeOptionalDepends bool, rootDir string) {
// Add current package name to unresolved slice
*unresolved = append(*unresolved, pkgInfo.Name)
// Loop through all dependencies
for _, pkgIR := range pkgInfo.GetDependencies(includeMakeDepends, includeOptionalDepends) {
depend := pkgIR.PkgName
if isVirtual, p := IsVirtualPackage(depend, rootDir); isVirtual {
depend = p
}
if !slices.Contains(*resolved, depend) {
// Add current dependency to resolved slice when circular dependency is detected
if slices.Contains(*unresolved, depend) {
if !slices.Contains(*resolved, depend) {
*resolved = append(*resolved, depend)
}
continue
}
dependInfo := GetPackageInfo(depend, rootDir)
if dependInfo != nil {
dependInfo.getAllDependencies(resolved, unresolved, includeMakeDepends, includeOptionalDepends, rootDir)
}
}
}
if !slices.Contains(*resolved, pkgInfo.Name) {
*resolved = append(*resolved, pkgInfo.Name)
}
*unresolved = stringSliceRemove(*unresolved, pkgInfo.Name)
}
func ResolveAllPackageDependenciesFromDatabases(pkgInfo *PackageInfo, checkMake, checkOptional, ignoreInstalled, verbose bool, rootDir string) (resolved []pkgInstallationReason, unresolved []string) {
// Initialize slices
resolved = make([]pkgInstallationReason, 0)
unresolved = make([]string, 0)
// Call unexported function
resolvePackageDependenciesFromDatabase(&resolved, &unresolved, pkgInfo, checkMake, checkOptional, ignoreInstalled, verbose, rootDir)
// Remove main package from unresolved slice
unresolved = stringSliceRemove(unresolved, pkgInfo.Name)
return resolved, unresolved
}
func resolvePackageDependenciesFromDatabase(resolved *[]pkgInstallationReason, unresolved *[]string, pkgInfo *PackageInfo, checkMake, checkOptional, ignoreInstalled, verbose bool, rootDir string) {
// Add current package name to unresolved slice
*unresolved = append(*unresolved, pkgInfo.Name)
// Loop through all dependencies
for _, pkgIR := range pkgInfo.GetDependencies(pkgInfo.Type == "source", checkOptional) {
// Skip dependency if it has already been resolved
if slices.ContainsFunc(*resolved, func(p pkgInstallationReason) bool {
return p.PkgName == pkgIR.PkgName
}) {
continue
}
// Add current dependency to resolved slice when circular dependency is detected
if slices.Contains(*unresolved, pkgIR.PkgName) {
if verbose {
fmt.Printf("Circular dependency was detected (%s -> %s). Installing %s first\n", pkgInfo.Name, pkgIR.PkgName, pkgIR.PkgName)
}
*resolved = append(*resolved, pkgInstallationReason{
PkgName: pkgIR.PkgName,
InstallationReason: pkgIR.InstallationReason,
})
continue
}
// Skip dependency if it is already installed or provided
if ignoreInstalled && IsPackageProvided(pkgIR.PkgName, rootDir) {
continue
}
// Get database entry for dependency
var err error
var entry *BPMDatabaseEntry
entry, _, err = GetDatabaseEntry(pkgIR.PkgName)
if err != nil {
if entry = ResolveVirtualPackage(pkgIR.PkgName); entry == nil {
if !slices.Contains(*unresolved, pkgIR.PkgName) {
*unresolved = append(*unresolved, pkgIR.PkgName)
}
continue
}
}
// Resolve the dependencies of this dependency
resolvePackageDependenciesFromDatabase(resolved, unresolved, entry.Info, checkMake, checkOptional, ignoreInstalled, verbose, rootDir)
// Move dependency from the unresolved slice to the resolved slice
if !slices.ContainsFunc(*resolved, func(p pkgInstallationReason) bool {
return p.PkgName == entry.Info.Name
}) {
*resolved = append(*resolved, pkgInstallationReason{
PkgName: entry.Info.Name,
InstallationReason: pkgIR.InstallationReason,
})
}
*unresolved = stringSliceRemove(*unresolved, entry.Info.Name)
}
}
func GetPackageDependants(pkgName string, rootDir string) ([]string, error) {
ret := make([]string, 0)
// Get BPM package
pkg := GetPackage(pkgName, rootDir)
if pkg == nil {
return nil, errors.New("package not found: " + pkgName)
}
// Get installed package names // Get installed package names
pkgs, err := GetInstalledPackages(rootDir) pkgs, ok := localPackageInformation[rootDir]
if err != nil { if !ok {
return nil, errors.New("could not get installed packages") return nil
} }
// Loop through all installed packages // Loop through all installed packages
for _, installedPkgName := range pkgs { for _, installedPkg := range pkgs {
// Get installed BPM package
installedPkg := GetPackage(installedPkgName, rootDir)
if installedPkg == nil {
return nil, errors.New("package not found: " + installedPkgName)
}
// Skip iteration if comparing the same packages // Skip iteration if comparing the same packages
if installedPkg.PkgInfo.Name == pkgName { if installedPkg.Name == pkgInfo.Name {
continue continue
} }
// Get installed package dependencies
dependencies := installedPkg.PkgInfo.GetDependencies(false, true)
// Add installed package to list if its dependencies include pkgName // Add installed package to list if its dependencies include pkgName
if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { if slices.ContainsFunc(installedPkg.Depends, func(n string) bool {
return p.PkgName == pkgName n, _, _ = SplitPkgNameAndVersion(n)
return n == pkgInfo.Name
}) { }) {
ret = append(ret, installedPkgName) dependants = append(dependants, installedPkg.Name)
continue
}
// Add installed package to list if its runtime dependencies include pkgName
if slices.ContainsFunc(installedPkg.RuntimeDepends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == pkgInfo.Name
}) {
dependants = append(dependants, installedPkg.Name)
continue continue
} }
// Loop through each virtual package // Loop through each virtual package
for _, vpkg := range pkg.PkgInfo.Provides { for _, vpkg := range pkgInfo.Provides {
if skipMultipleProviders && len(GetVirtualPackageInfo(vpkg, rootDir)) > 1 {
continue
}
// Add installed package to list if its dependencies contain a provided virtual package // Add installed package to list if its dependencies contain a provided virtual package
if slices.ContainsFunc(dependencies, func(p pkgInstallationReason) bool { if slices.ContainsFunc(installedPkg.Depends, func(n string) bool {
return p.PkgName == vpkg n, _, _ = SplitPkgNameAndVersion(n)
return n == vpkg
}) { }) {
ret = append(ret, installedPkgName) dependants = append(dependants, installedPkg.Name)
break
}
// Add installed package to list if its runtime dependencies contain a provided virtual package
if slices.ContainsFunc(installedPkg.RuntimeDepends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == vpkg
}) {
dependants = append(dependants, installedPkg.Name)
break break
} }
} }
} }
return ret, nil return dependants
}
func (pkgInfo *PackageInfo) GetPackageOptionalDependants(rootDir string) (dependants []string) {
// Get installed package names
pkgs, ok := localPackageInformation[rootDir]
if !ok {
return nil
}
// Loop through all installed packages
for _, installedPkg := range pkgs {
// Skip iteration if comparing the same packages
if installedPkg.Name == pkgInfo.Name {
continue
}
// Add installed package to list if its optional dependencies include pkgName
if slices.ContainsFunc(installedPkg.OptionalDepends, func(n string) bool {
// Remove optional dependency comment
n = strings.SplitN(n, ":", 2)[0]
// Remove required version
n, _, _ = SplitPkgNameAndVersion(n)
return n == pkgInfo.Name
}) {
dependants = append(dependants, installedPkg.Name)
continue
}
// Loop through each virtual package
for _, vpkg := range pkgInfo.Provides {
// Add installed package to list if its optional dependencies contain a provided virtual package
if slices.ContainsFunc(installedPkg.OptionalDepends, func(n string) bool {
// Remove optional dependency comment
n = strings.SplitN(n, ":", 2)[0]
// Remove required version
n, _, _ = SplitPkgNameAndVersion(n)
return n == vpkg
}) {
dependants = append(dependants, installedPkg.Name)
break
}
}
}
return dependants
}
type ResolvedPackage struct {
DatabaseEntry *BPMDatabaseEntry
InstallationReason InstallationReason
}
func ResolveDependencies(pkgInfo *PackageInfo, resolvedVirtualPackages map[string]string, includeRuntimeDepends bool, rootDir string) (resolved []ResolvedPackage, unresolved []string) {
visited := make([]string, 0)
var dfs func(resolvedPkg *PackageInfo)
dfs = func(pkgInfo *PackageInfo) {
checkDependencies := func(dependencies []string, installationReason InstallationReason) {
for _, depend := range dependencies {
// Split dependency name and required version
dependName, _, _ := SplitPkgNameAndVersion(depend)
// Ignore if package is already installed
if IsPackageInstalled(dependName, rootDir) && EvaluateDependency(depend, GetPackageInfo(dependName, rootDir).Version) {
continue
} else if providers := GetVirtualPackageInfo(dependName, rootDir); len(providers) > 0 {
continue
}
// Find database entry for dependency
var dependEntry *BPMDatabaseEntry
if resolvedVpkg, ok := resolvedVirtualPackages[dependName]; ok {
dependEntry, _, _ = GetDatabaseEntry(resolvedVpkg)
} else if entry, _, _ := GetDatabaseEntry(dependName); entry != nil {
dependEntry = entry
} else if providers := GetDatabaseVirtualPackageEntry(dependName); len(providers) > 0 {
dependEntry = providers[0]
}
if dependEntry == nil {
unresolved = append(unresolved, depend)
continue
}
// Ensure entry has required version
if !EvaluateDependency(depend, dependEntry.Info.Version) {
unresolved = append(unresolved, depend)
continue
}
// Skip ignored packages in config
if rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, dependEntry.Info.Name) {
continue
}
if !slices.Contains(visited, dependEntry.Info.Name) {
dfs(dependEntry.Info)
resolved = append(resolved, ResolvedPackage{DatabaseEntry: dependEntry, InstallationReason: installationReason})
}
}
}
visited = append(visited, pkgInfo.Name)
checkDependencies(pkgInfo.Depends, InstallationReasonDependency)
if includeRuntimeDepends {
checkDependencies(pkgInfo.RuntimeDepends, InstallationReasonDependency)
}
if pkgInfo.Type == "source" {
checkDependencies(pkgInfo.MakeDepends, InstallationReasonMakeDependency)
checkDependencies(pkgInfo.CheckDepends, InstallationReasonMakeDependency)
}
}
dfs(pkgInfo)
return resolved, unresolved
}
func SplitPkgNameAndVersion(pkg string) (string, string, string) {
if strings.Contains(pkg, ">=") {
pkgSplit := strings.SplitN(pkg, ">=", 2)
pkgName := pkgSplit[0]
pkgVersion := pkgSplit[1]
return pkgName, ">=", pkgVersion
} else if strings.Contains(pkg, ">") {
pkgSplit := strings.SplitN(pkg, ">", 2)
pkgName := pkgSplit[0]
pkgVersion := pkgSplit[1]
return pkgName, ">", pkgVersion
} else if strings.Contains(pkg, "<=") {
pkgSplit := strings.SplitN(pkg, "<=", 2)
pkgName := pkgSplit[0]
pkgVersion := pkgSplit[1]
return pkgName, "<=", pkgVersion
} else if strings.Contains(pkg, "<") {
pkgSplit := strings.SplitN(pkg, "<", 2)
pkgName := pkgSplit[0]
pkgVersion := pkgSplit[1]
return pkgName, "<", pkgVersion
} else if strings.Contains(pkg, "=") {
pkgSplit := strings.SplitN(pkg, "=", 2)
pkgName := pkgSplit[0]
pkgVersion := pkgSplit[1]
return pkgName, "=", pkgVersion
}
return pkg, "", ""
}
func EvaluateDependency(pkg, matchVersion string) bool {
_, comparisonSymbol, pkgVersion := SplitPkgNameAndVersion(pkg)
switch comparisonSymbol {
case ">=":
return CompareVersions(matchVersion, pkgVersion) >= 0
case ">":
return CompareVersions(matchVersion, pkgVersion) > 0
case "<=":
return CompareVersions(matchVersion, pkgVersion) <= 0
case "<":
return CompareVersions(matchVersion, pkgVersion) < 0
case "=":
if cutPkgVersion, ok := strings.CutSuffix(pkgVersion, "*"); ok {
return strings.HasPrefix(matchVersion, cutPkgVersion)
} else {
return CompareVersions(matchVersion, pkgVersion) == 0
}
default:
return true
}
} }
+12
View File
@@ -2,6 +2,7 @@ package bpmlib
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
) )
@@ -10,6 +11,7 @@ type PackageNotFoundErr struct {
} }
func (e PackageNotFoundErr) Error() string { func (e PackageNotFoundErr) Error() string {
slices.Sort(e.packages)
return "The following packages were not found in any databases: " + strings.Join(e.packages, ", ") return "The following packages were not found in any databases: " + strings.Join(e.packages, ", ")
} }
@@ -18,6 +20,7 @@ type DependencyNotFoundErr struct {
} }
func (e DependencyNotFoundErr) Error() string { func (e DependencyNotFoundErr) Error() string {
slices.Sort(e.dependencies)
return "The following dependencies were not found in any databases: " + strings.Join(e.dependencies, ", ") return "The following dependencies were not found in any databases: " + strings.Join(e.dependencies, ", ")
} }
@@ -27,6 +30,7 @@ type PackageConflictErr struct {
} }
func (e PackageConflictErr) Error() string { func (e PackageConflictErr) Error() string {
slices.Sort(e.conflicts)
return fmt.Sprintf("Package (%s) is in conflict with the following packages: %s", e.pkg, strings.Join(e.conflicts, ", ")) return fmt.Sprintf("Package (%s) is in conflict with the following packages: %s", e.pkg, strings.Join(e.conflicts, ", "))
} }
@@ -40,3 +44,11 @@ type PackageScriptErr struct {
func (e PackageScriptErr) Error() string { func (e PackageScriptErr) Error() string {
return fmt.Sprintf("could not execute package script (%s) for package (%s): %s", e.packageScript, e.packageName, e.err) return fmt.Sprintf("could not execute package script (%s) for package (%s): %s", e.packageScript, e.packageName, e.err)
} }
type PackageRemovalDependencyErr struct {
RequiredPackages map[string][]string
}
func (e PackageRemovalDependencyErr) Error() string {
return "removing these package would break other installed packages"
}
+208 -70
View File
@@ -4,32 +4,28 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"maps"
"os" "os"
"path" "path"
"slices" "slices"
"strings" "strings"
) )
type ReinstallMethod uint8
const (
ReinstallMethodNone ReinstallMethod = iota
ReinstallMethodSpecified ReinstallMethod = iota
ReinstallMethodAll ReinstallMethod = iota
)
// InstallPackages installs the specified packages into the given root directory by fetching them from databases or directly from local bpm archives // InstallPackages installs the specified packages into the given root directory by fetching them from databases or directly from local bpm archives
func InstallPackages(rootDir string, forceInstallationReason InstallationReason, reinstallMethod ReinstallMethod, installOptionalDependencies, forceInstallation, verbose bool, packages ...string) (operation *BPMOperation, err error) { func InstallPackages(rootDir string, forceInstallationReason InstallationReason, reinstallPackages bool, installRuntimeDependencies, forceInstallation, runChecks bool, verbose bool, packages ...string) (operation *BPMOperation, err error) {
// Setup operation struct // Setup operation struct
operation = &BPMOperation{ operation = &BPMOperation{
Actions: make([]OperationAction, 0), Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0), UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string), ModifiedFiles: make(map[string]string),
RunChecks: runChecks,
RootDir: rootDir, RootDir: rootDir,
compiledPackages: make(map[string]string), compiledPackages: make(map[string]string),
} }
// Remove duplicates from packages
packages = removeDuplicates(packages)
// Resolve packages // Resolve packages
pkgsNotFound := make([]string, 0) pkgsNotFound := make([]string, 0)
for _, pkg := range packages { for _, pkg := range packages {
@@ -41,7 +37,7 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
if bpmpkg.PkgInfo.Type == "source" && bpmpkg.PkgInfo.IsSplitPackage() { if bpmpkg.PkgInfo.Type == "source" && bpmpkg.PkgInfo.IsSplitPackage() {
for _, splitPkg := range bpmpkg.PkgInfo.SplitPackages { for _, splitPkg := range bpmpkg.PkgInfo.SplitPackages {
if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(splitPkg.Name, rootDir) && GetPackageInfo(splitPkg.Name, rootDir).GetFullVersion() == splitPkg.GetFullVersion() { if !reinstallPackages && IsPackageInstalled(splitPkg.Name, rootDir) && GetPackageInfo(splitPkg.Name, rootDir).GetFullVersion() == splitPkg.GetFullVersion() {
continue continue
} }
@@ -49,13 +45,13 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
installationReason := forceInstallationReason installationReason := forceInstallationReason
if installationReason == InstallationReasonUnknown { if installationReason == InstallationReasonUnknown {
if IsPackageInstalled(splitPkg.Name, rootDir) { if IsPackageInstalled(splitPkg.Name, rootDir) {
installationReason = GetInstallationReason(splitPkg.Name, rootDir) installationReason = GetPackage(splitPkg.Name, rootDir).LocalInfo.GetInstallationReason()
} else { } else {
installationReason = InstallationReasonManual installationReason = InstallationReasonManual
} }
} }
operation.AppendAction(&InstallPackageAction{ operation.Actions = append(operation.Actions, &InstallPackageAction{
File: pkg, File: pkg,
InstallationReason: installationReason, InstallationReason: installationReason,
BpmPackage: bpmpkg, BpmPackage: bpmpkg,
@@ -65,7 +61,7 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
continue continue
} }
if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() { if !reinstallPackages && IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() {
continue continue
} }
@@ -73,35 +69,44 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
installationReason := forceInstallationReason installationReason := forceInstallationReason
if installationReason == InstallationReasonUnknown { if installationReason == InstallationReasonUnknown {
if IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) { if IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) {
installationReason = GetInstallationReason(bpmpkg.PkgInfo.Name, rootDir) installationReason = GetPackage(bpmpkg.PkgInfo.Name, rootDir).LocalInfo.GetInstallationReason()
} else { } else {
installationReason = InstallationReasonManual installationReason = InstallationReasonManual
} }
} }
operation.AppendAction(&InstallPackageAction{ operation.Actions = append(operation.Actions, &InstallPackageAction{
File: pkg, File: pkg,
InstallationReason: installationReason, InstallationReason: installationReason,
BpmPackage: bpmpkg, BpmPackage: bpmpkg,
}) })
} else { } else {
// Split package name and required version
pkgName, _, _ := SplitPkgNameAndVersion(pkg)
var entry *BPMDatabaseEntry var entry *BPMDatabaseEntry
if e, _, err := GetDatabaseEntry(pkg); err == nil { if e, _, err := GetDatabaseEntry(pkgName); err == nil {
entry = e entry = e
} else if isVirtual, p := IsVirtualPackage(pkg, rootDir); isVirtual { } else if providers := GetVirtualPackageInfo(pkgName, rootDir); len(providers) > 0 {
entry, _, err = GetDatabaseEntry(p) entry, _, err = GetDatabaseEntry(providers[0].Name)
if err != nil { if err != nil {
pkgsNotFound = append(pkgsNotFound, pkg) pkgsNotFound = append(pkgsNotFound, pkg)
continue continue
} }
} else if e := ResolveVirtualPackage(pkg); e != nil { } else if providers := GetDatabaseVirtualPackageEntry(pkgName); len(providers) > 0 {
entry = e entry = providers[0]
} else { } else {
pkgsNotFound = append(pkgsNotFound, pkg) pkgsNotFound = append(pkgsNotFound, pkg)
continue continue
} }
if reinstallMethod == ReinstallMethodNone && IsPackageInstalled(entry.Info.Name, rootDir) && GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() {
if !EvaluateDependency(pkg, entry.Info.Version) {
pkgsNotFound = append(pkgsNotFound, pkg)
continue
}
if !reinstallPackages && IsPackageInstalled(entry.Info.Name, rootDir) && GetPackageInfo(entry.Info.Name, rootDir).GetFullVersion() == entry.Info.GetFullVersion() {
continue continue
} }
@@ -109,13 +114,13 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
installationReason := forceInstallationReason installationReason := forceInstallationReason
if installationReason == InstallationReasonUnknown { if installationReason == InstallationReasonUnknown {
if IsPackageInstalled(entry.Info.Name, rootDir) { if IsPackageInstalled(entry.Info.Name, rootDir) {
installationReason = GetInstallationReason(entry.Info.Name, rootDir) installationReason = GetPackage(entry.Info.Name, rootDir).LocalInfo.GetInstallationReason()
} else { } else {
installationReason = InstallationReasonManual installationReason = InstallationReasonManual
} }
} }
operation.AppendAction(&FetchPackageAction{ operation.Actions = append(operation.Actions, &FetchPackageAction{
InstallationReason: installationReason, InstallationReason: installationReason,
DatabaseEntry: entry, DatabaseEntry: entry,
}) })
@@ -128,10 +133,7 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
} }
// Resolve dependencies // Resolve dependencies
err = operation.ResolveDependencies(reinstallMethod == ReinstallMethodAll, installOptionalDependencies, verbose) operation.ResolveDependencies(installRuntimeDependencies)
if err != nil {
return nil, fmt.Errorf("could not resolve dependencies: %s", err)
}
if len(operation.UnresolvedDepends) != 0 { if len(operation.UnresolvedDepends) != 0 {
if !forceInstallation { if !forceInstallation {
return nil, DependencyNotFoundErr{operation.UnresolvedDepends} return nil, DependencyNotFoundErr{operation.UnresolvedDepends}
@@ -144,12 +146,9 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
operation.ReplaceObsoletePackages() operation.ReplaceObsoletePackages()
// Check for conflicts // Check for conflicts
conflicts, err := operation.CheckForConflicts() conflicts := operation.CheckForConflicts()
if err != nil {
return nil, fmt.Errorf("could not complete package conflict check: %s", err)
}
if len(conflicts) > 0 { if len(conflicts) > 0 {
err = nil err = fmt.Errorf("conflicts detected")
for pkg, conflict := range conflicts { for pkg, conflict := range conflicts {
err = errors.Join(err, PackageConflictErr{pkg, conflict}) err = errors.Join(err, PackageConflictErr{pkg, conflict})
} }
@@ -164,14 +163,14 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
if rootDir != "/" { if rootDir != "/" {
sourcePackages := make([]string, 0) sourcePackages := make([]string, 0)
for _, action := range operation.Actions { for _, action := range operation.Actions {
switch action.(type) { switch action := action.(type) {
case *InstallPackageAction: case *InstallPackageAction:
if action.(*InstallPackageAction).BpmPackage.PkgInfo.Type == "source" { if action.BpmPackage.PkgInfo.Type == "source" {
sourcePackages = append(sourcePackages, action.(*InstallPackageAction).BpmPackage.PkgInfo.Name) sourcePackages = append(sourcePackages, action.BpmPackage.PkgInfo.Name)
} }
case *FetchPackageAction: case *FetchPackageAction:
if action.(*FetchPackageAction).DatabaseEntry.Info.Type == "source" { if action.DatabaseEntry.Info.Type == "source" {
sourcePackages = append(sourcePackages, action.(*FetchPackageAction).DatabaseEntry.Info.Name) sourcePackages = append(sourcePackages, action.DatabaseEntry.Info.Name)
} }
} }
} }
@@ -186,39 +185,80 @@ func InstallPackages(rootDir string, forceInstallationReason InstallationReason,
} }
// RemovePackages removes the specified packages from the given root directory // RemovePackages removes the specified packages from the given root directory
func RemovePackages(rootDir string, removeUnusedPackagesOnly, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) { func RemovePackages(rootDir string, force, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) {
operation = &BPMOperation{ operation = &BPMOperation{
Actions: make([]OperationAction, 0), Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0), UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string), ModifiedFiles: make(map[string]string),
RootDir: rootDir, RootDir: rootDir,
compiledPackages: make(map[string]string), compiledPackages: make(map[string]string),
} }
// Remove duplicates from packages
packages = removeDuplicates(packages)
// Search for packages // Search for packages
for _, pkg := range packages { for _, pkg := range packages {
bpmpkg := GetPackage(pkg, rootDir) bpmpkg := GetPackage(pkg, rootDir)
if providers := GetVirtualPackageInfo(pkg, rootDir); len(providers) > 0 {
bpmpkg = GetPackage(providers[0].Name, rootDir)
}
if bpmpkg == nil { if bpmpkg == nil {
continue continue
} }
operation.AppendAction(&RemovePackageAction{BpmPackage: bpmpkg}) operation.Actions = append(operation.Actions, &RemovePackageAction{BpmPackage: bpmpkg})
}
// Do not remove packages which other packages depend on
if removeUnusedPackagesOnly {
err := operation.RemoveNeededPackages()
if err != nil {
return nil, fmt.Errorf("could not skip needed packages: %s", err)
}
} }
// Do package cleanup // Do package cleanup
if cleanupDependencies { if cleanupDependencies {
err := operation.Cleanup(true) err := operation.Cleanup(MainBPMConfig.CleanupMakeDependencies)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not perform cleanup for operation: %s", err) return nil, fmt.Errorf("could not perform cleanup for operation: %s", err)
} }
} }
// Return error if other packages depend on removed ones
if !force {
// Get packages and their dependants
packageDepndants := make(map[string][]string, 0)
for _, action := range operation.Actions {
// Skip package if ignored in config
if rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, action.(*RemovePackageAction).BpmPackage.PkgInfo.Name) {
continue
}
dependants := action.(*RemovePackageAction).BpmPackage.PkgInfo.GetPackageDependants(rootDir, true)
packageDepndants[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = dependants
}
// Remove dependant packages from map if they are to be removed by this operation
for pkg, required := range packageDepndants {
required = slices.DeleteFunc(required, func(pkgName string) bool {
_, ok := packageDepndants[pkgName]
return ok
})
packageDepndants[pkg] = required
}
// Remove dependant packages if ignored
for pkg, required := range packageDepndants {
required = slices.DeleteFunc(required, func(pkgName string) bool {
return rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, pkgName)
})
packageDepndants[pkg] = required
}
// Remove empty keys from map
maps.DeleteFunc(packageDepndants, func(pkg string, required []string) bool {
return len(required) == 0
})
// Return error
if len(packageDepndants) != 0 {
return nil, PackageRemovalDependencyErr{RequiredPackages: packageDepndants}
}
}
return operation, nil return operation, nil
} }
@@ -227,7 +267,7 @@ func CleanupPackages(cleanupMakeDepends bool, rootDir string) (operation *BPMOpe
operation = &BPMOperation{ operation = &BPMOperation{
Actions: make([]OperationAction, 0), Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0), UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string), ModifiedFiles: make(map[string]string),
RootDir: rootDir, RootDir: rootDir,
compiledPackages: make(map[string]string), compiledPackages: make(map[string]string),
} }
@@ -324,7 +364,7 @@ func CleanupCache(rootDir string, cleanupCompilationFiles, cleanupCompiledPackag
} }
// UpdatePackages fetches the newest versions of all installed packages from // UpdatePackages fetches the newest versions of all installed packages from
func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependencies, forceInstallation, verbose bool) (operation *BPMOperation, err error) { func UpdatePackages(rootDir string, syncDatabase, allowDowngrades, forceInstallation, runChecks, verbose bool) (operation *BPMOperation, err error) {
// Sync databases // Sync databases
if syncDatabase { if syncDatabase {
err := SyncDatabase(verbose) err := SyncDatabase(verbose)
@@ -355,14 +395,16 @@ func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependenci
operation = &BPMOperation{ operation = &BPMOperation{
Actions: make([]OperationAction, 0), Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0), UnresolvedDepends: make([]string, 0),
Changes: make(map[string]string), ModifiedFiles: make(map[string]string),
RunChecks: runChecks,
RootDir: rootDir, RootDir: rootDir,
compiledPackages: make(map[string]string), compiledPackages: make(map[string]string),
} }
// Search for packages // Search for packages
pkgsNotFound := make([]string, 0)
for _, pkg := range pkgs { for _, pkg := range pkgs {
if slices.Contains(MainBPMConfig.IgnorePackages, pkg) { if rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, pkg) {
continue continue
} }
var entry *BPMDatabaseEntry var entry *BPMDatabaseEntry
@@ -377,31 +419,127 @@ func UpdatePackages(rootDir string, syncDatabase bool, installOptionalDependenci
if installedInfo == nil { if installedInfo == nil {
return nil, fmt.Errorf("could not get package info for package (%s)", pkg) return nil, fmt.Errorf("could not get package info for package (%s)", pkg)
} else { } else {
comparison := ComparePackageVersions(*entry.Info, *installedInfo) comparison := CompareVersions(entry.Info.GetFullVersion(), installedInfo.GetFullVersion())
if comparison > 0 { if (!allowDowngrades && comparison > 0) || (allowDowngrades && comparison != 0) {
operation.AppendAction(&FetchPackageAction{ operation.Actions = append(operation.Actions, &FetchPackageAction{
InstallationReason: GetInstallationReason(pkg, rootDir), InstallationReason: GetPackage(pkg, rootDir).LocalInfo.GetInstallationReason(),
DatabaseEntry: entry, DatabaseEntry: entry,
}) })
} }
// Check for missing dependencies
for _, depend := range entry.Info.Depends {
// Split package name and required version
dependName, _, _ := SplitPkgNameAndVersion(depend)
if IsPackageInstalled(dependName, rootDir) && EvaluateDependency(depend, GetPackageInfo(dependName, rootDir).Version) {
continue
}
if len(GetVirtualPackageInfo(dependName, rootDir)) > 0 {
continue
}
// Find database entry for missing dependency
dependEntry, _, err := GetDatabaseEntry(dependName)
if err != nil {
providers := GetDatabaseVirtualPackageEntry(dependName)
if len(providers) == 0 {
pkgsNotFound = append(pkgsNotFound, depend)
continue
}
dependEntry = providers[0]
}
// Skip dependency if action already exists
if ActionSliceIndex(operation.Actions, dependEntry.Info.Name) != -1 {
continue
}
// Skip dependency if ignored in config
if rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, dependEntry.Info.Name) {
continue
}
// Ensure entry has required version
if !EvaluateDependency(depend, dependEntry.Info.Version) {
pkgsNotFound = append(pkgsNotFound, depend)
continue
}
// Fetch dependency
operation.Actions = append(operation.Actions, &FetchPackageAction{
InstallationReason: InstallationReasonDependency,
DatabaseEntry: dependEntry,
})
}
// Check for missing runtime dependencies
for _, depend := range entry.Info.RuntimeDepends {
// Split package name and required version
dependName, _, _ := SplitPkgNameAndVersion(depend)
if IsPackageInstalled(dependName, rootDir) && EvaluateDependency(depend, GetPackageInfo(dependName, rootDir).Version) {
continue
}
if len(GetVirtualPackageInfo(dependName, rootDir)) > 0 {
continue
}
// Find database entry for missing dependency
dependEntry, _, err := GetDatabaseEntry(dependName)
if err != nil {
providers := GetDatabaseVirtualPackageEntry(dependName)
if len(providers) == 0 {
pkgsNotFound = append(pkgsNotFound, depend)
continue
}
dependEntry = providers[0]
}
// Skip dependency if ignored in config
if rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, dependEntry.Info.Name) {
continue
}
// Ensure entry has required version
if !EvaluateDependency(depend, dependEntry.Info.Version) {
pkgsNotFound = append(pkgsNotFound, depend)
continue
}
// Fetch dependency
operation.Actions = append(operation.Actions, &FetchPackageAction{
InstallationReason: InstallationReasonDependency,
DatabaseEntry: dependEntry,
})
}
} }
} }
// Check for new dependencies in updated packages // Return error if not all packages are found
err = operation.ResolveDependencies(false, installOptionalDependencies, verbose) if len(pkgsNotFound) != 0 {
if err != nil { return nil, PackageNotFoundErr{pkgsNotFound}
return nil, fmt.Errorf("could not resolve dependencies: %s", err)
}
if len(operation.UnresolvedDepends) != 0 {
if !forceInstallation {
return nil, DependencyNotFoundErr{operation.UnresolvedDepends}
} else if verbose {
log.Printf("Warning: %s", DependencyNotFoundErr{operation.UnresolvedDepends})
}
} }
// Replace obsolete packages // Replace obsolete packages
operation.ReplaceObsoletePackages() operation.ReplaceObsoletePackages()
// Check for conflicts
conflicts := operation.CheckForConflicts()
if len(conflicts) > 0 {
err = fmt.Errorf("conflicts detected")
for pkg, conflict := range conflicts {
err = errors.Join(err, PackageConflictErr{pkg, conflict})
}
if !forceInstallation {
return nil, err
} else {
log.Printf("Warning: %s", err)
}
}
return operation, nil return operation, nil
} }
+11 -2
View File
@@ -1,8 +1,17 @@
module git.enumerated.dev/bubble-package-manager/bpm/src/bpmlib module github.com/EnumeratedDev/bpm/src/bpmlib
go 1.23 go 1.24.0
require ( require (
github.com/drone/envsubst v1.0.3
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f
github.com/schollz/progressbar/v3 v3.18.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require (
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/term v0.36.0 // indirect
)
+24
View File
@@ -1,5 +1,29 @@
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
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/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g=
github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g= github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f h1:xt29M2T6STgldg+WEP51gGePQCsQvklmP2eIhPIBK3g=
github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+216
View File
@@ -0,0 +1,216 @@
package bpmlib
import (
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
)
func InitializeKeyring(rootDir string) error {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
// Create GPG directory
err := os.Mkdir(gpgHomedir, 0700)
if err != nil && !os.IsExist(err) {
return err
}
// Get number of secret keys
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-secret-keys", "--with-colons")
output, err := cmd.Output()
if err != nil {
return err
}
secretKeysLineCount := len(strings.Split(strings.TrimSpace(string(output)), "\n"))
// Create signing key
if secretKeysLineCount <= 1 {
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--batch", "--gen-key")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = strings.NewReader(`%echo Creating signing key...
Key-Type: RSA
Key-Length: 4096
Key-Usage: sign
Name-Real: BPM signing key
Name-Email: bpm@localhost
Expire-Date: 0
%no-protection
%commit
%echo Done`)
err = cmd.Run()
if err != nil {
return err
}
}
return nil
}
func IsKeyringInitialized(rootDir string) bool {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
// Check if gpg directory exists
if stat, err := os.Stat(gpgHomedir); err != nil || !stat.IsDir() {
return false
}
// Get number of secret keys
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-secret-keys", "--with-colons")
output, err := cmd.Output()
if err != nil {
return false
}
secretKeysLineCount := len(strings.Split(strings.TrimSpace(string(output)), "\n"))
// Return false if no signing key has been created
if secretKeysLineCount <= 1 {
return false
}
return true
}
func PopulateKeyring(rootDir string) error {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
keyringsDir := path.Join(rootDir, "/var/lib/bpm/keyrings")
dirEntries, err := os.ReadDir(keyringsDir)
if err != nil && !os.IsNotExist(err) {
return err
}
// Remove removed keys
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".revoked") {
continue
}
data, err := os.ReadFile(path.Join(keyringsDir, entry.Name()))
if err != nil {
return err
}
// Loop over all key IDs
for entry := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
// Ensure key ID exists
err := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-keys", entry).Run()
if err != nil {
continue
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--batch", "--yes", "--delete-secret-and-public-keys", entry)
err = cmd.Run()
if err != nil {
return err
}
}
}
// Import all keyrings
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".pgp") && !strings.HasSuffix(entry.Name(), ".asc") {
continue
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--import", path.Join(keyringsDir, entry.Name()))
err := cmd.Run()
if err != nil {
return err
}
}
// Trust keys
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
if !strings.HasSuffix(entry.Name(), ".trustdb") {
continue
}
data, err := os.ReadFile(path.Join(keyringsDir, entry.Name()))
if err != nil {
return err
}
// Loop over all key IDs
for entry := range strings.SplitSeq(strings.TrimSpace(string(data)), "\n") {
keyID := strings.Split(entry, ":")[0]
// Ensure key ID exists
err := exec.Command("gpg", "--homedir="+gpgHomedir, "--list-keys", keyID).Run()
if err != nil {
continue
}
// Sign key
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--command-fd=0", "--batch", "--lsign-key", keyID)
cmd.Stdin = strings.NewReader("y\ny\n")
err = cmd.Run()
if err != nil {
return err
}
}
// Import owner trust database
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--import-ownertrust", path.Join(keyringsDir, entry.Name()))
err = cmd.Run()
if err != nil {
return err
}
}
return nil
}
func VerifySignature(filename, signature string, requireTrusted bool, rootDir string) error {
gpgHomedir := path.Join(rootDir, "/var/lib/bpm/gpg")
if _, err := os.Stat(gpgHomedir); err != nil {
return err
}
cmd := exec.Command("gpg", "--homedir="+gpgHomedir, "--status-fd=3", "--verify", signature, filename)
pipeReader, pipeWriter, err := os.Pipe()
if err != nil {
return err
}
defer pipeReader.Close()
defer pipeWriter.Close()
cmd.ExtraFiles = append(cmd.ExtraFiles, pipeWriter)
err = cmd.Run()
if err != nil {
return err
}
pipeWriter.Close()
if requireTrusted {
data, err := io.ReadAll(pipeReader)
if err != nil {
return err
}
dataStr := string(data)
if !strings.Contains(dataStr, "[GNUPG:] TRUST_FULLY") && !strings.Contains(dataStr, "[GNUPG:] TRUST_ULTIMATE") {
return fmt.Errorf("signature verified but not trusted")
}
}
return err
}
+40 -61
View File
@@ -1,26 +1,27 @@
package bpmlib package bpmlib
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"gopkg.in/yaml.v3"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"syscall" "syscall"
"gopkg.in/yaml.v3"
) )
type BPMHook struct { type BPMHook struct {
SourcePath string SourcePath string
SourceContent string SourceContent string
TriggerOperations []string `yaml:"trigger_operations"` TriggerActions []string `yaml:"trigger_actions"`
TargetType string `yaml:"target_type"` TriggerPreOperation bool `yaml:"trigger_pre_operation"`
Targets []string `yaml:"targets"` Targets []string `yaml:"targets"`
Depends []string `yaml:"depends"`
Run string `yaml:"run"` Run string `yaml:"run"`
PassTargets bool `yaml:"pass_targets"`
} }
// createHook returns a BPMHook instance based on the content of the given string // createHook returns a BPMHook instance based on the content of the given string
@@ -35,10 +36,8 @@ func createHook(sourcePath string) (*BPMHook, error) {
hook := &BPMHook{ hook := &BPMHook{
SourcePath: sourcePath, SourcePath: sourcePath,
SourceContent: string(bytes), SourceContent: string(bytes),
TriggerOperations: nil, TriggerActions: nil,
TargetType: "",
Targets: nil, Targets: nil,
Depends: nil,
Run: "", Run: "",
} }
@@ -61,19 +60,15 @@ func (hook *BPMHook) IsValid() error {
ValidOperations := []string{"install", "upgrade", "remove"} ValidOperations := []string{"install", "upgrade", "remove"}
// Return error if any trigger operation is not valid or none are given // Return error if any trigger operation is not valid or none are given
if len(hook.TriggerOperations) == 0 { if len(hook.TriggerActions) == 0 {
return errors.New("no trigger operations specified") return errors.New("no trigger operations specified")
} }
for _, operation := range hook.TriggerOperations { for _, operation := range hook.TriggerActions {
if !slices.Contains(ValidOperations, operation) { if !slices.Contains(ValidOperations, operation) {
return errors.New("trigger operation '" + operation + "' is not valid") return errors.New("trigger operation '" + operation + "' is not valid")
} }
} }
if hook.TargetType != "package" && hook.TargetType != "path" {
return errors.New("target type '" + hook.TargetType + "' is not valid")
}
if len(hook.Run) == 0 { if len(hook.Run) == 0 {
return errors.New("command to run is empty") return errors.New("command to run is empty")
} }
@@ -83,55 +78,30 @@ func (hook *BPMHook) IsValid() error {
} }
// Execute hook if all conditions are met // Execute hook if all conditions are met
func (hook *BPMHook) Execute(packageChanges map[string]string, verbose bool, rootDir string) error { func (hook *BPMHook) Execute(modifiedFiles map[string]string, preOperation bool, verbose bool, rootDir string) error {
// Check if package dependencies are met
installedPackages, err := GetInstalledPackages(rootDir)
if err != nil {
return err
}
for _, depend := range hook.Depends {
if !slices.Contains(installedPackages, depend) {
return nil
}
}
// Get modified files slice
modifiedFiles := make([]*PackageFileEntry, 0)
for pkg := range packageChanges {
if GetPackage(pkg, rootDir) != nil {
modifiedFiles = append(modifiedFiles, GetPackage(pkg, rootDir).PkgFiles...)
}
}
// Check if any targets are met // Check if any targets are met
targetMet := false targetsMet := make([]string, 0)
for _, target := range hook.Targets { for _, target := range hook.Targets {
if targetMet { for modifiedFile, action := range modifiedFiles {
break // Check if this hook is triggered by this file's action
if !slices.Contains(hook.TriggerActions, action) {
continue
} }
if hook.TargetType == "package" {
for change, operation := range packageChanges { // Check if file has already been checked
if target == change && slices.Contains(hook.TriggerOperations, operation) { if slices.Contains(targetsMet, modifiedFile) {
targetMet = true continue
break }
if matched, _ := filepath.Match(target, modifiedFile); !matched {
continue
}
targetsMet = append(targetsMet, modifiedFile)
} }
} }
} else {
glob, err := filepath.Glob(path.Join(rootDir, target)) if len(targetsMet) == 0 {
if err != nil {
return err
}
for _, change := range modifiedFiles {
if slices.Contains(glob, path.Join(rootDir, change.Path)) {
targetMet = true
break
}
}
}
}
if !targetMet {
return nil return nil
} }
@@ -140,16 +110,25 @@ func (hook *BPMHook) Execute(packageChanges map[string]string, verbose bool, roo
cmd := exec.Command(splitCommand[0], splitCommand[1:]...) cmd := exec.Command(splitCommand[0], splitCommand[1:]...)
// Setup subprocess environment // Setup subprocess environment
cmd.Dir = "/" cmd.Dir = "/"
// Pass targets
if hook.PassTargets {
buffer := bytes.Buffer{}
buffer.WriteString(strings.Join(targetsMet, "\n") + "\n")
cmd.Stdin = &buffer
}
// Run hook in chroot if using the -R flag // Run hook in chroot if using the -R flag
if rootDir != "/" { if rootDir != "/" {
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir} cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir}
} }
if verbose { if !verbose {
fmt.Printf("Running hook (%s) with run command: %s\n", hook.SourcePath, strings.Join(splitCommand, " ")) fmt.Printf("Running hook (%s)\n", filepath.Base(hook.SourcePath))
} else {
fmt.Printf("Running hook (%s) with run command: %s\n", filepath.Base(hook.SourcePath), strings.Join(splitCommand, " "))
} }
err = cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
return err return err
} }
+226 -69
View File
@@ -1,32 +1,54 @@
package bpmlib package bpmlib
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"gopkg.in/yaml.v3"
) )
var localPackageInformation map[string]map[string]*BPMPackage = make(map[string]map[string]*BPMPackage) var persistentDataVersion int = 1
func initializeLocalPackageInformation(rootDir string) (err error) { var localPackageInformation map[string]map[string]*PackageInfo = make(map[string]map[string]*PackageInfo)
var installedVirtualPackages map[string]map[string][]*PackageInfo = make(map[string]map[string][]*PackageInfo)
func InitializeLocalPackageInformation(rootDir string) (err error) {
// Return if information is already initialized // Return if information is already initialized
if _, ok := localPackageInformation[rootDir]; ok { if _, ok := localPackageInformation[rootDir]; ok {
return nil return nil
} }
tempPackageInformation := make(map[string]*BPMPackage) tempPackageInformation := make(map[string]*PackageInfo)
tempInstalledVirtualPackages := make(map[string][]*PackageInfo)
// Get path to installed package information directory // Get paths
installedDir := path.Join(rootDir, "var/lib/bpm/installed/") persistentDataDir := path.Join(rootDir, "var/lib/bpm")
installedDir := path.Join(persistentDataDir, "installed")
// Ensure persistent data directory is up-to-date
if _, err := os.Stat(persistentDataDir); err == nil {
data, err := os.ReadFile(path.Join(persistentDataDir, ".version"))
if err != nil {
return fmt.Errorf("persistent data is not up-to-date! Please run 'bpm upgrade-persistent-data' first")
}
currentPersistentDataVersion, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
return fmt.Errorf("persistent data is not up-to-date! Please run 'bpm upgrade-persistent-data' first")
}
if currentPersistentDataVersion != persistentDataVersion {
return fmt.Errorf("persistent data is not up-to-date! Please run 'bpm upgrade-persistent-data' first")
}
}
// Get directory content // Get directory content
items, err := os.ReadDir(installedDir) items, err := os.ReadDir(installedDir)
if os.IsNotExist(err) { if os.IsNotExist(err) {
localPackageInformation[rootDir] = make(map[string]*BPMPackage) localPackageInformation[rootDir] = make(map[string]*PackageInfo)
return nil return nil
} }
if err != nil { if err != nil {
@@ -50,30 +72,68 @@ func initializeLocalPackageInformation(rootDir string) (err error) {
return err return err
} }
// Read package files
files := getPackageFiles(info.Name, rootDir)
// Add package to slice // Add package to slice
tempPackageInformation[info.Name] = &BPMPackage{ tempPackageInformation[info.Name] = info
PkgInfo: info,
PkgFiles: files, // Add virtual packages
for _, vpkg := range info.Provides {
tempInstalledVirtualPackages[vpkg] = append(tempInstalledVirtualPackages[vpkg], info)
} }
} }
localPackageInformation[rootDir] = tempPackageInformation localPackageInformation[rootDir] = tempPackageInformation
installedVirtualPackages[rootDir] = tempInstalledVirtualPackages
return nil return nil
} }
func GetInstalledPackages(rootDir string) (ret []string, err error) { func GetInstalledPackages(rootDir string) (ret []string, err error) {
// Initialize local package information // Initialize local package information
err = initializeLocalPackageInformation(rootDir) err = InitializeLocalPackageInformation(rootDir)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Loop through each package and add it to slice // Loop through each package and add it to slice
for _, bpmpkg := range localPackageInformation[rootDir] { for _, pkgInfo := range localPackageInformation[rootDir] {
ret = append(ret, bpmpkg.PkgInfo.Name) ret = append(ret, pkgInfo.Name)
}
// Sort packages
slices.Sort(ret)
return ret, nil
}
func GetPathOwners(path, rootDir string) (ret []string, err error) {
// Get absolte path to path
path, err = filepath.Abs(path)
if err != nil {
return
}
path, err = filepath.Rel(rootDir, path)
if err != nil {
return
}
// Trim leading and trailing slashes
path = strings.TrimLeft(path, "/")
path = strings.TrimRight(path, "/")
// Get installed packages
pkgs, err := GetInstalledPackages(rootDir)
if err != nil {
return
}
// Add packages that own path to list
for _, pkg := range pkgs {
pkgFiles := getPackageFiles(pkg, rootDir)
if slices.ContainsFunc(pkgFiles, func(entry *PackageFileEntry) bool {
return entry.Path == path
}) {
ret = append(ret, pkg)
}
} }
return ret, nil return ret, nil
@@ -81,7 +141,7 @@ func GetInstalledPackages(rootDir string) (ret []string, err error) {
func IsPackageInstalled(pkg, rootDir string) bool { func IsPackageInstalled(pkg, rootDir string) bool {
// Initialize local package information // Initialize local package information
err := initializeLocalPackageInformation(rootDir) err := InitializeLocalPackageInformation(rootDir)
if err != nil { if err != nil {
return false return false
} }
@@ -92,67 +152,43 @@ func IsPackageInstalled(pkg, rootDir string) bool {
return true return true
} }
func GetPackageInfo(pkg string, rootDir string) *PackageInfo { func GetVirtualPackageInfo(vpkg, rootDir string) []*PackageInfo {
// Get BPM package err := InitializeLocalPackageInformation(rootDir)
bpmpkg := GetPackage(pkg, rootDir) if err != nil {
// Return nil if not found
if bpmpkg == nil {
return nil return nil
} }
return bpmpkg.PkgInfo providers := installedVirtualPackages[rootDir][vpkg]
slices.SortFunc(providers, func(a, b *PackageInfo) int {
return strings.Compare(a.Name, b.Name)
})
return providers
} }
func IsVirtualPackage(pkg, rootDir string) (bool, string) { func GetPackageInfo(pkg string, rootDir string) *PackageInfo {
pkgs, err := GetInstalledPackages(rootDir) err := InitializeLocalPackageInformation(rootDir)
if err != nil { if err != nil {
return false, "" return nil
} }
for _, p := range pkgs {
if p == pkg {
return false, ""
}
i := GetPackageInfo(p, rootDir)
if i == nil {
continue
}
if slices.Contains(i.Provides, pkg) {
return true, p
}
}
return false, ""
}
func IsPackageProvided(pkg, rootDir string) bool { return localPackageInformation[rootDir][pkg]
pkgs, err := GetInstalledPackages(rootDir)
if err != nil {
return false
}
for _, p := range pkgs {
if p == pkg {
return true
}
i := GetPackageInfo(p, rootDir)
if i == nil {
continue
}
if slices.Contains(i.Provides, pkg) {
return true
}
}
return false
} }
func GetPackage(pkg, rootDir string) *BPMPackage { func GetPackage(pkg, rootDir string) *BPMPackage {
err := initializeLocalPackageInformation(rootDir) pkgInfo := GetPackageInfo(pkg, rootDir)
if err != nil { if pkgInfo == nil {
return nil return nil
} }
bpmpkg := localPackageInformation[rootDir][pkg] files := getPackageFiles(pkgInfo.Name, rootDir)
localInfo := getPackageLocalInfo(pkgInfo.Name, rootDir)
return bpmpkg return &BPMPackage{
PkgInfo: pkgInfo,
PkgFiles: files,
LocalInfo: localInfo,
}
} }
func GetAllPackageFiles(rootDir string, excludePackages ...string) (map[string][]*BPMPackage, error) { func GetAllPackageFiles(rootDir string, excludePackages ...string) (map[string][]*BPMPackage, error) {
@@ -169,14 +205,11 @@ func GetAllPackageFiles(rootDir string, excludePackages ...string) (map[string][
} }
bpmpkg := GetPackage(pkgName, rootDir) bpmpkg := GetPackage(pkgName, rootDir)
if bpmpkg == nil { if bpmpkg == nil {
return nil, errors.New(fmt.Sprintf("could not get BPM package (%s)", pkgName)) return nil, fmt.Errorf("could not get BPM package (%s)", pkgName)
} }
for _, entry := range bpmpkg.PkgFiles { for _, entry := range bpmpkg.PkgFiles {
if _, ok := ret[entry.Path]; ok {
ret[entry.Path] = append(ret[entry.Path], bpmpkg) ret[entry.Path] = append(ret[entry.Path], bpmpkg)
} else {
ret[entry.Path] = []*BPMPackage{bpmpkg}
}
} }
} }
@@ -226,7 +259,7 @@ func getPackageFiles(pkg, rootDir string) []*PackageFileEntry {
if err != nil { if err != nil {
return nil return nil
} }
size, err := strconv.ParseUint(stringEntry[len(stringEntry)-1], 0, 64) size, err := strconv.ParseInt(stringEntry[len(stringEntry)-1], 0, 64)
if err != nil { if err != nil {
return nil return nil
} }
@@ -241,3 +274,127 @@ func getPackageFiles(pkg, rootDir string) []*PackageFileEntry {
return pkgFiles return pkgFiles
} }
func getPackageLocalInfo(pkg, rootDir string) PackageLocalInfo {
localInfo := PackageLocalInfo{}
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
pkgDir := path.Join(installedDir, pkg)
localInfoFile := path.Join(path.Join(pkgDir, "local"))
if _, err := os.Stat(localInfoFile); os.IsNotExist(err) {
return localInfo
}
file, err := os.Open(localInfoFile)
if err != nil {
return localInfo
}
defer file.Close()
err = yaml.NewDecoder(file).Decode(&localInfo)
if err != nil {
return localInfo
}
return localInfo
}
func SetPackageLocalInfo(pkg string, localInfo PackageLocalInfo, rootDir string) error {
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
pkgDir := path.Join(installedDir, pkg)
localFile, err := os.OpenFile(path.Join(pkgDir, "local"), os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
defer localFile.Close()
err = yaml.NewEncoder(localFile).Encode(localInfo)
if err != nil {
return err
}
return nil
}
func UpgradePersistentData(rootDir string) error {
persistentDataDir := path.Join(rootDir, "var/lib/bpm")
// Create persistent data directory
os.MkdirAll(persistentDataDir, 0755)
// Upgrade installed package directories
dirEntries, err := os.ReadDir(path.Join(persistentDataDir, "installed"))
if err != nil && !os.IsNotExist(err) {
return err
} else if err == nil {
for _, entry := range dirEntries {
pkgDir := path.Join(persistentDataDir, "installed", entry.Name())
// Generate default local package information file
if _, err := os.Stat(path.Join(pkgDir, "local")); err != nil && !os.IsNotExist(err) {
return err
} else if os.IsNotExist(err) {
fmt.Printf("Generating local package information for package (%s)\n", entry.Name())
out, err := yaml.Marshal(PackageLocalInfo{
InstallationReason: "unknown",
InstalledOn: 0,
LastUpdatedOn: 0,
})
if err != nil {
return err
}
err = os.WriteFile(path.Join(pkgDir, "local"), out, 0644)
if err != nil {
return err
}
}
// Move installation reason to local package information file
if installationReason, err := os.ReadFile(path.Join(pkgDir, "installation_reason")); err != nil && !os.IsNotExist(err) {
return err
} else if err == nil {
fmt.Printf("Moving installation reason to local package information for package (%s)\n", entry.Name())
data, err := os.ReadFile(path.Join(pkgDir, "local"))
if err != nil {
return err
}
localInfo := &PackageLocalInfo{}
err = yaml.Unmarshal(data, localInfo)
if err != nil {
return err
}
localInfo.InstallationReason = strings.TrimSpace(string(installationReason))
out, err := yaml.Marshal(localInfo)
if err != nil {
return err
}
err = os.WriteFile(path.Join(pkgDir, "local"), out, 0644)
if err != nil {
return err
}
err = os.Remove(path.Join(pkgDir, "installation_reason"))
if err != nil {
return err
}
}
}
}
// Set persistent data version number
err = os.WriteFile(path.Join(persistentDataDir, ".version"), []byte(strconv.Itoa(persistentDataVersion)), 0644)
if err != nil {
return err
}
return nil
}
+58
View File
@@ -0,0 +1,58 @@
package bpmlib
import (
"fmt"
"io"
"net/http"
"os"
"path"
"strings"
)
func downloadFile(barText, u, filepath string, perm os.FileMode) error {
if strings.HasSuffix(filepath, "/") {
return fmt.Errorf("Filepath must not end in '/'")
}
req, err := http.NewRequest("GET", u, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Create parent directories
err = os.MkdirAll(path.Dir(filepath), 0755)
if err != nil {
return err
}
// Create file
file, err := os.Create(filepath)
if err != nil {
return err
}
defer file.Close()
// Create progress bar
bar := createProgressBar(resp.ContentLength, barText, barText == "")
defer bar.Close()
// Copy data
_, err = io.Copy(io.MultiWriter(file, bar), resp.Body)
if err != nil {
return err
}
// Set file permissions
err = file.Chmod(perm)
if err != nil {
return err
}
return nil
}
+426 -214
View File
@@ -8,84 +8,23 @@ import (
"path" "path"
"slices" "slices"
"strings" "strings"
"text/tabwriter"
) )
type BPMOperation struct { type BPMOperation struct {
Actions []OperationAction Actions []OperationAction
UnresolvedDepends []string UnresolvedDepends []string
Changes map[string]string ModifiedFiles map[string]string
CompilationJobs int
RunChecks bool
RootDir string RootDir string
compiledPackages map[string]string compiledPackages map[string]string
hasFetchedPackages bool
} }
func (operation *BPMOperation) ActionsContainPackage(pkg string) bool { func (operation *BPMOperation) GetTotalDownloadSize() int64 {
for _, action := range operation.Actions { var ret int64 = 0
if action.GetActionType() == "install" {
if action.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg {
return true
}
} else if action.GetActionType() == "fetch" {
if action.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg {
return true
}
} else if action.GetActionType() == "remove" {
if action.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg {
return true
}
}
}
return false
}
func (operation *BPMOperation) AppendAction(action OperationAction) {
operation.InsertActionAt(len(operation.Actions), action)
}
func (operation *BPMOperation) InsertActionAt(index int, action OperationAction) {
if len(operation.Actions) == index { // nil or empty slice or after last element
operation.Actions = append(operation.Actions, action)
} else {
operation.Actions = append(operation.Actions[:index+1], operation.Actions[index:]...) // index < len(a)
operation.Actions[index] = action
}
if action.GetActionType() == "install" {
pkgInfo := action.(*InstallPackageAction).BpmPackage.PkgInfo
if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) {
operation.Changes[pkgInfo.Name] = "install"
} else {
operation.Changes[pkgInfo.Name] = "upgrade"
}
} else if action.GetActionType() == "fetch" {
pkgInfo := action.(*FetchPackageAction).DatabaseEntry.Info
if !IsPackageInstalled(pkgInfo.Name, operation.RootDir) {
operation.Changes[pkgInfo.Name] = "install"
} else {
operation.Changes[pkgInfo.Name] = "upgrade"
}
} else if action.GetActionType() == "remove" {
operation.Changes[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = "remove"
}
}
func (operation *BPMOperation) RemoveAction(pkg, actionType string) {
operation.Actions = slices.DeleteFunc(operation.Actions, func(a OperationAction) bool {
if a.GetActionType() != actionType {
return false
}
if a.GetActionType() == "install" {
return a.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg
} else if a.GetActionType() == "fetch" {
return a.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg
} else if a.GetActionType() == "remove" {
return a.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg
}
return false
})
}
func (operation *BPMOperation) GetTotalDownloadSize() uint64 {
var ret uint64 = 0
for _, action := range operation.Actions { for _, action := range operation.Actions {
if action.GetActionType() == "fetch" { if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).DatabaseEntry.DownloadSize ret += action.(*FetchPackageAction).DatabaseEntry.DownloadSize
@@ -94,8 +33,8 @@ func (operation *BPMOperation) GetTotalDownloadSize() uint64 {
return ret return ret
} }
func (operation *BPMOperation) GetTotalInstalledSize() uint64 { func (operation *BPMOperation) GetTotalInstalledSize() int64 {
var ret uint64 = 0 var ret int64 = 0
for _, action := range operation.Actions { for _, action := range operation.Actions {
if action.GetActionType() == "install" { if action.GetActionType() == "install" {
ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize() ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize()
@@ -110,21 +49,25 @@ func (operation *BPMOperation) GetFinalActionSize(rootDir string) int64 {
var ret int64 = 0 var ret int64 = 0
for _, action := range operation.Actions { for _, action := range operation.Actions {
if action.GetActionType() == "install" { if action.GetActionType() == "install" {
ret += int64(action.(*InstallPackageAction).BpmPackage.GetInstalledSize()) ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize()
if IsPackageInstalled(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir) { if IsPackageInstalled(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir) {
ret -= int64(GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize()) ret -= GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize()
} }
} else if action.GetActionType() == "fetch" { } else if action.GetActionType() == "fetch" {
ret += int64(action.(*FetchPackageAction).DatabaseEntry.InstalledSize) ret += action.(*FetchPackageAction).DatabaseEntry.InstalledSize
if IsPackageInstalled(action.(*FetchPackageAction).DatabaseEntry.Info.Name, rootDir) {
ret -= action.(*FetchPackageAction).DatabaseEntry.InstalledSize
}
} else if action.GetActionType() == "remove" { } else if action.GetActionType() == "remove" {
ret -= int64(action.(*RemovePackageAction).BpmPackage.GetInstalledSize()) ret -= action.(*RemovePackageAction).BpmPackage.GetInstalledSize()
} }
} }
return ret return ret
} }
func (operation *BPMOperation) ResolveDependencies(reinstallDependencies, installOptionalDependencies, verbose bool) error { func (operation *BPMOperation) ResolveDependencies(installRuntimeDepends bool) {
pos := 0 // Discover resolved virtual packages
resolvedVirtualPackages := make(map[string]string)
for _, value := range slices.Clone(operation.Actions) { for _, value := range slices.Clone(operation.Actions) {
var pkgInfo *PackageInfo var pkgInfo *PackageInfo
if value.GetActionType() == "install" { if value.GetActionType() == "install" {
@@ -134,61 +77,63 @@ func (operation *BPMOperation) ResolveDependencies(reinstallDependencies, instal
action := value.(*FetchPackageAction) action := value.(*FetchPackageAction)
pkgInfo = action.DatabaseEntry.Info pkgInfo = action.DatabaseEntry.Info
} else { } else {
pos++
continue continue
} }
resolved, unresolved := ResolveAllPackageDependenciesFromDatabases(pkgInfo, pkgInfo.Type == "source", installOptionalDependencies, !reinstallDependencies, verbose, operation.RootDir) for _, vpkg := range pkgInfo.Provides {
if _, ok := resolvedVirtualPackages[vpkg]; !ok {
resolvedVirtualPackages[vpkg] = pkgInfo.Name
}
}
}
// Discover all dependencies
newActions := make([]OperationAction, 0)
for _, value := range operation.Actions {
var pkgInfo *PackageInfo
if value.GetActionType() == "install" {
action := value.(*InstallPackageAction)
pkgInfo = action.BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
action := value.(*FetchPackageAction)
pkgInfo = action.DatabaseEntry.Info
} else {
continue
}
resolved, unresolved := ResolveDependencies(pkgInfo, resolvedVirtualPackages, installRuntimeDepends, operation.RootDir)
// Append unresolved dependencies
operation.UnresolvedDepends = append(operation.UnresolvedDepends, unresolved...) operation.UnresolvedDepends = append(operation.UnresolvedDepends, unresolved...)
operation.UnresolvedDepends = removeDuplicates(operation.UnresolvedDepends)
for _, resolvedPkg := range resolved { for _, resolvedPkg := range resolved {
if !operation.ActionsContainPackage(resolvedPkg.PkgName) && resolvedPkg.PkgName != pkgInfo.Name { if ActionSliceIndex(newActions, resolvedPkg.DatabaseEntry.Info.Name) == -1 { // Dependency not in actions slice
if !reinstallDependencies && IsPackageInstalled(resolvedPkg.PkgName, operation.RootDir) { var action OperationAction = &FetchPackageAction{
continue
}
entry, _, err := GetDatabaseEntry(resolvedPkg.PkgName)
if err != nil {
return errors.New("could not get database entry for package (" + resolvedPkg.PkgName + ")")
}
operation.InsertActionAt(pos, &FetchPackageAction{
InstallationReason: resolvedPkg.InstallationReason, InstallationReason: resolvedPkg.InstallationReason,
DatabaseEntry: entry, DatabaseEntry: resolvedPkg.DatabaseEntry,
})
pos++
}
}
pos++
} }
newActions = append(newActions, action)
return nil for _, vpkg := range resolvedPkg.DatabaseEntry.Info.Provides {
} if _, ok := resolvedVirtualPackages[vpkg]; !ok {
resolvedVirtualPackages[vpkg] = resolvedPkg.DatabaseEntry.Info.Name
func (operation *BPMOperation) RemoveNeededPackages() error {
removeActions := make(map[string]*RemovePackageAction)
for _, action := range slices.Clone(operation.Actions) {
if action.GetActionType() == "remove" {
removeActions[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = action.(*RemovePackageAction)
} }
} }
for pkg, action := range removeActions { // Check if can move original action
dependants, err := GetPackageDependants(action.BpmPackage.PkgInfo.Name, operation.RootDir) if i := ActionSliceIndex(operation.Actions, resolvedPkg.DatabaseEntry.Info.Name); i != -1 {
if err != nil { newActions[len(newActions)-1] = operation.Actions[i]
return errors.New("could not get dependant packages for package (" + pkg + ")")
} }
dependants = slices.DeleteFunc(dependants, func(d string) bool {
if _, ok := removeActions[d]; ok {
return true
}
return false
})
if len(dependants) != 0 {
operation.RemoveAction(pkg, action.GetActionType())
} }
} }
return nil if ActionSliceIndex(newActions, pkgInfo.Name) == -1 {
newActions = append(newActions, value)
}
}
operation.Actions = newActions
} }
func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error { func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error {
@@ -199,11 +144,11 @@ func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error {
} }
installedPackages := make([]*PackageInfo, len(installedPackageNames)) installedPackages := make([]*PackageInfo, len(installedPackageNames))
for i, value := range installedPackageNames { for i, value := range installedPackageNames {
bpmpkg := GetPackage(value, operation.RootDir) pkgInfo := GetPackageInfo(value, operation.RootDir)
if bpmpkg == nil { if pkgInfo == nil {
return errors.New("could not find installed package (" + value + ")") return errors.New("could not find installed package (" + value + ")")
} }
installedPackages[i] = bpmpkg.PkgInfo installedPackages[i] = pkgInfo
} }
// Get packages to remove // Get packages to remove
@@ -214,34 +159,70 @@ func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error {
} }
} }
// Get manually installed packages, resolve all their dependencies and add them to the keepPackages slice // Run BFS on all manually installed packages
keepPackages := make([]string, 0) visited := make([]string, 0)
for _, pkg := range slices.Clone(installedPackages) { for _, pkg := range installedPackages {
if GetInstallationReason(pkg.Name, operation.RootDir) != InstallationReasonManual { if getPackageLocalInfo(pkg.Name, operation.RootDir).GetInstallationReason() != InstallationReasonManual {
continue continue
} }
// Do not resolve dependencies or add package to keepPackages slice if package removal action exists for it queue := make([]*PackageInfo, 0)
if _, ok := removeActions[pkg.Name]; ok {
queue = append(queue, pkg)
for len(queue) > 0 {
v := queue[len(queue)-1]
queue = queue[:len(queue)-1]
// Skip package if it's to be removed
if _, ok := removeActions[v.Name]; ok {
continue continue
} }
keepPackages = append(keepPackages, pkg.Name) // Mark package as visited
resolved := pkg.GetAllDependencies(!cleanupMakeDepends, true, operation.RootDir) visited = append(visited, v.Name)
for _, value := range resolved {
if !slices.Contains(keepPackages, value) { // Get all package dependencies
keepPackages = append(keepPackages, value) depends := v.Depends
depends = append(depends, v.RuntimeDepends...)
if cleanupMakeDepends && v.Type == "source" {
depends = append(depends, v.MakeDepends...)
depends = append(depends, v.CheckDepends...)
}
// Loop through all dependencies
for _, depend := range depends {
// Remove required version
depend, _, _ = SplitPkgNameAndVersion(depend)
// Resolve dependency
var dependPkgInfo *PackageInfo
if providers := GetVirtualPackageInfo(depend, operation.RootDir); len(providers) > 0 {
dependPkgInfo = providers[0]
} else {
dependPkgInfo = GetPackageInfo(depend, operation.RootDir)
}
if dependPkgInfo == nil {
continue
}
// Mark dependency as visited and add it to the queue
if !slices.Contains(visited, dependPkgInfo.Name) {
visited = append(visited, dependPkgInfo.Name)
queue = append(queue, dependPkgInfo)
}
} }
} }
} }
// Get all installed packages that are not in the keepPackages slice and add them to the BPM operation // Remove all packages that were not discovered after running BFS
for _, pkg := range installedPackageNames { for _, pkg := range installedPackageNames {
// Do not add package removal action if there already is one // Do not add package removal action if there already is one
if _, ok := removeActions[pkg]; ok { if _, ok := removeActions[pkg]; ok {
continue continue
} }
if !slices.Contains(keepPackages, pkg) {
if !slices.Contains(visited, pkg) {
bpmpkg := GetPackage(pkg, operation.RootDir) bpmpkg := GetPackage(pkg, operation.RootDir)
if bpmpkg == nil { if bpmpkg == nil {
return errors.New("Error: could not find installed package (" + pkg + ")") return errors.New("Error: could not find installed package (" + pkg + ")")
@@ -268,63 +249,102 @@ func (operation *BPMOperation) ReplaceObsoletePackages() {
} }
for _, r := range pkgInfo.Replaces { for _, r := range pkgInfo.Replaces {
if bpmpkg := GetPackage(r, operation.RootDir); bpmpkg != nil && !operation.ActionsContainPackage(bpmpkg.PkgInfo.Name) { if bpmpkg := GetPackage(r, operation.RootDir); bpmpkg != nil && ActionSliceIndex(operation.Actions, bpmpkg.PkgInfo.Name) == -1 {
operation.InsertActionAt(0, &RemovePackageAction{ var action OperationAction = &RemovePackageAction{
BpmPackage: bpmpkg, BpmPackage: bpmpkg,
}) }
operation.Actions = slices.Insert(operation.Actions, 0, action)
} }
} }
} }
} }
func (operation *BPMOperation) CheckForConflicts() (map[string][]string, error) { func (operation *BPMOperation) CheckForConflicts() map[string][]string {
conflicts := make(map[string][]string) conflicts := make(map[string][]string)
installedPackages, err := GetInstalledPackages(operation.RootDir)
if err != nil {
return nil, err
}
allPackages := make([]*PackageInfo, len(installedPackages))
for i, value := range installedPackages {
bpmpkg := GetPackage(value, operation.RootDir)
if bpmpkg == nil {
return nil, fmt.Errorf("could not find installed package (%s)", value)
}
allPackages[i] = bpmpkg.PkgInfo
}
// Add all new packages to the allPackages slice // Get installed packages
installedPackages := localPackageInformation[operation.RootDir]
// Get packages to be removed
removedPackages := make([]string, 0)
for _, value := range slices.Clone(operation.Actions) { for _, value := range slices.Clone(operation.Actions) {
if value.GetActionType() != "remove" {
continue
}
removedPackages = append(removedPackages, value.(*RemovePackageAction).BpmPackage.PkgInfo.Name)
}
// Check for conflicts
for _, value := range slices.Clone(operation.Actions) {
var pkgInfo *PackageInfo
if value.GetActionType() == "install" { if value.GetActionType() == "install" {
action := value.(*InstallPackageAction) pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo
pkgInfo := action.BpmPackage.PkgInfo
allPackages = append(allPackages, pkgInfo)
} else if value.GetActionType() == "fetch" { } else if value.GetActionType() == "fetch" {
action := value.(*FetchPackageAction) pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info
pkgInfo := action.DatabaseEntry.Info } else {
allPackages = append(allPackages, pkgInfo) continue
} else if value.GetActionType() == "remove" { }
action := value.(*RemovePackageAction)
pkgInfo := action.BpmPackage.PkgInfo // Check for conflicts with installed packages
for i := len(allPackages) - 1; i >= 0; i-- { for _, installedPkg := range installedPackages {
info := allPackages[i] // Skip if package is to be removed
if info.Name == pkgInfo.Name { if slices.Contains(removedPackages, installedPkg.Name) {
allPackages = append(allPackages[:i], allPackages[i+1:]...) continue
}
// Skip if same package
if pkgInfo.Name == installedPkg.Name {
continue
}
// Check for new package conflicts
if slices.Contains(pkgInfo.Conflicts, installedPkg.Name) {
conflicts[pkgInfo.Name] = append(conflicts[pkgInfo.Name], installedPkg.Name)
}
for _, vpkg := range installedPkg.Provides {
if slices.Contains(pkgInfo.Conflicts, vpkg) {
conflicts[pkgInfo.Name] = append(conflicts[pkgInfo.Name], vpkg+" ("+installedPkg.Name+")")
}
}
// Check for installed package conflicts
for _, vpkg := range pkgInfo.Provides {
if slices.Contains(installedPkg.Conflicts, vpkg) {
conflicts[installedPkg.Name] = append(conflicts[installedPkg.Name], vpkg+" ("+pkgInfo.Name+")")
}
}
}
// Check for conflicts with other new packages
for _, value := range slices.Clone(operation.Actions) {
var pkgInfo2 *PackageInfo
if value.GetActionType() == "install" {
pkgInfo2 = value.(*InstallPackageAction).BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
pkgInfo2 = value.(*FetchPackageAction).DatabaseEntry.Info
} else {
continue
}
// Skip if same package
if pkgInfo.Name == pkgInfo2.Name {
continue
}
// Check for other package conflicts
if slices.Contains(pkgInfo.Conflicts, pkgInfo2.Name) {
conflicts[pkgInfo.Name] = append(conflicts[pkgInfo.Name], pkgInfo2.Name)
}
for _, vpkg := range pkgInfo2.Provides {
if slices.Contains(pkgInfo.Conflicts, vpkg) {
conflicts[pkgInfo.Name] = append(conflicts[pkgInfo.Name], vpkg+" ("+pkgInfo2.Name+")")
} }
} }
} }
} }
for _, value := range allPackages { return conflicts
for _, conflict := range value.Conflicts {
if slices.ContainsFunc(allPackages, func(info *PackageInfo) bool {
return info.Name == conflict
}) {
conflicts[value.Name] = append(conflicts[value.Name], conflict)
}
}
}
return conflicts, nil
} }
func (operation *BPMOperation) ShowOperationSummary() { func (operation *BPMOperation) ShowOperationSummary() {
@@ -333,6 +353,9 @@ func (operation *BPMOperation) ShowOperationSummary() {
return return
} }
writer := tabwriter.NewWriter(os.Stdout, 6, 4, 6, ' ', 0)
fmt.Fprintln(writer, "Name\tVersion\tAction\tInstallation Reason\tFrom Source")
for _, value := range operation.Actions { for _, value := range operation.Actions {
var pkgInfo *PackageInfo var pkgInfo *PackageInfo
var installationReason = InstallationReasonUnknown var installationReason = InstallationReasonUnknown
@@ -347,55 +370,132 @@ func (operation *BPMOperation) ShowOperationSummary() {
pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info
} else { } else {
pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo
fmt.Printf("%s: %s (Remove)\n", pkgInfo.Name, pkgInfo.GetFullVersion()) fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Remove", "-", "-")
continue continue
} }
installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir) installationReasonStr := ""
additionalInfo := ""
switch installationReason { switch installationReason {
case InstallationReasonManual: case InstallationReasonManual:
additionalInfo = "(Manual)" installationReasonStr = "Manual"
case InstallationReasonDependency: case InstallationReasonDependency:
additionalInfo = "(Dependency)" installationReasonStr = "Dependency"
case InstallationReasonMakeDependency: case InstallationReasonMakeDependency:
additionalInfo = "(Make dependency)" installationReasonStr = "Make dependency"
default: default:
additionalInfo = "(Unknown)" installationReasonStr = "Unknown"
}
if pkgInfo.Type == "source" {
additionalInfo += " (From Source)"
} }
installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir)
if installedInfo == nil { if installedInfo == nil {
fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Install", installationReasonStr, pkgInfo.Type == "source")
} else { } else {
comparison := ComparePackageVersions(*pkgInfo, *installedInfo) comparison := CompareVersions(pkgInfo.GetFullVersion(), installedInfo.GetFullVersion())
if comparison < 0 { if comparison < 0 {
fmt.Printf("%s: %s -> %s (Downgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Downgrade", installationReasonStr, pkgInfo.Type == "source")
} else if comparison > 0 { } else if comparison > 0 {
fmt.Printf("%s: %s -> %s (Upgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s -> %s\t%s\t%s\t%t\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), "Upgrade", installationReasonStr, pkgInfo.Type == "source")
} else { } else {
fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), additionalInfo) fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Reinstall", installationReasonStr, pkgInfo.Type == "source")
} }
} }
} }
writer.Flush()
fmt.Println()
if operation.RootDir != "/" { if operation.RootDir != "/" {
fmt.Println("Warning: Operating in " + operation.RootDir) fmt.Println("Warning: Operating in " + operation.RootDir)
} }
if operation.GetTotalDownloadSize() > 0 { if operation.GetTotalDownloadSize() > 0 {
fmt.Printf("%s will be downloaded to complete this operation\n", unsignedBytesToHumanReadable(operation.GetTotalDownloadSize())) fmt.Printf("%s will be downloaded to complete this operation\n", BytesToHumanReadable(operation.GetTotalDownloadSize()))
} }
if operation.GetFinalActionSize(operation.RootDir) > 0 { if operation.GetFinalActionSize(operation.RootDir) > 0 {
fmt.Printf("A total of %s will be installed after the operation finishes\n", bytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir))) fmt.Printf("A total of %s will be installed after the operation finishes\n", BytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)))
} else if operation.GetFinalActionSize(operation.RootDir) < 0 { } else if operation.GetFinalActionSize(operation.RootDir) < 0 {
fmt.Printf("A total of %s will be freed after the operation finishes\n", strings.TrimPrefix(bytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)), "-")) fmt.Printf("A total of %s will be freed after the operation finishes\n", strings.TrimPrefix(BytesToHumanReadable(operation.GetFinalActionSize(operation.RootDir)), "-"))
} }
} }
func (operation *BPMOperation) RunHooks(verbose bool) error { func (operation *BPMOperation) ShowSourcePackageContent() (sourcePackagesShown int, err error) {
// Fetch packages
if !operation.hasFetchedPackages {
err = operation.FetchPackages()
if err != nil {
return 0, err
}
}
for _, action := range operation.Actions {
if action.GetActionType() != "install" {
continue
}
if action.(*InstallPackageAction).BpmPackage.PkgInfo.Type != "source" {
continue
}
err = showPackageFiles(action.(*InstallPackageAction).File)
if err != nil {
return 0, err
}
sourcePackagesShown++
}
return sourcePackagesShown, nil
}
func (operation *BPMOperation) GetOptionalDependencies() (optionalDepends map[string][]string) {
optionalDepends = make(map[string][]string)
// Find all optional dependencies
for _, value := range slices.Clone(operation.Actions) {
var pkgInfo *PackageInfo
if value.GetActionType() == "install" {
action := value.(*InstallPackageAction)
pkgInfo = action.BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
action := value.(*FetchPackageAction)
pkgInfo = action.DatabaseEntry.Info
} else {
continue
}
for _, depend := range pkgInfo.OptionalDepends {
// Get optional dependency name
dependSplit := strings.SplitN(depend, ":", 2)
dependName, _, _ := SplitPkgNameAndVersion(dependSplit[0])
// Skip if dependency is already installed
if IsPackageInstalled(dependName, operation.RootDir) {
continue
}
// Skip if not a new dependency of the package
if installedPkg := GetPackage(pkgInfo.Name, operation.RootDir); installedPkg != nil && slices.ContainsFunc(installedPkg.PkgInfo.OptionalDepends, func(n string) bool {
// Remove optional dependency comment
n = strings.SplitN(n, ":", 2)[0]
// Remove required version
n, _, _ = SplitPkgNameAndVersion(n)
return n == dependName
}) {
continue
}
if len(dependSplit) == 2 {
optionalDepends[pkgInfo.Name] = append(optionalDepends[pkgInfo.Name], fmt.Sprintf("%s (%s)", dependSplit[0], dependSplit[1]))
} else {
optionalDepends[pkgInfo.Name] = append(optionalDepends[pkgInfo.Name], dependName)
}
}
}
return
}
func (operation *BPMOperation) RunPreHooks(verbose bool) error {
// Return if hooks directory does not exist // Return if hooks directory does not exist
if stat, err := os.Stat(path.Join(operation.RootDir, "var/lib/bpm/hooks")); err != nil || !stat.IsDir() { if stat, err := os.Stat(path.Join(operation.RootDir, "var/lib/bpm/hooks")); err != nil || !stat.IsDir() {
return nil return nil
@@ -415,7 +515,11 @@ func (operation *BPMOperation) RunHooks(verbose bool) error {
log.Printf("Error while reading hook (%s): %s", entry.Name(), err) log.Printf("Error while reading hook (%s): %s", entry.Name(), err)
} }
err = hook.Execute(operation.Changes, verbose, operation.RootDir) if !hook.TriggerPreOperation {
continue
}
err = hook.Execute(operation.ModifiedFiles, false, verbose, operation.RootDir)
if err != nil { if err != nil {
log.Printf("Warning: could not execute hook (%s): %s\n", entry.Name(), err) log.Printf("Warning: could not execute hook (%s): %s\n", entry.Name(), err)
continue continue
@@ -426,7 +530,42 @@ func (operation *BPMOperation) RunHooks(verbose bool) error {
return nil return nil
} }
func (operation *BPMOperation) Execute(verbose, force bool) (err error) { func (operation *BPMOperation) RunPostHooks(verbose bool) error {
// Return if hooks directory does not exist
if stat, err := os.Stat(path.Join(operation.RootDir, "var/lib/bpm/hooks")); err != nil || !stat.IsDir() {
return nil
}
// Get directory entries in hooks directory
dirEntries, err := os.ReadDir(path.Join(operation.RootDir, "var/lib/bpm/hooks"))
if err != nil {
return err
}
// Find all hooks, validate and execute them
for _, entry := range dirEntries {
if entry.Type().IsRegular() && strings.HasSuffix(entry.Name(), ".bpmhook") {
hook, err := createHook(path.Join(operation.RootDir, "var/lib/bpm/hooks", entry.Name()))
if err != nil {
log.Printf("Error while reading hook (%s): %s", entry.Name(), err)
}
if hook.TriggerPreOperation {
continue
}
err = hook.Execute(operation.ModifiedFiles, false, verbose, operation.RootDir)
if err != nil {
log.Printf("Warning: could not execute hook (%s): %s\n", entry.Name(), err)
continue
}
}
}
return nil
}
func (operation *BPMOperation) FetchPackages() (err error) {
// Fetch packages from databases // Fetch packages from databases
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "fetch" return action.GetActionType() == "fetch"
@@ -448,7 +587,7 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
var bpmpkg *BPMPackage var bpmpkg *BPMPackage
// Check if package has already been fetched from download link // Check if package has already been fetched from download link
if _, ok := fetchedPackages[entry.Download]; !ok { if fetchedFilepath, ok := fetchedPackages[entry.Filepath]; !ok {
// Fetch package from database // Fetch package from database
fetchedPackage, err := entry.Database.FetchPackage(entry.Info.Name) fetchedPackage, err := entry.Database.FetchPackage(entry.Info.Name)
if err != nil { if err != nil {
@@ -462,29 +601,35 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
} }
// Add fetched package to map // Add fetched package to map
fetchedPackages[entry.Download] = fetchedPackage fetchedPackages[entry.Filepath] = fetchedPackage
fmt.Printf("Package (%s) was successfully fetched!\n", entry.Info.Name)
} else { } else {
// Read fetched package // Read fetched package
bpmpkg, err = ReadPackage(fetchedPackages[entry.Download]) bpmpkg, err = ReadPackage(fetchedFilepath)
if err != nil { if err != nil {
return fmt.Errorf("could not read package (%s): %s\n", entry.Info.Name, err) return fmt.Errorf("could not read package (%s): %s\n", entry.Info.Name, err)
} }
fmt.Printf("Package (%s) was successfully fetched!\n", entry.Info.Name) // Get size of fetched archive
stat, err := os.Stat(fetchedFilepath)
if err != nil {
return err
}
bar := createProgressBar(stat.Size(), "Downloading "+entry.Info.Name, false)
bar.Set64(stat.Size())
bar.Close()
} }
if bpmpkg.PkgInfo.IsSplitPackage() { if bpmpkg.PkgInfo.IsSplitPackage() {
operation.Actions[i] = &InstallPackageAction{ operation.Actions[i] = &InstallPackageAction{
File: fetchedPackages[entry.Download], File: fetchedPackages[entry.Filepath],
InstallationReason: action.(*FetchPackageAction).InstallationReason, InstallationReason: action.(*FetchPackageAction).InstallationReason,
BpmPackage: bpmpkg, BpmPackage: bpmpkg,
SplitPackageToInstall: entry.Info.Name, SplitPackageToInstall: entry.Info.Name,
} }
} else { } else {
operation.Actions[i] = &InstallPackageAction{ operation.Actions[i] = &InstallPackageAction{
File: fetchedPackages[entry.Download], File: fetchedPackages[entry.Filepath],
InstallationReason: action.(*FetchPackageAction).InstallationReason, InstallationReason: action.(*FetchPackageAction).InstallationReason,
BpmPackage: bpmpkg, BpmPackage: bpmpkg,
} }
@@ -492,6 +637,44 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
} }
} }
operation.hasFetchedPackages = true
return nil
}
func (operation *BPMOperation) GetModifiedFiles() {
// Get modified files
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
installAction := action.(*InstallPackageAction)
isUpgrade := IsPackageInstalled(installAction.BpmPackage.PkgInfo.Name, operation.RootDir)
for _, pkgFile := range installAction.BpmPackage.PkgFiles {
operation.ModifiedFiles[pkgFile.Path] = "install"
if isUpgrade {
operation.ModifiedFiles[pkgFile.Path] = "upgrade"
}
}
}
if action.GetActionType() == "remove" {
removeAction := action.(*RemovePackageAction)
for _, pkgFile := range removeAction.BpmPackage.PkgFiles {
operation.ModifiedFiles[pkgFile.Path] = "remove"
}
}
}
}
func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
// Fetch packages
if !operation.hasFetchedPackages {
err = operation.FetchPackages()
if err != nil {
return err
}
}
// Determine words to be used for the following message // Determine words to be used for the following message
words := make([]string, 0) words := make([]string, 0)
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool { if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
@@ -546,7 +729,7 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
// Compile source package if not compiled already // Compile source package if not compiled already
if _, ok := operation.compiledPackages[pkgNameToInstall]; !ok { if _, ok := operation.compiledPackages[pkgNameToInstall]; !ok {
outputBpmPackages, err := CompileSourcePackage(value.File, compiledDir, false) outputBpmPackages, err := CompileSourcePackage(value.File, compiledDir, operation.CompilationJobs, !operation.RunChecks, false, verbose)
if err != nil { if err != nil {
return fmt.Errorf("could not compile source package (%s): %s\n", value.File, err) return fmt.Errorf("could not compile source package (%s): %s\n", value.File, err)
} }
@@ -566,24 +749,15 @@ func (operation *BPMOperation) Execute(verbose, force bool) (err error) {
} }
if value.InstallationReason != InstallationReasonManual { if value.InstallationReason != InstallationReasonManual {
err = installPackage(fileToInstall, operation.RootDir, verbose, true) err = installPackage(fileToInstall, value.InstallationReason, operation.RootDir, verbose, true)
} else { } else {
err = installPackage(fileToInstall, operation.RootDir, verbose, force) err = installPackage(fileToInstall, value.InstallationReason, operation.RootDir, verbose, force)
} }
if err != nil { if err != nil {
return fmt.Errorf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err) return fmt.Errorf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err)
} }
// Set installed package's installation reason
err = SetInstallationReason(bpmpkg.PkgInfo.Name, value.InstallationReason, operation.RootDir)
if err != nil {
return fmt.Errorf("could not set installation reason for package (%s): %s\n", value.BpmPackage.PkgInfo.Name, err)
}
fmt.Printf("Package (%s) was successfully installed\n", bpmpkg.PkgInfo.Name)
} }
} }
fmt.Println("Operation complete!")
return nil return nil
} }
@@ -619,3 +793,41 @@ type RemovePackageAction struct {
func (action *RemovePackageAction) GetActionType() string { func (action *RemovePackageAction) GetActionType() string {
return "remove" return "remove"
} }
func ActionSliceIndex(actions []OperationAction, pkg string) int {
for i, action := range actions {
if action.GetActionType() == "install" {
if action.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg {
return i
}
} else if action.GetActionType() == "fetch" {
if action.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg {
return i
}
} else if action.GetActionType() == "remove" {
if action.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg {
return i
}
}
}
return -1
}
func ActionSliceRemove(actions []OperationAction, pkg, actionType string) []OperationAction {
actions = slices.DeleteFunc(actions, func(a OperationAction) bool {
if a.GetActionType() != actionType {
return false
}
if a.GetActionType() == "install" {
return a.(*InstallPackageAction).BpmPackage.PkgInfo.Name == pkg
} else if a.GetActionType() == "fetch" {
return a.(*FetchPackageAction).DatabaseEntry.Info.Name == pkg
} else if a.GetActionType() == "remove" {
return a.(*RemovePackageAction).BpmPackage.PkgInfo.Name == pkg
}
return false
})
return actions
}
+364 -138
View File
@@ -10,50 +10,82 @@ import (
"os" "os"
"os/exec" "os/exec"
"path" "path"
"path/filepath"
"regexp"
"slices" "slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"syscall" "syscall"
"time"
version "github.com/knqyf263/go-rpm-version"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
type BPMPackage struct { type BPMPackage struct {
PkgInfo *PackageInfo PkgInfo *PackageInfo
PkgFiles []*PackageFileEntry PkgFiles []*PackageFileEntry
LocalInfo PackageLocalInfo
} }
type PackageInfo struct { type PackageInfo struct {
Name string `yaml:"name,omitempty"` Name string `yaml:"name"`
Description string `yaml:"description,omitempty"` Description string `yaml:"description,omitempty"`
Version string `yaml:"version,omitempty"` Version string `yaml:"version,omitempty"`
Revision int `yaml:"revision,omitempty"` Revision int `yaml:"revision,omitempty"`
Url string `yaml:"url,omitempty"` Url string `yaml:"url,omitempty"`
License string `yaml:"license,omitempty"` License string `yaml:"license,omitempty"`
Maintainers []string `yaml:"maintainers,omitempty"`
Arch string `yaml:"architecture,omitempty"` Arch string `yaml:"architecture,omitempty"`
OutputArch string `yaml:"output_architecture,omitempty"`
Type string `yaml:"type,omitempty"` Type string `yaml:"type,omitempty"`
Keep []string `yaml:"keep,omitempty"` Keep []string `yaml:"keep,omitempty"`
Depends []string `yaml:"depends,omitempty"` Depends []string `yaml:"depends,omitempty"`
MakeDepends []string `yaml:"make_depends,omitempty"` RuntimeDepends []string `yaml:"runtime_depends,omitempty"`
OptionalDepends []string `yaml:"optional_depends,omitempty"` OptionalDepends []string `yaml:"optional_depends,omitempty"`
MakeDepends []string `yaml:"make_depends,omitempty"`
CheckDepends []string `yaml:"check_depends,omitempty"`
Conflicts []string `yaml:"conflicts,omitempty"` Conflicts []string `yaml:"conflicts,omitempty"`
Replaces []string `yaml:"replaces,omitempty"` Replaces []string `yaml:"replaces,omitempty"`
Provides []string `yaml:"provides,omitempty"` Provides []string `yaml:"provides,omitempty"`
Options []string `yaml:"options,omitempty"`
Downloads []PackageDownload `yaml:"downloads,omitempty"`
SplitPackages []*PackageInfo `yaml:"split_packages,omitempty"` SplitPackages []*PackageInfo `yaml:"split_packages,omitempty"`
} }
type PackageDownload struct {
Url string `yaml:"url"`
Type string `yaml:"type,omitempty"`
Filepath string `yaml:"filepath,omitempty"`
// Archive options
NoExtract bool `yaml:"no_extract,omitempty"`
ExtractTo string `yaml:"extract_to,omitempty"`
ExtractStripComponents int `yaml:"extract_strip_components,omitempty"`
// Git options
CloneTo string `yaml:"clone_to,omitempty"`
GitBranch string `yaml:"git_branch,omitempty"`
Checksum string `yaml:"checksum,omitempty"`
}
type PackageFileEntry struct { type PackageFileEntry struct {
Path string Path string
OctalPerms uint32 OctalPerms uint32
UserID int UserID int
GroupID int GroupID int
SizeInBytes uint64 SizeInBytes int64
} }
func (pkg *BPMPackage) GetInstalledSize() uint64 { type PackageLocalInfo struct {
var totalSize uint64 = 0 InstallationReason string `yaml:"installation_reason"`
InstalledOn int64 `yaml:"installed_on"`
LastUpdatedOn int64 `yaml:"last_updated_on"`
}
func (pkg *BPMPackage) GetInstalledSize() int64 {
var totalSize int64 = 0
for _, entry := range pkg.PkgFiles { for _, entry := range pkg.PkgFiles {
totalSize += entry.SizeInBytes totalSize += entry.SizeInBytes
} }
@@ -100,42 +132,17 @@ const (
InstallationReasonUnknown InstallationReason = "unknown" InstallationReasonUnknown InstallationReason = "unknown"
) )
func ComparePackageVersions(info1, info2 PackageInfo) int { func (localInfo PackageLocalInfo) GetInstallationReason() InstallationReason {
v1 := version.NewVersion(info1.GetFullVersion()) switch localInfo.InstallationReason {
v2 := version.NewVersion(info2.GetFullVersion()) case "manual":
return v1.Compare(v2)
}
func GetInstallationReason(pkg, rootDir string) InstallationReason {
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
pkgDir := path.Join(installedDir, pkg)
if stat, err := os.Stat(path.Join(pkgDir, "installation_reason")); err != nil || stat.IsDir() {
return InstallationReasonManual return InstallationReasonManual
} case "dependency":
b, err := os.ReadFile(path.Join(pkgDir, "installation_reason"))
if err != nil {
return InstallationReasonUnknown
}
reason := strings.TrimSpace(string(b))
if reason == "manual" {
return InstallationReasonManual
} else if reason == "dependency" {
return InstallationReasonDependency return InstallationReasonDependency
} else if reason == "make_dependency" { case "make_dependency":
return InstallationReasonMakeDependency return InstallationReasonMakeDependency
} default:
return InstallationReasonUnknown return InstallationReasonUnknown
}
func SetInstallationReason(pkg string, reason InstallationReason, rootDir string) error {
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
pkgDir := path.Join(installedDir, pkg)
err := os.WriteFile(path.Join(pkgDir, "installation_reason"), []byte(reason), 0644)
if err != nil {
return err
} }
return nil
} }
func GetPackageInfoRaw(filename string) (string, error) { func GetPackageInfoRaw(filename string) (string, error) {
@@ -177,10 +184,10 @@ func ReadPackage(filename string) (*BPMPackage, error) {
} }
file, err := os.Open(filename) file, err := os.Open(filename)
defer file.Close()
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer file.Close()
tr := tar.NewReader(file) tr := tar.NewReader(file)
for { for {
@@ -219,7 +226,7 @@ func ReadPackage(filename string) (*BPMPackage, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
size, err := strconv.ParseUint(stringEntry[len(stringEntry)-1], 0, 64) size, err := strconv.ParseInt(stringEntry[len(stringEntry)-1], 0, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -303,14 +310,6 @@ func ReadPackageScripts(filename string) (map[string]string, error) {
return ret, nil return ret, nil
} }
type packageOperation uint8
const (
packageOperationInstall packageOperation = 0
packageOperationUpdate = 1
packageOperationRemove = 2
)
func executePackageScript(pkg, rootDir string, verbose bool, packageScript string) error { func executePackageScript(pkg, rootDir string, verbose bool, packageScript string) error {
var bpmpkg *BPMPackage var bpmpkg *BPMPackage
var err error var err error
@@ -363,9 +362,44 @@ func executePackageScript(pkg, rootDir string, verbose bool, packageScript strin
cmd := exec.Command("/bin/bash", "-c", content) cmd := exec.Command("/bin/bash", "-c", content)
// Setup subprocess environment // Setup subprocess environment
cmd.Dir = "/" cmd.Dir = "/"
// Run hook in chroot if using the -R flag // Run package script in chroot if using the -R flag
if rootDir != "/" { if rootDir != "/" {
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir} cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir}
// Bind mount /etc/resolv.conf
mounted, err := func() (bool, error) {
if _, err := os.Stat("/etc/resolv.conf"); err != nil {
return false, nil
}
if _, err := os.Stat(path.Join(rootDir, "etc/resolv.conf")); os.IsNotExist(err) {
err = os.WriteFile(path.Join(rootDir, "/etc/resolv.conf"), nil, 0644)
if err != nil {
return false, nil
}
} else if err != nil {
return false, err
}
mntCmd := exec.Command("mount", "-o", "ro,bind", "/etc/resolv.conf", path.Join(rootDir, "/etc/resolv.conf"))
if verbose {
mntCmd.Stdout = os.Stdout
mntCmd.Stderr = os.Stderr
}
err = mntCmd.Run()
if err != nil {
return false, err
}
return true, nil
}()
if err != nil {
return err
}
if mounted {
defer exec.Command("umount", "-f", path.Join(rootDir, "/etc/resolv.conf")).Run()
}
} }
// Show output if verbose // Show output if verbose
if verbose { if verbose {
@@ -413,21 +447,18 @@ func executePackageScript(pkg, rootDir string, verbose bool, packageScript strin
func ReadPackageInfo(contents string) (*PackageInfo, error) { func ReadPackageInfo(contents string) (*PackageInfo, error) {
pkgInfo := &PackageInfo{ pkgInfo := &PackageInfo{
Name: "",
Description: "",
Version: "",
Revision: 1, Revision: 1,
Url: "", OutputArch: GetArch(),
License: "",
Arch: "",
Type: "",
Keep: make([]string, 0), Keep: make([]string, 0),
Depends: make([]string, 0), Depends: make([]string, 0),
RuntimeDepends: make([]string, 0),
MakeDepends: make([]string, 0), MakeDepends: make([]string, 0),
OptionalDepends: make([]string, 0), OptionalDepends: make([]string, 0),
Conflicts: make([]string, 0), Conflicts: make([]string, 0),
Replaces: make([]string, 0), Replaces: make([]string, 0),
Provides: make([]string, 0), Provides: make([]string, 0),
Options: make([]string, 0),
Downloads: make([]PackageDownload, 0),
SplitPackages: make([]*PackageInfo, 0), SplitPackages: make([]*PackageInfo, 0),
} }
err := yaml.Unmarshal([]byte(contents), &pkgInfo) err := yaml.Unmarshal([]byte(contents), &pkgInfo)
@@ -455,82 +486,168 @@ func ReadPackageInfo(contents string) (*PackageInfo, error) {
} }
} }
// Ensure package name is valid
if match, _ := regexp.MatchString("^[a-zA-Z0-9._-]+$", pkgInfo.Name); !match {
return nil, fmt.Errorf("package name (%s) is invalid", pkgInfo.Name)
}
// Setup split package information // Setup split package information
for i, splitPkg := range pkgInfo.SplitPackages { for i, splitPkg := range pkgInfo.SplitPackages {
// Ensure split package contains a name // Ensure split package contains a name
if splitPkg.Name == "" { if splitPkg.Name == "" {
return nil, fmt.Errorf("invalid split package name: %s", splitPkg.Name) return nil, fmt.Errorf("package name (%s) is invalid", splitPkg.Name)
} }
// Turn split package into json data // Ensure split package name is valid
splitPkgJson, err := yaml.Marshal(splitPkg) if match, _ := regexp.MatchString("^[a-zA-Z0-9._-]+$", splitPkg.Name); !match {
return nil, fmt.Errorf("package name (%s) is invalid", splitPkg.Name)
}
// Turn split package into yaml data
splitPkgYaml, err := yaml.Marshal(splitPkg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Clone all main package fields onto split package // Clone all main package fields onto split package
pkgInfoClone := *pkgInfo *splitPkg = *pkgInfo
pkgInfo.SplitPackages[i] = &pkgInfoClone
// Set split package field of split package to nil // Set split package field of split package to nil
pkgInfo.SplitPackages[i].SplitPackages = nil splitPkg.SplitPackages = nil
// Unmarshal json data back to struct // Unmarshal json data back to struct
err = yaml.Unmarshal(splitPkgJson, &pkgInfo.SplitPackages[i]) err = yaml.Unmarshal(splitPkgYaml, splitPkg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Force set split package version, revision // Force set split package version, revision
pkgInfo.SplitPackages[i].Version = pkgInfo.Version splitPkg.Version = pkgInfo.Version
pkgInfo.SplitPackages[i].Revision = pkgInfo.Revision splitPkg.Revision = pkgInfo.Revision
pkgInfo.SplitPackages[i] = splitPkg
} }
return pkgInfo, nil return pkgInfo, nil
} }
func CreateReadableInfo(showArchitecture, showType, showPackageRelations, showInstallationReason bool, pkgInfo *PackageInfo, rootDir string) string { func (pkgInfo *PackageInfo) CreateReadableInfo(rootDir string) string {
ret := make([]string, 0) builder := strings.Builder{}
appendArray := func(label string, array []string) { builderWriteStringNotEmpty := func(label string, value string) {
if value != "" {
builder.WriteString(label + ": " + value + "\n")
}
}
builderWriteArray := func(label string, array []string, sort bool) {
if len(array) == 0 { if len(array) == 0 {
return return
} }
ret = append(ret, fmt.Sprintf("%s: %s", label, strings.Join(array, ", ")))
// Sort array
if sort {
slices.Sort(array)
} }
ret = append(ret, "Name: "+pkgInfo.Name)
ret = append(ret, "Description: "+pkgInfo.Description) builder.WriteString(label + " (" + strconv.Itoa(len(array)) + "):\n")
ret = append(ret, "Version: "+pkgInfo.GetFullVersion()) for _, val := range array {
ret = append(ret, "URL: "+pkgInfo.Url) builder.WriteString(" - " + val + "\n")
ret = append(ret, "License: "+pkgInfo.License)
if showArchitecture {
ret = append(ret, "Architecture: "+pkgInfo.Arch)
} }
if showType {
ret = append(ret, "Type: "+pkgInfo.Type)
} }
if showPackageRelations { builderWriteDependencyArray := func(label string, depends []string) {
appendArray("Dependencies", pkgInfo.Depends) if len(depends) == 0 {
return
}
// Sort array
slices.Sort(depends)
builder.WriteString(label + " (" + strconv.Itoa(len(depends)) + "):\n")
for _, val := range depends {
builder.WriteString(" - " + val)
// Show virtual package providers
if providers := GetVirtualPackageInfo(val, rootDir); len(providers) > 0 {
builder.WriteString(" (")
for i, vpkg := range providers {
if i == len(providers)-1 {
builder.WriteString(vpkg.Name)
} else {
builder.WriteString(vpkg.Name + ", ")
}
}
builder.WriteString(")")
}
builder.WriteString("\n")
}
}
// Main information
builder.WriteString("Name: " + pkgInfo.Name + "\n")
builder.WriteString("Description: " + pkgInfo.Description + "\n")
builder.WriteString("Version: " + pkgInfo.GetFullVersion() + "\n")
builderWriteStringNotEmpty("URL", pkgInfo.Url)
builderWriteStringNotEmpty("License", pkgInfo.License)
builderWriteArray("Maintainers", pkgInfo.Maintainers, false)
builder.WriteString("Architecture: " + pkgInfo.Arch + "\n")
if pkgInfo.Type == "source" && pkgInfo.OutputArch != "" && pkgInfo.OutputArch != GetArch() {
builder.WriteString("Output architecture: " + pkgInfo.OutputArch + "\n")
}
builder.WriteString("Type: " + pkgInfo.Type + "\n")
// Dependencies
builderWriteDependencyArray("Dependencies", pkgInfo.Depends)
if pkgInfo.Type == "source" { if pkgInfo.Type == "source" {
appendArray("Make Dependencies", pkgInfo.MakeDepends) builderWriteDependencyArray("Make dependencies", pkgInfo.MakeDepends)
builderWriteDependencyArray("Check dependencies", pkgInfo.CheckDepends)
} }
appendArray("Optional dependencies", pkgInfo.OptionalDepends) builderWriteDependencyArray("Runtime dependencies", pkgInfo.RuntimeDepends)
dependants, err := GetPackageDependants(pkgInfo.Name, rootDir) if len(pkgInfo.OptionalDepends) > 0 {
if err == nil { builder.WriteString("Optional dependencies (" + strconv.Itoa(len(pkgInfo.OptionalDepends)) + "):\n")
appendArray("Dependant packages", dependants) for _, depend := range pkgInfo.OptionalDepends {
dependSplit := strings.SplitN(depend, ":", 2)
if len(dependSplit) == 2 {
builder.WriteString(fmt.Sprintf(" - %s (%s)", dependSplit[0], dependSplit[1]))
} else {
builder.WriteString(" - " + dependSplit[0])
} }
appendArray("Conflicting packages", pkgInfo.Conflicts)
appendArray("Provided packages", pkgInfo.Provides) // Show virtual package providers
appendArray("Replaces packages", pkgInfo.Replaces) if providers := GetVirtualPackageInfo(dependSplit[0], rootDir); len(providers) > 0 {
builder.WriteString(" (")
for i, vpkg := range providers {
if i == len(providers)-1 {
builder.WriteString(vpkg.Name)
} else {
builder.WriteString(vpkg.Name + ", ")
} }
}
builder.WriteString(")")
}
builder.WriteString("\n")
}
}
builderWriteArray("Dependant packages", pkgInfo.GetPackageDependants(rootDir, false), true)
builderWriteArray("Optionally dependant packages", pkgInfo.GetPackageOptionalDependants(rootDir), true)
// Other package relations
builderWriteArray("Conflicting packages", pkgInfo.Conflicts, true)
builderWriteArray("Provided packages", pkgInfo.Provides, true)
builderWriteArray("Replaces packages", pkgInfo.Replaces, true)
// Split packages
if pkgInfo.Type == "source" && len(pkgInfo.SplitPackages) != 0 { if pkgInfo.Type == "source" && len(pkgInfo.SplitPackages) != 0 {
splitPkgs := make([]string, len(pkgInfo.SplitPackages)) splitPkgs := make([]string, len(pkgInfo.SplitPackages))
for i, splitPkgInfo := range pkgInfo.SplitPackages { for i, splitPkgInfo := range pkgInfo.SplitPackages {
splitPkgs[i] = splitPkgInfo.Name splitPkgs[i] = splitPkgInfo.Name
} }
appendArray("Split Packages", splitPkgs) builderWriteArray("Split packages", splitPkgs, true)
} }
if IsPackageInstalled(pkgInfo.Name, rootDir) && showInstallationReason {
installationReason := GetInstallationReason(pkgInfo.Name, rootDir) // Installation reason
if rootDir != "" && IsPackageInstalled(pkgInfo.Name, rootDir) {
installationReason := GetPackage(pkgInfo.Name, rootDir).LocalInfo.GetInstallationReason()
var installationReasonString string var installationReasonString string
switch installationReason { switch installationReason {
case InstallationReasonManual: case InstallationReasonManual:
@@ -542,29 +659,23 @@ func CreateReadableInfo(showArchitecture, showType, showPackageRelations, showIn
default: default:
installationReasonString = "Unknown" installationReasonString = "Unknown"
} }
ret = append(ret, "Installation Reason: "+installationReasonString) builder.WriteString("Installation reason: " + installationReasonString + "\n")
} }
return strings.Join(ret, "\n")
return strings.TrimSpace(builder.String())
} }
func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) error { func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string) error {
if !IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) {
err := executePackageScript(filename, rootDir, verbose, "pre_install.sh")
if err != nil {
log.Printf("Warning: %s\n", err)
}
} else {
err := executePackageScript(filename, rootDir, verbose, "pre_update.sh")
if err != nil {
log.Printf("Warning: %s\n", err)
}
}
seenHardlinks := make(map[string]string) seenHardlinks := make(map[string]string)
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return err return err
} }
// Initialize progress bar
bar := createProgressBar(bpmpkg.GetInstalledSize(), "Installing "+bpmpkg.PkgInfo.Name, verbose)
defer bar.Close()
tarballFile, err := readTarballFile(filename, "files.tar.gz") tarballFile, err := readTarballFile(filename, "files.tar.gz")
if err != nil { if err != nil {
return err return err
@@ -576,6 +687,7 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string)
return err return err
} }
packageFilesReader := tar.NewReader(archive) packageFilesReader := tar.NewReader(archive)
for { for {
header, err := packageFilesReader.Next() header, err := packageFilesReader.Next()
if err == io.EOF { if err == io.EOF {
@@ -587,6 +699,17 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string)
extractFilename := path.Join(rootDir, header.Name) extractFilename := path.Join(rootDir, header.Name)
switch header.Typeflag { switch header.Typeflag {
case tar.TypeDir: case tar.TypeDir:
// Check if path is set to be ignored
if ok := slices.ContainsFunc(MainBPMConfig.IgnorePaths, func(s string) bool {
matched, _ := filepath.Match(s, header.Name)
return matched
}); rootDir == "/" && ok {
if verbose {
fmt.Printf("Skipping Directory: %s (Path was ignored)\n", extractFilename)
}
continue
}
if _, err := os.Stat(extractFilename); err == nil { if _, err := os.Stat(extractFilename); err == nil {
if verbose { if verbose {
fmt.Printf("Skipping Directory: %s (Directory already exists)\n", extractFilename) fmt.Printf("Skipping Directory: %s (Directory already exists)\n", extractFilename)
@@ -612,7 +735,19 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string)
if verbose { if verbose {
fmt.Printf("Created directory %s (%o)\n", extractFilename, header.Mode) fmt.Printf("Created directory %s (%o)\n", extractFilename, header.Mode)
} }
bar.Add64(header.Size)
case tar.TypeReg: case tar.TypeReg:
// Check if path is set to be ignored
if ok := slices.ContainsFunc(MainBPMConfig.IgnorePaths, func(s string) bool {
matched, _ := filepath.Match(s, header.Name)
return matched
}); rootDir == "/" && ok {
if verbose {
fmt.Printf("Skipping File: %s (Path was ignored)\n", extractFilename)
}
continue
}
skip := false skip := false
if _, err := os.Stat(extractFilename); err == nil { if _, err := os.Stat(extractFilename); err == nil {
for _, k := range bpmpkg.PkgInfo.Keep { for _, k := range bpmpkg.PkgInfo.Keep {
@@ -668,7 +803,19 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string)
if verbose { if verbose {
fmt.Printf("Created File: %s (%o)\n", extractFilename, header.Mode) fmt.Printf("Created File: %s (%o)\n", extractFilename, header.Mode)
} }
bar.Add64(header.Size)
case tar.TypeSymlink: case tar.TypeSymlink:
// Check if path is set to be ignored
if ok := slices.ContainsFunc(MainBPMConfig.IgnorePaths, func(s string) bool {
matched, _ := filepath.Match(s, header.Name)
return matched
}); rootDir == "/" && ok {
if verbose {
fmt.Printf("Skipping Symlink: %s (Path was ignored)\n", extractFilename)
}
continue
}
err := os.Remove(extractFilename) err := os.Remove(extractFilename)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return err return err
@@ -682,7 +829,19 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string)
if verbose { if verbose {
fmt.Println("Created Symlink: " + extractFilename + " -> " + header.Linkname) fmt.Println("Created Symlink: " + extractFilename + " -> " + header.Linkname)
} }
bar.Add64(header.Size)
case tar.TypeLink: case tar.TypeLink:
// Check if path is set to be ignored
if ok := slices.ContainsFunc(MainBPMConfig.IgnorePaths, func(s string) bool {
matched, _ := filepath.Match(s, header.Name)
return matched
}); rootDir == "/" && ok {
if verbose {
fmt.Printf("Skipping Hard Link: %s (Path was ignored)\n", extractFilename)
}
continue
}
if verbose { if verbose {
fmt.Println("Detected Hard Link: " + extractFilename + " -> " + path.Join(rootDir, strings.TrimPrefix(header.Linkname, "files/"))) fmt.Println("Detected Hard Link: " + extractFilename + " -> " + path.Join(rootDir, strings.TrimPrefix(header.Linkname, "files/")))
} }
@@ -691,6 +850,7 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string)
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return err return err
} }
bar.Add64(header.Size)
default: default:
return errors.New("unknown type (" + strconv.Itoa(int(header.Typeflag)) + ") in " + extractFilename) return errors.New("unknown type (" + strconv.Itoa(int(header.Typeflag)) + ") in " + extractFilename)
} }
@@ -710,7 +870,7 @@ func extractPackage(bpmpkg *BPMPackage, verbose bool, filename, rootDir string)
return nil return nil
} }
func installPackage(filename, rootDir string, verbose, force bool) error { func installPackage(filename string, installationReason InstallationReason, rootDir string, verbose, force bool) error {
if _, err := os.Stat(filename); os.IsNotExist(err) { if _, err := os.Stat(filename); os.IsNotExist(err) {
return err return err
} }
@@ -725,6 +885,20 @@ func installPackage(filename, rootDir string, verbose, force bool) error {
} }
packageInstalled := IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) packageInstalled := IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir)
// Run pre-* package scripts
if !packageInstalled {
err := executePackageScript(filename, rootDir, verbose, "pre_install.sh")
if err != nil {
log.Printf("Warning: %s\n", err)
}
} else {
err := executePackageScript(filename, rootDir, verbose, "pre_update.sh")
if err != nil {
log.Printf("Warning: %s\n", err)
}
}
// Check if package is installed and remove current files // Check if package is installed and remove current files
if packageInstalled { if packageInstalled {
// Fetching and reversing package file entry list // Fetching and reversing package file entry list
@@ -743,26 +917,39 @@ func installPackage(filename, rootDir string, verbose, force bool) error {
fmt.Printf("Removing old files for package (%s)...\n", bpmpkg.PkgInfo.Name) fmt.Printf("Removing old files for package (%s)...\n", bpmpkg.PkgInfo.Name)
} }
for _, entry := range fileEntries { for _, entry := range fileEntries {
file := path.Join(rootDir, entry.Path) finalPath := path.Join(rootDir, entry.Path)
stat, err := os.Lstat(file)
stat, err := os.Lstat(finalPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
continue continue
} } else if err != nil {
if err != nil {
return err return err
} }
if len(files[entry.Path]) != 0 {
// Check if path is set to be ignored
if ok := slices.ContainsFunc(MainBPMConfig.IgnorePaths, func(s string) bool {
matched, _ := filepath.Match(s, entry.Path)
return matched
}); rootDir == "/" && ok {
if verbose { if verbose {
fmt.Println("Skipping path: " + file + " (Path is managed by multiple packages)") fmt.Printf("Skipping path: %s (Path was ignored)\n", finalPath)
} }
continue continue
} }
if len(files[entry.Path]) != 0 {
if verbose {
fmt.Println("Skipping path: " + finalPath + " (Path is managed by multiple packages)")
}
continue
}
shouldContinue := false shouldContinue := false
for _, value := range bpmpkg.PkgInfo.Keep { for _, value := range bpmpkg.PkgInfo.Keep {
if strings.HasSuffix(value, "/") { if strings.HasSuffix(value, "/") {
if strings.HasPrefix(entry.Path, value) || entry.Path == strings.TrimSuffix(value, "/") { if strings.HasPrefix(entry.Path, value) || entry.Path == strings.TrimSuffix(value, "/") {
if verbose { if verbose {
fmt.Println("Skipping path: " + file + " (Path is set to be kept during reinstalls/updates)") fmt.Println("Skipping path: " + finalPath + " (Path is set to be kept during reinstalls/updates)")
} }
shouldContinue = true shouldContinue = true
continue continue
@@ -770,7 +957,7 @@ func installPackage(filename, rootDir string, verbose, force bool) error {
} else { } else {
if entry.Path == value { if entry.Path == value {
if verbose { if verbose {
fmt.Println("Skipping path: " + file + " (Path is set to be kept during reinstalls/updates)") fmt.Println("Skipping path: " + finalPath + " (Path is set to be kept during reinstalls/updates)")
} }
shouldContinue = true shouldContinue = true
continue continue
@@ -782,37 +969,37 @@ func installPackage(filename, rootDir string, verbose, force bool) error {
} }
if stat.Mode()&os.ModeSymlink != 0 { if stat.Mode()&os.ModeSymlink != 0 {
if verbose { if verbose {
fmt.Println("Removing: " + file) fmt.Println("Removing: " + finalPath)
} }
err := os.Remove(file) err := os.Remove(finalPath)
if err != nil { if err != nil {
return err return err
} }
continue continue
} }
if stat.IsDir() { if stat.IsDir() {
dir, err := os.ReadDir(file) dir, err := os.ReadDir(finalPath)
if err != nil { if err != nil {
return err return err
} }
if len(dir) != 0 { if len(dir) != 0 {
if verbose { if verbose {
fmt.Println("Skipping non-empty directory: " + file) fmt.Println("Skipping non-empty directory: " + finalPath)
} }
continue continue
} }
if verbose { if verbose {
fmt.Println("Removing: " + file) fmt.Println("Removing: " + finalPath)
} }
err = os.Remove(file) err = os.Remove(finalPath)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
if verbose { if verbose {
fmt.Println("Removing: " + file) fmt.Println("Removing: " + finalPath)
} }
err := os.Remove(file) err := os.Remove(finalPath)
if err != nil { if err != nil {
return err return err
} }
@@ -880,6 +1067,16 @@ func installPackage(filename, rootDir string, verbose, force bool) error {
return err return err
} }
// Write local package information
localInfo := getPackageLocalInfo(bpmpkg.PkgInfo.Name, rootDir)
if !packageInstalled {
localInfo.InstalledOn = time.Now().Unix()
}
localInfo.LastUpdatedOn = time.Now().Unix()
localInfo.InstallationReason = string(installationReason)
SetPackageLocalInfo(bpmpkg.PkgInfo.Name, localInfo, rootDir)
// Save remove package scripts // Save remove package scripts
packageScripts, err := ReadPackageScripts(filename) packageScripts, err := ReadPackageScripts(filename)
if err != nil { if err != nil {
@@ -906,6 +1103,7 @@ func installPackage(filename, rootDir string, verbose, force bool) error {
f.Close() f.Close()
} }
// Run post-* package scripts
if !packageInstalled { if !packageInstalled {
err = executePackageScript(filename, rootDir, verbose, "post_install.sh") err = executePackageScript(filename, rootDir, verbose, "post_install.sh")
if err != nil { if err != nil {
@@ -918,14 +1116,22 @@ func installPackage(filename, rootDir string, verbose, force bool) error {
} }
} }
// Write persistent data version number
if _, err := os.Stat(path.Join(rootDir, "var/lib/bpm/.version")); err != nil {
err = os.WriteFile(path.Join(rootDir, "var/lib/bpm/.version"), []byte(strconv.Itoa(persistentDataVersion)), 0644)
if err != nil {
return err
}
}
// Ensure local package information has been initialized for rootDir // Ensure local package information has been initialized for rootDir
err = initializeLocalPackageInformation(rootDir) err = InitializeLocalPackageInformation(rootDir)
if err != nil { if err != nil {
return err return err
} }
// Add or update package information for rootDir // Add or update package information for rootDir
localPackageInformation[rootDir][bpmpkg.PkgInfo.Name] = bpmpkg localPackageInformation[rootDir][bpmpkg.PkgInfo.Name] = bpmpkg.PkgInfo
return nil return nil
} }
@@ -943,8 +1149,11 @@ func removePackage(pkg string, verbose bool, rootDir string) error {
log.Printf("Warning: %s\n", err) log.Printf("Warning: %s\n", err)
} }
// Get BPM package
bpmpkg := GetPackage(pkg, rootDir)
// Fetching and reversing package file entry list // Fetching and reversing package file entry list
fileEntries := GetPackage(pkg, rootDir).PkgFiles fileEntries := bpmpkg.PkgFiles
sort.Slice(fileEntries, func(i, j int) bool { sort.Slice(fileEntries, func(i, j int) bool {
return fileEntries[i].Path < fileEntries[j].Path return fileEntries[i].Path < fileEntries[j].Path
}) })
@@ -954,33 +1163,50 @@ func removePackage(pkg string, verbose bool, rootDir string) error {
return err return err
} }
bar := createProgressBar(bpmpkg.GetInstalledSize(), "Removing "+bpmpkg.PkgInfo.Name, verbose)
defer bar.Close()
// Removing package files // Removing package files
for _, entry := range fileEntries { for _, entry := range fileEntries {
file := path.Join(rootDir, entry.Path) bar.Add64(entry.SizeInBytes)
lstat, err := os.Lstat(file)
finalPath := path.Join(rootDir, entry.Path)
lstat, err := os.Lstat(finalPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
continue continue
} } else if err != nil {
if err != nil {
return err return err
} }
// Check if path is set to be ignored
if ok := slices.ContainsFunc(MainBPMConfig.IgnorePaths, func(s string) bool {
matched, _ := filepath.Match(s, entry.Path)
return matched
}); rootDir == "/" && ok {
if verbose {
fmt.Printf("Skipping path: %s (Path was ignored)\n", finalPath)
}
continue
}
if len(files[entry.Path]) != 0 { if len(files[entry.Path]) != 0 {
if verbose { if verbose {
fmt.Println("Skipping path: " + file + "(Path is managed by multiple packages)") fmt.Println("Skipping path: " + finalPath + "(Path is managed by multiple packages)")
} }
continue continue
} }
if lstat.Mode()&os.ModeSymlink != 0 { if lstat.Mode()&os.ModeSymlink != 0 {
if verbose { if verbose {
fmt.Println("Removing: " + file) fmt.Println("Removing: " + finalPath)
} }
err := os.Remove(file) err := os.Remove(finalPath)
if err != nil { if err != nil {
return err return err
} }
continue continue
} }
stat, err := os.Stat(file) stat, err := os.Stat(finalPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
continue continue
} }
@@ -988,28 +1214,28 @@ func removePackage(pkg string, verbose bool, rootDir string) error {
return err return err
} }
if stat.IsDir() { if stat.IsDir() {
dir, err := os.ReadDir(file) dir, err := os.ReadDir(finalPath)
if err != nil { if err != nil {
return err return err
} }
if len(dir) != 0 { if len(dir) != 0 {
if verbose { if verbose {
fmt.Println("Skipping non-empty directory: " + file) fmt.Println("Skipping non-empty directory: " + finalPath)
} }
continue continue
} }
if verbose { if verbose {
fmt.Println("Removing: " + file) fmt.Println("Removing: " + finalPath)
} }
err = os.Remove(file) err = os.Remove(finalPath)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
if verbose { if verbose {
fmt.Println("Removing: " + file) fmt.Println("Removing: " + finalPath)
} }
err := os.Remove(file) err := os.Remove(finalPath)
if err != nil { if err != nil {
return err return err
} }
@@ -1032,7 +1258,7 @@ func removePackage(pkg string, verbose bool, rootDir string) error {
} }
// Ensure local package information has been initialized for rootDir // Ensure local package information has been initialized for rootDir
err = initializeLocalPackageInformation(rootDir) err = InitializeLocalPackageInformation(rootDir)
if err != nil { if err != nil {
return err return err
} }
+98 -8
View File
@@ -2,10 +2,58 @@ package bpmlib
import ( import (
"fmt" "fmt"
"io"
"math" "math"
"os"
"path"
"syscall" "syscall"
"time"
version "github.com/knqyf263/go-rpm-version"
"github.com/schollz/progressbar/v3"
) )
type BPMLock struct {
file *os.File
path string
}
func LockBPM(rootDir string) (*BPMLock, error) {
// Create cache directory if it doesn't already exist
err := os.MkdirAll(path.Join(rootDir, "/var/cache/bpm"), 0755)
if err != nil {
return nil, err
}
// Create file
f, err := os.Create(path.Join(rootDir, "var/cache/bpm/bpm.lock"))
if err != nil {
return nil, err
}
// Get exclusive file lock on file
err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err != nil {
return nil, err
}
return &BPMLock{f, path.Join(rootDir, "var/cache/bpm/bpm.lock")}, nil
}
func (lock *BPMLock) Unlock() error {
err := lock.file.Close()
if err != nil {
return err
}
err = os.Remove(lock.path)
if err != nil {
return err
}
return nil
}
func GetArch() string { func GetArch() string {
uname := syscall.Utsname{} uname := syscall.Utsname{}
err := syscall.Uname(&uname) err := syscall.Uname(&uname)
@@ -21,6 +69,47 @@ func GetArch() string {
return string(byteString[:indexLength]) return string(byteString[:indexLength])
} }
func CompareVersions(version1, version2 string) int {
v1 := version.NewVersion(version1)
v2 := version.NewVersion(version2)
return v1.Compare(v2)
}
func createProgressBar(max int64, description string, hideBar bool) *progressbar.ProgressBar {
var output io.Writer
if hideBar {
output = io.Discard
} else {
output = os.Stderr
}
if len(description) < 40 {
for i := len(description); i < 40; i++ {
description += " "
}
}
if len(description) > 40 {
overflow := len(description) - 37
description = description[:len(description)-overflow] + "..."
}
return progressbar.NewOptions64(max,
progressbar.OptionSetDescription(description),
progressbar.OptionSetWriter(output),
progressbar.OptionShowBytes(true),
progressbar.OptionShowTotalBytes(true),
progressbar.OptionSetWidth(20),
progressbar.OptionThrottle(65*time.Millisecond),
progressbar.OptionShowCount(),
progressbar.OptionOnCompletion(func() {
fmt.Fprint(output, "\n")
}),
progressbar.OptionSpinnerType(14),
progressbar.OptionSetRenderBlankState(true),
progressbar.OptionSetTheme(progressbar.ThemeASCII))
}
func stringSliceRemove(s []string, r string) []string { func stringSliceRemove(s []string, r string) []string {
for i, v := range s { for i, v := range s {
if v == r { if v == r {
@@ -30,7 +119,7 @@ func stringSliceRemove(s []string, r string) []string {
return s return s
} }
func unsignedBytesToHumanReadable(b uint64) string { func BytesToHumanReadable(b int64) string {
bf := float64(b) bf := float64(b)
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} {
if math.Abs(bf) < 1024.0 { if math.Abs(bf) < 1024.0 {
@@ -41,13 +130,14 @@ func unsignedBytesToHumanReadable(b uint64) string {
return fmt.Sprintf("%.1fYiB", bf) return fmt.Sprintf("%.1fYiB", bf)
} }
func bytesToHumanReadable(b int64) string { func removeDuplicates[T comparable](sliceList []T) []T {
bf := float64(b) allKeys := make(map[T]bool)
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { list := []T{}
if math.Abs(bf) < 1024.0 { for _, item := range sliceList {
return fmt.Sprintf("%3.1f%sB", bf, unit) if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
} }
bf /= 1024.0
} }
return fmt.Sprintf("%.1fYiB", bf) return list
} }