237 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
EnumDev 85d5befbda Merge pull request 'Merge BPM 0.5.0 branch into master' (#12) from develop into master
Reviewed-on: #12
2025-08-23 10:47:06 +00:00
EnumDev 27713e468c Update README.md 2025-08-23 13:30:16 +03:00
EnumDev da184ecb19 Update Makefile 2025-08-23 13:21:58 +03:00
EnumDev 812caa1485 Split compilation config options into separate config file 2025-08-14 22:35:11 +03:00
EnumDev 5181c43ecf Rewrite executePackageScripts function 2025-08-14 18:29:13 +03:00
EnumDev 17af8dbaaf Turn package script fatal errors into warnings 2025-08-14 17:09:18 +03:00
EnumDev 6501105dd1 Fix remove package scripts not working with chroot 2025-08-14 17:07:42 +03:00
EnumDev c01ca62e34 Use chroot for package scripts 2025-08-14 14:48:41 +03:00
EnumDev 998d4a2252 Merge pull request 'Readd source package compilation functionality' (#11) from readd_source_packages into develop
Reviewed-on: bubble-package-manager/bpm#11
2025-08-13 10:34:14 +00:00
EnumDev 57231fdec5 Fix package installation reason not being displayed or set correctly 2025-08-11 12:23:45 +03:00
EnumDev 713eafb708 Remove dependency on 'which' 2025-08-06 21:07:07 +03:00
EnumDev 38b6ea0175 Update README.md 2025-08-03 09:26:43 +03:00
EnumDev ef5697321b Default to not installing optional dependencies 2025-08-03 09:20:37 +03:00
EnumDev e63398c853 Set correct ownership during package extraction 2025-08-01 19:19:08 +03:00
EnumDev 6492fd028f Fix directories not being created with correct permissions and improve debug messages 2025-07-17 17:09:36 +03:00
EnumDev 4bfc325c66 Show error when compiling source packages in a different root directory 2025-07-06 22:01:57 +03:00
EnumDev 8a627254c9 Fix dependency installation order 2025-07-06 21:21:55 +03:00
EnumDev 458c091ac2 Add "make_dependency" installation reason 2025-05-23 14:16:52 +03:00
EnumDev 966d351a80 Readd make depends field to binary packages 2025-05-15 16:07:15 +03:00
EnumDev 815d0bb29a Rename BPM Repositories to BPM Databases 2025-05-14 16:33:30 +03:00
EnumDev bf2b4e95ac Fix installation reason showing in 'info' subcommand when viewing not-installed package, database entry or BPM file 2025-05-14 15:01:28 +03:00
EnumDev d26d878308 Fix error when installing, updating or remove packages 2025-05-13 20:44:16 +03:00
EnumDev f20d7b66d1 Remove unused verbose parameters 2025-05-13 20:33:20 +03:00
EnumDev 5f03a6a1ad Redo dependency resolution and cache installed package information 2025-05-13 20:26:55 +03:00
EnumDev f2bc7c8968 Fix not reading local databases after reloading config during package update 2025-05-09 14:36:08 +03:00
EnumDev 97d5a06ce7 Remove force setting of URL field in split packages 2025-05-08 15:12:06 +03:00
EnumDev f0709ff410 Fix 'prepare' function running in source directory during compilation 2025-05-08 12:44:55 +03:00
EnumDev a32dfba54a Allow split package to have same name as main package 2025-05-04 21:03:05 +03:00
EnumDev b07df8c7df 'compile' subcommand will now detect if virtual package is installed 2025-05-03 17:30:27 +03:00
EnumDev 1329109810 Add cache cleanup to 'cleanup' subcommand 2025-05-03 12:38:29 +03:00
EnumDev 4a8c4e22ec Move directories 2025-05-03 10:47:32 +03:00
EnumDev eb8db1bc99 Fix output filename for source package compilation 2025-05-02 21:29:07 +03:00
EnumDev 4f1eeeb11d Fix dependency installation command arguments for 'bpm compile -d' 2025-05-02 21:20:56 +03:00
EnumDev 2e416b9e6f Allow direct split source package installation through repository entries 2025-05-02 20:43:24 +03:00
EnumDev d8a42c780d Allow direct split source package installation through local BPM archive 2025-05-01 16:40:42 +03:00
EnumDev b1bb8de661 Fix packages not compiling without root permissions 2025-04-27 12:04:30 +03:00
EnumDev c425b263fe Fix cleanup flags not being read 2025-04-26 16:28:24 +03:00
EnumDev 5776784021 Remove no longer used flags from help message 2025-04-26 16:23:09 +03:00
EnumDev 903c7dce3e Change -o flag from output filename to output directory 2025-04-26 16:20:40 +03:00
EnumDev 5085981f52 Allow split source package compilation 2025-04-26 15:29:45 +03:00
EnumDev 9cdb3d29aa Reallow direct source package installation using 'bpm install' 2025-04-25 14:13:12 +03:00
EnumDev d8146cb3f3 Remove unused function 2025-04-24 18:03:57 +03:00
EnumDev c8c1fded76 Set output package architecture after compilation 2025-04-24 18:03:32 +03:00
EnumDev 4bf9ac9c60 Add .compilation-options file support 2025-04-24 17:39:17 +03:00
EnumDev b73519ba9c Fix shell commands used in source package compilation 2025-04-24 16:44:37 +03:00
EnumDev 382a3fc8b6 Add flags for 'compile' subcommand 2025-04-24 13:56:30 +03:00
EnumDev 1843bceef5 Fix pre_remove.sh package script not being read or run 2025-04-22 19:28:36 +03:00
EnumDev 6af3b77d69 Include package scripts in compilation 2025-04-22 19:10:53 +03:00
EnumDev 8b6ef5fa63 Added compilation environment array to BPM config 2025-04-22 17:41:37 +03:00
EnumDev e8d5f0a565 Add basic compilation functionality 2025-04-22 17:04:47 +03:00
EnumDev 7b0a8bf1d6 Merge pull request 'Reorganize codebase' (#10) from code_reorganization into develop
Reviewed-on: bubble-package-manager/bpm#10
2025-04-17 11:49:51 +00:00
EnumDev 3a62f23db5 Update README.md 2025-04-17 14:04:11 +03:00
EnumDev d9d85b4943 Simplify Makefile and make it posix compliant 2025-04-17 13:58:49 +03:00
EnumDev 877fba8767 Use install command in Makefile, remove compress and run target and add uninstall target 2025-04-17 13:49:41 +03:00
EnumDev 0858a49636 Add .gitignore 2025-04-16 20:15:46 +03:00
EnumDev b27137da29 Allow sync and update subcommands when local database files are corrupt 2025-04-16 20:13:22 +03:00
EnumDev 78dce34b64 Hide make dependencies when reading info of binary packages 2025-04-16 20:08:24 +03:00
EnumDev f74372f13b Add 'ignore_packages' field to default config 2025-04-16 19:02:55 +03:00
EnumDev dd41369e05 Fix error when hooks directory does not exist 2025-04-16 18:49:54 +03:00
EnumDev fa3f59d8ba Update bpm.conf 2025-04-10 15:31:36 +03:00
EnumDev 46f7d96acf Update README.md 2025-04-10 15:29:44 +03:00
EnumDev 2f8d6a7ea9 Add missing dashes in the printHelp function 2025-04-10 15:08:27 +03:00
EnumDev c767f26937 Remove direct source compilation (To be reworked) 2025-04-10 15:07:28 +03:00
EnumDev 3f2d5cf7a1 Remove all log.Fatal and os.Exit calls from bpmlib 2025-04-10 14:54:41 +03:00
EnumDev 14fe71842e Added custom error types 2025-04-10 14:38:32 +03:00
EnumDev d1a2d28cff Fix some error handling and comments 2025-04-10 13:06:18 +03:00
EnumDev 151de2112e Fix virtual packages not being accounted for in GetDependants function 2025-04-08 19:20:49 +03:00
EnumDev e60381beb1 Rename constants 2025-04-08 17:41:14 +03:00
EnumDev 0801612166 Create functions for basic package management in bpmlib 2025-04-07 21:03:33 +03:00
EnumDev 9485248d8e Unexport functions 2025-04-07 17:28:26 +03:00
EnumDev 68291e2666 Rename go files 2025-04-07 17:14:04 +03:00
EnumDev 05f273d687 Update bpm frontend module name 2025-04-07 17:10:19 +03:00
EnumDev fcb2ef1515 Split project into the BPM frontend (src/bpm) and the BPM backend module (src/bpmlib) 2025-04-07 17:06:40 +03:00
EnumDev 97de746a7d Move go code to src/ subdirectory 2025-04-07 15:35:18 +03:00
EnumDev a26737859c Merge pull request 'Add hook functionality' (#9) from hooks into develop
Reviewed-on: bubble-package-manager/bpm#9
2025-04-07 12:20:24 +00:00
EnumDev 90d5a23fcc Remove redundant underscore 2025-04-07 14:59:28 +03:00
EnumDev 7104441727 Allow globstar in bpmhook path targets 2025-04-07 14:58:56 +03:00
EnumDev 5f8e4f00ea Added basic hook functionality 2025-04-05 15:03:00 +03:00
EnumDev 87c492a30c Improve virtual package handling 2025-04-04 21:40:21 +03:00
EnumDev e94b2a8816 Add replaces field to package information 2025-03-15 11:20:33 +02:00
EnumDev 4f9d2cdecd Add ability to ignore packages 2025-03-13 15:34:43 +02:00
EnumDev e2e2629dc9 Make bash the default shell in Makefile 2025-03-13 15:34:10 +02:00
EnumDev c3981ad95c Added repository entry information flag for the info subcommand and slightly altered the search subcommand format 2024-11-17 18:02:54 +02:00
EnumDev 852e615806 Changed info subcommand format and added support for checking .bpm file information 2024-11-17 17:32:49 +02:00
EnumDev e285fd792e Fixed bug where dependencies would be installed with 'manual' as installation reason 2024-11-06 09:06:46 +02:00
EnumDev 2accc30390 Added --installation-reason flag 2024-11-05 15:34:55 +02:00
EnumDev b625fe97ef Added --cleanup flag to the remove subcommand 2024-11-04 19:38:54 +02:00
EnumDev 6e2bb86ef0 Show dependant packages in the 'info' subcommand 2024-11-04 14:05:38 +02:00
EnumDev e30a32c568 Added an --unused flag for the remove subcommand and remove will no longer display errors if a package does not exist 2024-10-30 16:35:52 +02:00
EnumDev a2da73e26b Added a 'cleanup' subcommand that removes unused dependencies 2024-10-24 12:44:43 +03:00
EnumDev 2c7c4eeb73 Fixed small bug regarding installation reasons not always being read correctly 2024-10-23 19:13:18 +03:00
EnumDev 6e11f937a6 Added conflict checking 2024-10-23 16:10:42 +03:00
EnumDev db6a776763 Merge branch 'improve_bpm_structure' into 'master'
Switch to new BPM file structure

See merge request bubble-package-manager/bpm!8
2024-10-23 06:22:39 +00:00
EnumDev e888fe06e8 Merge branch 'develop' into 'master'
Bug fixes and added pre_update.sh pacakge scripts

See merge request bubble-package-manager/bpm!7
2024-10-23 06:18:00 +00:00
EnumDev fe928dbbd6 Fixed ActionsContainPackage BPMOperation receiver function incorrectly returning false 2024-10-22 17:43:05 +03:00
EnumDev 0b439bcfed Changed BPM Version to 0.5.0 2024-10-22 15:37:16 +03:00
EnumDev 4aed0fe5b0 Made GetArch() function run independently of the 'uname' command 2024-10-21 21:55:39 +03:00
EnumDev a054717b23 Improved package removal and fixed bugs related to it 2024-10-21 21:51:03 +03:00
EnumDev 918ff984ca Moved package fetching, installation and removal functionality to operations.go 2024-10-17 11:54:32 +03:00
EnumDev a7b025d92d Removed additional empty line in output 2024-10-15 14:34:32 +03:00
EnumDev ef16fa8195 Improve tarball single file reading 2024-10-15 14:34:01 +03:00
EnumDev 1aec288013 Added octal permissions to BPMFileEntry struct 2024-10-15 12:20:26 +03:00
EnumDev 7816d0072c Added BPM operation structs which make handling package installation/removal easier and fixed multiple bugs 2024-10-15 10:03:06 +03:00
EnumDev 1bd57110a0 Removed some unused functions and renamed some variables 2024-10-10 12:44:06 +03:00
EnumDev 76efa42bcf Started implementation on the new bpm file structure 2024-10-10 12:36:29 +03:00
EnumDev c8939614b1 Fixed circular dependency problems 2024-10-08 20:21:49 +03:00
EnumDev 4793424f3d Fixed error when installing new package 2024-10-08 11:10:49 +03:00
EnumDev e1726ddce5 Add pre_remove.sh package scripts 2024-10-07 19:30:42 +03:00
EnumDev e6cfd112ea Improved version comparison 2024-10-07 19:12:17 +03:00
EnumDev 2d35ac12a1 Provided packages should now function correctly 2024-10-07 14:47:57 +03:00
EnumDev a3d1167358 Replaced references to old username (CapCreeperGR) with new username (EnumDev) to avoid confusion 2024-10-07 09:36:38 +03:00
EnumDev 3ff01093a0 Added BPM_PKG_REVISION, BPM_PKG_OLD_VERSION, BPM_PKG_OLD_REVISION variables to package scripts 2024-10-07 09:30:57 +03:00
32 changed files with 7455 additions and 2610 deletions
+5
View File
@@ -0,0 +1,5 @@
# IDE Directories
.idea
# Build directory
build
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 CapCreeperGR
Copyright (c) 2024 EnumDev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+30 -27
View File
@@ -1,38 +1,41 @@
ifeq ($(PREFIX),)
PREFIX := /usr/local
endif
ifeq ($(BINDIR),)
BINDIR := $(PREFIX)/bin
endif
ifeq ($(SYSCONFDIR),)
SYSCONFDIR := $(PREFIX)/etc
endif
ifeq ($(GO),)
GO := $(shell type -a -P go | head -n 1)
endif
# Installation paths
PREFIX ?= /usr/local
SBINDIR ?= $(PREFIX)/sbin
SYSCONFDIR := $(PREFIX)/etc
# Compilers and tools
GO ?= go
# Build-time variables
VERSION ?= $(shell git describe --tags --dirty)
ROOT_COMPILATION_UID ?= 65534
ROOT_COMPILATION_GID ?= 65534
build:
mkdir -p build
$(GO) build -ldflags "-w" -o build/bpm gitlab.com/bubble-package-manager/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/
mkdir -p $(DESTDIR)$(BINDIR)
mkdir -p $(DESTDIR)$(SYSCONFDIR)
cp build/bpm $(DESTDIR)$(BINDIR)/bpm
cp config/bpm.conf $(DESTDIR)$(SYSCONFDIR)/bpm.conf
# Create directory
install -dm755 $(DESTDIR)$(SBINDIR)
compress: build/bpm config/
mkdir -p bpm/$(BINDIR)
mkdir -p bpm/$(SYSCONFDIR)
cp build/bpm bpm/$(BINDIR)/bpm
cp config/bpm.conf bpm/$(SYSCONFDIR)/bpm.conf
tar --owner=root --group=root -czf bpm.tar.gz bpm
rm -r bpm
# Install binary
install -Dm755 build/bpm $(DESTDIR)$(SBINDIR)/bpm
run: build/bpm
build/bpm
install-config:
# Create directory
install -dm755 $(DESTDIR)$(SYSCONFDIR)
# Install files
install -Dm644 config/bpm.conf $(DESTDIR)$(SYSCONFDIR)/bpm.conf
install -Dm644 config/bpm-compilation.conf $(DESTDIR)$(SYSCONFDIR)/bpm-compilation.conf
uninstall:
-rm -f $(DESTDIR)$(SBINDIR)/bpm
-rm -f $(DESTDIR)$(SYSCONFDIR)/bpm.conf
-rm -f $(DESTDIR)$(SYSCONFDIR)/bpm-compilation.conf
clean:
rm -r build/
.PHONY: build
.PHONY: build install install-config uninstall clean
+28 -18
View File
@@ -1,29 +1,25 @@
# Bubble Package Manager (BPM)
## _A simple package manager_
BPM is a simple package manager for Linux systems
## _The official package manager for Tide Linux_
## Features
- Simple to use subcommands
- Can install binary and source packages
- Can be easily installed on practically any system
- No bloat
- Can install both binary and source packages
## Information
BPM is still in very early development. It should not be installed on any system you use seriously. I recommend trying this out in a VM or container. In addition to this, this is one of the first projects I have made using the go programming language so code quality may not be the best. This project was made to help me learn go and how linux systems work better. It is not meant to replace the big package managers in any way
BPM is still in very early development. There may be bugs that could completely break installations. For now it is recommended you use this only in Virtual Machines, containers or in a Tide Linux installation
## Build from source
- Download `go` from your package manager or from the go website
- Download `make` from your package manager
- Run the following command to compile the project
```
- Run the following command to compile the project. You may need to set the `GO` environment variable if your Go installation is not in your PATH
```sh
make
```
- Run the following command to install stormfetch into your system. You may also append a DESTDIR variable at the end of this line if you wish to install in a different location
```
- Run the following command to install BPM into your system. You may also append a DESTDIR variable at the end of this line if you wish to install in a different location
```sh
make install PREFIX=/usr SYSCONFDIR=/etc
make install-config PREFIX=/usr SYSCONFDIR=/etc
```
## How to use
@@ -32,24 +28,38 @@ You are able to install bpm packages by typing the following:
```sh
bpm install /path/to/package.bpm
```
You may also use the -y flag as shown below to bypass the installation confirmation prompt
You can also use the package name directly if using databases
```sh
bpm install package_name
```
The -y flag may be used as shown below to bypass the confirmation prompt
```sh
bpm install -y /path/to/package.bpm
```
Flags must strictly be typed before the first package path otherwise they'll be read as package locations themselves
Flags must strictly be typed before the first package path or name, otherwise they'll be read as package locations themselves
You can remove an installed package by typing the following
```sh
bpm remove package_name
```
The -y flag applies here as well if you wish to skip the removal confirmation prompt
To remove all unused dependencies and clean cached files try using the cleanup command
```sh
bpm cleanup
```
If using databases, all packages can be updated using this simple command
```sh
bpm update
```
For information on the rest of the commands simply use the help command or pass in no arguments at all
```
```sh
bpm help
```
## Package Creation
Package creation is simplified using the bpm-utils package which contains helper scripts for creating and archiving packages. \
Learn more here: https://gitlab.com/bubble-package-manager/bpm-utils
Package creation is simplified using the bpm-utils package which contains helper scripts for creating packages
Learn more here: https://git.enumerated.dev/bubble-package-manager/bpm-utils
+2
View File
@@ -0,0 +1,2 @@
privilege_escalator_cmd: "sudo"
compilation_env: []
+7 -8
View File
@@ -1,8 +1,7 @@
compilation_env: []
silent_compilation: false
compilation_dir: "/var/tmp/"
binary_output_dir: "/var/lib/bpm/compiled/"
repositories:
- name: example-repository
source: https://my-repo.xyz/
disabled: true
ignore_packages: []
show_source_package_contents: always
cleanup_make_dependencies: true
databases:
- name: example-database
source: https://my-database.xyz/
disabled: true
-8
View File
@@ -1,8 +0,0 @@
module gitlab.com/bubble-package-manager/bpm
go 1.22
require (
github.com/elliotchance/orderedmap/v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-5
View File
@@ -1,5 +0,0 @@
github.com/elliotchance/orderedmap/v2 v2.4.0 h1:6tUmMwD9F998FNpwFxA5E6NQvSpk2PVw7RKsVq3+2Cw=
github.com/elliotchance/orderedmap/v2 v2.4.0/go.mod h1:85lZyVbpGaGvHvnKa7Qhx7zncAdBIBq6u56Hb1PRU5Q=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-766
View File
@@ -1,766 +0,0 @@
package main
import (
"bufio"
"flag"
"fmt"
"github.com/elliotchance/orderedmap/v2"
"gitlab.com/bubble-package-manager/bpm/utils"
"log"
"os"
"path/filepath"
"slices"
"strings"
)
/* ---BPM | Bubble Package Manager--- */
/* Made By CapCreeperGR */
/* A simple-to-use package manager */
/* ---------------------------------- */
var bpmVer = "0.4.1"
var subcommand = "help"
var subcommandArgs []string
// Flags
var rootDir = "/"
var verbose = false
var yesAll = false
var buildSource = false
var skipCheck = false
var keepTempDir = false
var force = false
var pkgListNumbers = false
var pkgListNames = false
var reinstall = false
var reinstallAll = false
var noOptional = false
var nosync = true
func main() {
utils.ReadConfig()
resolveFlags()
resolveCommand()
}
type commandType uint8
const (
help commandType = iota
version
info
list
search
install
update
sync
remove
file
)
func getCommandType() commandType {
switch subcommand {
case "version":
return version
case "info":
return info
case "list":
return list
case "search":
return search
case "install":
return install
case "update":
return update
case "sync":
return sync
case "remove":
return remove
case "file":
return file
default:
return help
}
}
func resolveCommand() {
switch getCommandType() {
case version:
fmt.Println("Bubble Package Manager (BPM)")
fmt.Println("Version: " + bpmVer)
case info:
packages := subcommandArgs
if len(packages) == 0 {
fmt.Println("No packages were given")
return
}
for n, pkg := range packages {
var info *utils.PackageInfo
info = utils.GetPackageInfo(pkg, rootDir, false)
if info == nil {
log.Fatalf("Error: package (%s) is not installed\n", pkg)
}
fmt.Println("----------------")
fmt.Println(utils.CreateReadableInfo(true, true, true, info, rootDir))
if n == len(packages)-1 {
fmt.Println("----------------")
}
}
case list:
packages, err := utils.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s", err.Error())
return
}
if pkgListNumbers {
fmt.Println(len(packages))
} else if pkgListNames {
for _, pkg := range packages {
fmt.Println(pkg)
}
} else {
if len(packages) == 0 {
fmt.Println("No packages have been installed")
return
}
for n, pkg := range packages {
info := utils.GetPackageInfo(pkg, rootDir, false)
if info == nil {
fmt.Printf("Package (%s) could not be found\n", pkg)
continue
}
fmt.Println("----------------\n" + utils.CreateReadableInfo(true, true, true, info, rootDir))
if n == len(packages)-1 {
fmt.Println("----------------")
}
}
}
case search:
searchTerms := subcommandArgs
if len(searchTerms) == 0 {
log.Fatalf("Error: no search terms given")
}
for _, term := range searchTerms {
nameResults := make([]*utils.PackageInfo, 0)
descResults := make([]*utils.PackageInfo, 0)
for _, repo := range utils.BPMConfig.Repositories {
for _, entry := range repo.Entries {
if strings.Contains(entry.Info.Name, term) {
nameResults = append(nameResults, entry.Info)
} else if strings.Contains(entry.Info.Description, term) {
descResults = append(descResults, entry.Info)
}
}
}
results := append(nameResults, descResults...)
if len(results) == 0 {
log.Fatalf("Error: no results for term (%s) were found\n", term)
}
fmt.Printf("Results for term (%s)\n", term)
for i, result := range results {
fmt.Println("----------------")
fmt.Printf("%d) %s: %s (%s)\n", i+1, result.Name, result.Description, result.GetFullVersion())
}
}
case install:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
pkgs := subcommandArgs
if len(pkgs) == 0 {
fmt.Println("No packages or files were given to install")
return
}
pkgsToInstall := orderedmap.NewOrderedMap[string, *struct {
bpmFile string
isDependency bool
shouldFetch bool
pkgInfo *utils.PackageInfo
}]()
unresolvedDepends := make([]string, 0)
// Search for packages
for _, pkg := range pkgs {
if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() {
pkgInfo, err := utils.ReadPackage(pkg)
if err != nil {
log.Fatalf("Error: could not read package: %s\n", err)
}
if !reinstall && utils.IsPackageInstalled(pkgInfo.Name, rootDir) && utils.GetPackageInfo(pkgInfo.Name, rootDir, true).GetFullVersion() == pkgInfo.GetFullVersion() {
continue
}
pkgsToInstall.Set(pkgInfo.Name, &struct {
bpmFile string
isDependency bool
shouldFetch bool
pkgInfo *utils.PackageInfo
}{bpmFile: pkg, isDependency: false, shouldFetch: false, pkgInfo: pkgInfo})
} else {
entry, _, err := utils.GetRepositoryEntry(pkg)
if err != nil {
log.Fatalf("Error: could not find package (%s) in any repository\n", pkg)
}
if !reinstall && utils.IsPackageInstalled(entry.Info.Name, rootDir) && utils.GetPackageInfo(entry.Info.Name, rootDir, true).GetFullVersion() == entry.Info.GetFullVersion() {
continue
}
pkgsToInstall.Set(entry.Info.Name, &struct {
bpmFile string
isDependency bool
shouldFetch bool
pkgInfo *utils.PackageInfo
}{bpmFile: "", isDependency: false, shouldFetch: true, pkgInfo: entry.Info})
}
}
clone := pkgsToInstall.Copy()
pkgsToInstall = orderedmap.NewOrderedMap[string, *struct {
bpmFile string
isDependency bool
shouldFetch bool
pkgInfo *utils.PackageInfo
}]()
for _, pkg := range clone.Keys() {
value, _ := clone.Get(pkg)
resolved, unresolved := value.pkgInfo.ResolveAll(&[]string{}, &[]string{}, value.pkgInfo.Type == "source", !noOptional, !reinstall, rootDir)
unresolvedDepends = append(unresolvedDepends, unresolved...)
for _, depend := range resolved {
if _, ok := pkgsToInstall.Get(depend); !ok && depend != value.pkgInfo.Name {
if !reinstallAll && utils.IsPackageInstalled(depend, rootDir) {
continue
}
entry, _, err := utils.GetRepositoryEntry(depend)
if err != nil {
log.Fatalf("Error: could not find package (%s) in any repository\n", pkg)
}
pkgsToInstall.Set(depend, &struct {
bpmFile string
isDependency bool
shouldFetch bool
pkgInfo *utils.PackageInfo
}{bpmFile: "", isDependency: true, shouldFetch: true, pkgInfo: entry.Info})
}
}
pkgsToInstall.Set(pkg, value)
}
// Show summary
if len(unresolvedDepends) != 0 {
if !force {
log.Fatalf("Error: the following dependencies could not be found in any repositories: %s\n", strings.Join(unresolvedDepends, ", "))
} else {
log.Println("Warning: The following dependencies could not be found in any repositories: " + strings.Join(unresolvedDepends, ", "))
}
}
if pkgsToInstall.Len() == 0 {
fmt.Println("All packages are up to date!")
os.Exit(0)
}
for _, pkg := range pkgsToInstall.Keys() {
value, _ := pkgsToInstall.Get(pkg)
pkgInfo := value.pkgInfo
installedInfo := utils.GetPackageInfo(pkgInfo.Name, rootDir, false)
sourceInfo := ""
if pkgInfo.Type == "source" {
if rootDir != "/" && !force {
log.Fatalf("Error: cannot compile and install source packages to a different root directory")
}
sourceInfo = "(From Source)"
}
if installedInfo == nil {
fmt.Printf("%s: %s (Install) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), sourceInfo)
} else if strings.Compare(pkgInfo.GetFullVersion(), installedInfo.GetFullVersion()) < 0 {
fmt.Printf("%s: %s -> %s (Downgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), sourceInfo)
} else if strings.Compare(pkgInfo.GetFullVersion(), installedInfo.GetFullVersion()) > 0 {
fmt.Printf("%s: %s -> %s (Upgrade) %s\n", pkgInfo.Name, installedInfo.GetFullVersion(), pkgInfo.GetFullVersion(), sourceInfo)
} else {
fmt.Printf("%s: %s (Reinstall) %s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), sourceInfo)
}
}
if rootDir != "/" {
fmt.Println("Warning: Operating in " + rootDir)
}
if !yesAll {
reader := bufio.NewReader(os.Stdin)
if pkgsToInstall.Len() == 1 {
fmt.Printf("Do you wish to install this package? [y\\N] ")
} else {
fmt.Printf("Do you wish to install these %d packages? [y\\N] ", pkgsToInstall.Len())
}
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling...")
os.Exit(1)
}
}
// Fetch packages from repositories
fmt.Println("Fetching packages from available repositories...")
for _, pkg := range pkgsToInstall.Keys() {
value, _ := pkgsToInstall.Get(pkg)
if !value.shouldFetch {
continue
}
entry, repo, err := utils.GetRepositoryEntry(pkg)
if err != nil {
log.Fatalf("Error: could not find package (%s) in any repository\n", pkg)
}
fetchedPackage, err := repo.FetchPackage(entry.Info.Name)
if err != nil {
log.Fatalf("Error: could not fetch package (%s): %s\n", pkg, err)
}
fmt.Printf("Package (%s) was successfully fetched!\n", value.pkgInfo.Name)
value.bpmFile = fetchedPackage
pkgsToInstall.Set(pkg, value)
}
// Install fetched packages
for _, pkg := range pkgsToInstall.Keys() {
value, _ := pkgsToInstall.Get(pkg)
pkgInfo := value.pkgInfo
var err error
if value.isDependency {
err = utils.InstallPackage(value.bpmFile, rootDir, verbose, true, buildSource, skipCheck, keepTempDir)
} else {
err = utils.InstallPackage(value.bpmFile, rootDir, verbose, force, buildSource, skipCheck, keepTempDir)
}
if err != nil {
if pkgInfo.Type == "source" && keepTempDir {
fmt.Println("BPM temp directory was created at /var/tmp/bpm_source-" + pkgInfo.Name)
}
log.Fatalf("Error: could not install package (%s): %s\n", pkg, err)
}
fmt.Printf("Package (%s) was successfully installed\n", pkgInfo.Name)
if value.isDependency {
err := utils.SetInstallationReason(pkgInfo.Name, utils.Dependency, rootDir)
if err != nil {
log.Fatalf("Error: could not set installation reason for package: %s\n", err)
}
}
if pkgInfo.Type == "source" && keepTempDir {
fmt.Println("** It is recommended you delete the temporary bpm folder in /var/tmp **")
}
}
case update:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
// Sync repositories
if !nosync {
for _, repo := range utils.BPMConfig.Repositories {
fmt.Printf("Fetching package database for repository (%s)...\n", repo.Name)
err := repo.SyncLocalDatabase()
if err != nil {
log.Fatalf("Error: could not sync local database for repository (%s): %s\n", repo.Name, err)
}
}
fmt.Println("All package databases synced successfully!")
}
utils.ReadConfig()
// Get installed packages and check for updates
pkgs, err := utils.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s\n", err)
}
toUpdate := orderedmap.NewOrderedMap[string, *struct {
isDependency bool
entry *utils.RepositoryEntry
}]()
for _, pkg := range pkgs {
entry, _, err := utils.GetRepositoryEntry(pkg)
if err != nil {
continue
}
installedInfo := utils.GetPackageInfo(pkg, rootDir, true)
if installedInfo == nil {
log.Fatalf("Error: could not get package info for (%s)\n", pkg)
}
if strings.Compare(entry.Info.GetFullVersion(), installedInfo.GetFullVersion()) > 0 {
toUpdate.Set(entry.Info.Name, &struct {
isDependency bool
entry *utils.RepositoryEntry
}{isDependency: false, entry: entry})
} else if reinstall {
toUpdate.Set(entry.Info.Name, &struct {
isDependency bool
entry *utils.RepositoryEntry
}{isDependency: false, entry: entry})
}
}
if toUpdate.Len() == 0 {
fmt.Println("All packages are up to date!")
os.Exit(0)
}
// Check for new dependencies in updated packages
unresolved := make([]string, 0)
clone := toUpdate.Copy()
for _, key := range clone.Keys() {
pkg, _ := clone.Get(key)
r, u := pkg.entry.Info.ResolveAll(&[]string{}, &[]string{}, pkg.entry.Info.Type == "source", !noOptional, true, rootDir)
unresolved = append(unresolved, u...)
for _, depend := range r {
if _, ok := toUpdate.Get(depend); !ok {
entry, _, err := utils.GetRepositoryEntry(depend)
if err != nil {
log.Fatalf("Error: could not find package (%s) in any repository\n", depend)
}
toUpdate.Set(depend, &struct {
isDependency bool
entry *utils.RepositoryEntry
}{isDependency: true, entry: entry})
}
}
}
if len(unresolved) != 0 {
if !force {
log.Fatalf("Error: the following dependencies could not be found in any repositories: %s\n", strings.Join(unresolved, ", "))
} else {
log.Printf("Warning: the following dependencies could not be found in any repositories: %s\n", strings.Join(unresolved, ", "))
}
}
for _, key := range toUpdate.Keys() {
value, _ := toUpdate.Get(key)
installedInfo := utils.GetPackageInfo(value.entry.Info.Name, rootDir, true)
sourceInfo := ""
if value.entry.Info.Type == "source" {
sourceInfo = "(From Source)"
}
if installedInfo == nil {
fmt.Printf("%s: %s (Install) %s\n", value.entry.Info.Name, value.entry.Info.GetFullVersion(), sourceInfo)
continue
}
if strings.Compare(value.entry.Info.GetFullVersion(), installedInfo.GetFullVersion()) > 0 {
fmt.Printf("%s: %s -> %s (Upgrade) %s\n", value.entry.Info.Name, installedInfo.GetFullVersion(), value.entry.Info.GetFullVersion(), sourceInfo)
} else if reinstall {
fmt.Printf("%s: %s -> %s (Reinstall) %s\n", value.entry.Info.Name, installedInfo.GetFullVersion(), value.entry.Info.GetFullVersion(), sourceInfo)
}
}
// Update confirmation prompt
if !yesAll {
fmt.Printf("Are you sure you wish to update all %d packages? [y\\N] ", toUpdate.Len())
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling update...")
os.Exit(1)
}
}
// Fetch packages
pkgsToInstall := orderedmap.NewOrderedMap[string, *struct {
isDependency bool
entry *utils.RepositoryEntry
}]()
fmt.Println("Fetching packages from available repositories...")
for _, pkg := range toUpdate.Keys() {
value, _ := toUpdate.Get(pkg)
entry, repo, err := utils.GetRepositoryEntry(pkg)
if err != nil {
log.Fatalf("Error: could not find package (%s) in any repository\n", pkg)
}
fetchedPackage, err := repo.FetchPackage(entry.Info.Name)
if err != nil {
log.Fatalf("Error: could not fetch package (%s): %s\n", pkg, err)
}
fmt.Printf("Package (%s) was successfully fetched!\n", value.entry.Info.Name)
pkgsToInstall.Set(fetchedPackage, value)
}
// Install fetched packages
for _, pkg := range pkgsToInstall.Keys() {
value, _ := pkgsToInstall.Get(pkg)
pkgInfo := value.entry.Info
var err error
if value.isDependency {
err = utils.InstallPackage(pkg, rootDir, verbose, true, buildSource, skipCheck, keepTempDir)
} else {
err = utils.InstallPackage(pkg, rootDir, verbose, force, buildSource, skipCheck, keepTempDir)
}
if err != nil {
if pkgInfo.Type == "source" && keepTempDir {
fmt.Println("BPM temp directory was created at /var/tmp/bpm_source-" + pkgInfo.Name)
}
log.Fatalf("Error: could not install package (%s): %s\n", pkg, err)
}
fmt.Printf("Package (%s) was successfully installed!\n", pkgInfo.Name)
if value.isDependency {
err := utils.SetInstallationReason(pkgInfo.Name, utils.Dependency, rootDir)
if err != nil {
log.Fatalf("Error: could not set installation reason for package: %s\n", err)
}
}
if pkgInfo.Type == "source" && keepTempDir {
fmt.Println("** It is recommended you delete the temporary bpm folder in /var/tmp **")
}
}
case sync:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
if !yesAll {
fmt.Printf("Are you sure you wish to sync all databases? [y\\N] ")
reader := bufio.NewReader(os.Stdin)
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Println("Cancelling sync...")
os.Exit(1)
}
}
for _, repo := range utils.BPMConfig.Repositories {
fmt.Printf("Fetching package database for repository (%s)...\n", repo.Name)
err := repo.SyncLocalDatabase()
if err != nil {
log.Fatalf("Error: could not sync local database for repository (%s): %s\n", repo.Name, err)
}
}
fmt.Println("All package databases synced successfully!")
case remove:
if os.Getuid() != 0 {
log.Fatalf("Error: this subcommand needs to be run with superuser permissions")
}
packages := subcommandArgs
if len(packages) == 0 {
fmt.Println("No packages were given")
return
}
for _, pkg := range packages {
pkgInfo := utils.GetPackageInfo(pkg, rootDir, false)
if pkgInfo == nil {
fmt.Printf("Package (%s) could not be found\n", pkg)
continue
}
fmt.Println("----------------\n" + utils.CreateReadableInfo(false, false, false, pkgInfo, rootDir))
fmt.Println("----------------")
if rootDir != "/" {
fmt.Println("Warning: Operating in " + rootDir)
}
if !yesAll {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Do you wish to remove this package? [y\\N] ")
text, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(text)) != "y" && strings.TrimSpace(strings.ToLower(text)) != "yes" {
fmt.Printf("Skipping package (%s)...\n", pkgInfo.Name)
continue
}
}
err := utils.RemovePackage(pkg, verbose, rootDir)
if err != nil {
log.Fatalf("Error: could not remove package: %s\n", err)
}
fmt.Printf("Package (%s) was successfully removed!\n", pkgInfo.Name)
}
case file:
files := subcommandArgs
if len(files) == 0 {
fmt.Println("No files were given to get which packages manage it")
return
}
for _, file := range files {
absFile, err := filepath.Abs(file)
if err != nil {
log.Fatalf("Error: could not get absolute path of file (%s)\n", file)
}
stat, err := os.Stat(absFile)
if os.IsNotExist(err) {
log.Fatalf("Error: file (%s) does not exist!\n", absFile)
}
pkgs, err := utils.GetInstalledPackages(rootDir)
if err != nil {
log.Fatalf("Error: could not get installed packages: %s\n", err.Error())
}
if !strings.HasPrefix(absFile, rootDir) {
log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile)
}
absFile, err = filepath.Rel(rootDir, absFile)
if err != nil {
log.Fatalf("Error: could not get path of file (%s) relative to root path", absFile)
}
absFile = strings.TrimPrefix(absFile, "/")
if stat.IsDir() {
absFile = absFile + "/"
}
var pkgList []string
for _, pkg := range pkgs {
if slices.Contains(utils.GetPackageFiles(pkg, rootDir), absFile) {
pkgList = append(pkgList, pkg)
}
}
if len(pkgList) == 0 {
fmt.Println(absFile + " is not managed by any packages")
} else {
fmt.Println(absFile + " is managed by the following packages:")
for _, pkg := range pkgList {
fmt.Println("- " + pkg)
}
}
}
default:
printHelp()
}
}
func printHelp() {
fmt.Println("\033[1m---- Command Format ----\033[0m")
fmt.Println("-> command format: bpm <subcommand> [-flags]...")
fmt.Println("-> flags will be read if passed right after the subcommand otherwise they will be read as subcommand arguments")
fmt.Println("\033[1m---- Command List ----\033[0m")
fmt.Println("-> bpm version | shows information on the installed version of bpm")
fmt.Println("-> bpm info [-R] <packages...> | shows information on an installed package")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println("-> bpm list [-R, -c, -n] | lists all installed packages")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -c lists the amount of installed packages")
fmt.Println(" -n lists only the names of installed packages")
fmt.Println("-> bpm search <search terms...> | Searches for packages through declared repositories")
fmt.Println("-> bpm install [-R, -v, -y, -f, -o, -c, -b, -k, --reinstall, --reinstall-all, --no-optional] <packages...> | installs the following files")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println(" -f skips dependency, conflict and architecture checking")
fmt.Println(" -o=<path> set the binary package output directory (defaults to /var/lib/bpm/compiled)")
fmt.Println(" -c=<path> set the compilation directory (defaults to /var/tmp)")
fmt.Println(" -b creates a binary package from a source package after compilation and saves it in the binary package output directory")
fmt.Println(" -k keeps the compilation directory created by BPM after source package installation")
fmt.Println(" --reinstall Reinstalls packages even if they do not have a newer version available")
fmt.Println(" --reinstall-all Same as --reinstall but also reinstalls dependencies")
fmt.Println(" --no-optional Prevents installation of optional dependencies")
fmt.Println("-> bpm update [-R, -v, -y, -f, --reinstall, --no-sync] | updates all packages that are available in the repositories")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println(" -f skips dependency, conflict and architecture checking")
fmt.Println(" --reinstall Fetches and reinstalls all packages even if they do not have a newer version available")
fmt.Println(" --no-sync Skips package database syncing")
fmt.Println("-> bpm sync [-R, -v, -y] | Syncs package databases without updating packages")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println("-> bpm remove [-R, -v, -y] <packages...> | removes the following packages")
fmt.Println(" -v Show additional information about what BPM is doing")
fmt.Println(" -R=<path> lets you define the root path which will be used")
fmt.Println(" -y skips the confirmation prompt")
fmt.Println("-> bpm file [-R] <files...> | shows what packages the following packages are managed by")
fmt.Println(" -R=<root_path> lets you define the root path which will be used")
fmt.Println("\033[1m----------------\033[0m")
}
func resolveFlags() {
// List flags
listFlagSet := flag.NewFlagSet("List flags", flag.ExitOnError)
listFlagSet.Usage = printHelp
listFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
listFlagSet.BoolVar(&pkgListNumbers, "c", false, "List the number of all packages installed with BPM")
listFlagSet.BoolVar(&pkgListNames, "n", false, "List the names of all packages installed with BPM")
// Info flags
infoFlagSet := flag.NewFlagSet("Info flags", flag.ExitOnError)
infoFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
infoFlagSet.Usage = printHelp
// Install flags
installFlagSet := flag.NewFlagSet("Install flags", flag.ExitOnError)
installFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
installFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
installFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
installFlagSet.StringVar(&utils.BPMConfig.BinaryOutputDir, "o", utils.BPMConfig.BinaryOutputDir, "Set the binary output directory")
installFlagSet.StringVar(&utils.BPMConfig.CompilationDir, "c", utils.BPMConfig.CompilationDir, "Set the compilation directory")
installFlagSet.BoolVar(&buildSource, "b", false, "Build binary package from source package")
installFlagSet.BoolVar(&skipCheck, "s", false, "Skip check function during source compilation")
installFlagSet.BoolVar(&keepTempDir, "k", false, "Keep temporary directory after source compilation")
installFlagSet.BoolVar(&force, "f", false, "Force installation by skipping architecture and dependency resolution")
installFlagSet.BoolVar(&reinstall, "reinstall", false, "Reinstalls packages even if they do not have a newer version available")
installFlagSet.BoolVar(&reinstallAll, "reinstall-all", false, "Same as --reinstall but also reinstalls dependencies")
installFlagSet.BoolVar(&noOptional, "no-optional", false, "Prevents installation of optional dependencies")
installFlagSet.Usage = printHelp
// Update flags
updateFlagSet := flag.NewFlagSet("Update flags", flag.ExitOnError)
updateFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
updateFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
updateFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
updateFlagSet.BoolVar(&force, "f", false, "Force update by skipping architecture and dependency resolution")
updateFlagSet.BoolVar(&reinstall, "reinstall", false, "Fetches and reinstalls all packages even if they do not have a newer version available")
updateFlagSet.BoolVar(&nosync, "no-sync", false, "Skips package database syncing")
updateFlagSet.Usage = printHelp
// Sync flags
syncFlagSet := flag.NewFlagSet("Sync flags", flag.ExitOnError)
syncFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
syncFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
syncFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
syncFlagSet.Usage = printHelp
// Remove flags
removeFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError)
removeFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
removeFlagSet.BoolVar(&verbose, "v", false, "Show additional information about what BPM is doing")
removeFlagSet.BoolVar(&yesAll, "y", false, "Skip confirmation prompts")
removeFlagSet.Usage = printHelp
// File flags
fileFlagSet := flag.NewFlagSet("Remove flags", flag.ExitOnError)
fileFlagSet.StringVar(&rootDir, "R", "/", "Set the destination root")
fileFlagSet.Usage = printHelp
if len(os.Args[1:]) <= 0 {
subcommand = "help"
} else {
subcommand = os.Args[1]
subcommandArgs = os.Args[2:]
if getCommandType() == list {
err := listFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = listFlagSet.Args()
} else if getCommandType() == info {
err := infoFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = infoFlagSet.Args()
} else if getCommandType() == install {
err := installFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = installFlagSet.Args()
} else if getCommandType() == update {
err := updateFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = updateFlagSet.Args()
} else if getCommandType() == sync {
err := syncFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = syncFlagSet.Args()
} else if getCommandType() == remove {
err := removeFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = removeFlagSet.Args()
} else if getCommandType() == file {
err := fileFlagSet.Parse(subcommandArgs)
if err != nil {
return
}
subcommandArgs = fileFlagSet.Args()
}
if reinstallAll {
reinstall = true
}
}
}
+23
View File
@@ -0,0 +1,23 @@
module bpm
go 1.24.0
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/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
)
+66
View File
@@ -0,0 +1,66 @@
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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1690
View File
File diff suppressed because it is too large Load Diff
+758
View File
@@ -0,0 +1,758 @@
package bpmlib
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"syscall"
"github.com/drone/envsubst"
"gopkg.in/yaml.v3"
)
var rootCompilationUID = "65534"
var rootCompilationGID = "65534"
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
outputBpmPackages = make(map[string]string)
// Read BPM archive
bpmpkg, err := ReadPackage(archiveFilename)
if err != nil {
return nil, err
}
// Ensure package type is 'source'
if bpmpkg.PkgInfo.Type != "source" {
return nil, errors.New("cannot compile a non-source package")
}
// Get HOME directory
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
// Get UID and GID to use for compilation
var uid, gid int
if os.Getuid() == 0 {
_uid, err := strconv.ParseInt(rootCompilationUID, 10, 32)
if err != nil {
return nil, fmt.Errorf("could not convert UID '%s' to int", rootCompilationUID)
}
_gid, err := strconv.ParseInt(rootCompilationGID, 10, 32)
if err != nil {
return nil, fmt.Errorf("could not convert GID '%s' to int", rootCompilationGID)
}
uid = int(_uid)
gid = int(_gid)
} else {
uid = os.Getuid()
gid = os.Getgid()
}
// Set temporary directory
var tempDirectory string
if os.Getuid() == 0 {
tempDirectory = path.Join("/var/cache/bpm/compilation/", bpmpkg.PkgInfo.Name)
} else {
tempDirectory = path.Join(homeDir, ".cache/bpm/compilation/", bpmpkg.PkgInfo.Name)
}
// Ensure temporary directory does not exist
if _, err := os.Stat(tempDirectory); err == nil {
err := os.RemoveAll(tempDirectory)
if err != nil {
return nil, err
}
}
// Create temporary directory
err = os.MkdirAll(tempDirectory, 0755)
if err != nil {
return nil, err
}
// Change temporary directory owner
err = os.Chown(tempDirectory, uid, gid)
if err != nil {
return nil, err
}
// Extract source.sh file
err = extractTarballFile(archiveFilename, "source.sh", tempDirectory, uid, gid)
if err != nil {
return nil, err
}
// Get package scripts and extract them
packageScripts := getPackageScripts(archiveFilename)
for _, script := range packageScripts {
err = extractTarballFile(archiveFilename, script, tempDirectory, uid, gid)
if err != nil {
return nil, err
}
}
// Extract source files
err = extractTarballDirectory(archiveFilename, "source-files", tempDirectory, uid, gid)
if err != nil {
return nil, err
}
// Create source directory
err = os.Mkdir(path.Join(tempDirectory, "source"), 0755)
if err != nil {
return nil, err
}
// Change source directory owner
err = os.Chown(path.Join(tempDirectory, "source"), uid, gid)
if err != nil {
return nil, err
}
// Download files
err = downloadPackageFiles(bpmpkg.PkgInfo, tempDirectory, verbose)
if err != nil {
return nil, err
}
// Setup environment for commands
env := os.Environ()
env = append(env, "HOME="+tempDirectory)
env = append(env, "BPM_WORKDIR="+tempDirectory)
env = append(env, "BPM_SOURCE="+path.Join(tempDirectory, "source"))
env = append(env, "BPM_PKG_NAME="+bpmpkg.PkgInfo.Name)
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_URL="+bpmpkg.PkgInfo.Url)
env = append(env, "BPM_PKG_ARCH="+bpmpkg.PkgInfo.OutputArch)
env = append(env, "BPM_JOBS="+strconv.Itoa(compilationJobs))
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
cmd := exec.Command("bash", "-c",
"set -a\n"+ // Source and export functions and variables in source.sh script
". \"${BPM_WORKDIR}\"/source.sh\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 build) == \"function\" ]] && { echo \"Running build() function...\"; bash -e -c 'cd \"$BPM_SOURCE\" && build' || exit 1; }\n"+ // Run build() function if it exists
"exit 0")
cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
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
}
// Execute check function in source.sh script if not skipping checks
if !skipChecks {
cmd = exec.Command("bash", "-c",
"set -a\n"+ // Source and export functions and variables in source.sh script
". \"${BPM_WORKDIR}\"/source.sh\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
"exit 0")
cmd.Dir = tempDirectory
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
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
}
}
// Get all packages to compile
packagesToCompile := bpmpkg.PkgInfo.SplitPackages
if !bpmpkg.PkgInfo.IsSplitPackage() {
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
for _, pkg := range packagesToCompile {
// Get package function name
packageFunctionName := "package"
if bpmpkg.PkgInfo.IsSplitPackage() {
packageFunctionName = "package_" + pkg.Name
}
// Execute package function in source.sh script and generate package file list
cmd = exec.Command("bash", "-c",
"set -a\n"+ // Source and export functions and variables in source.sh script
". \"${BPM_WORKDIR}\"/source.sh\n"+
"set +a\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
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 {
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 {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}
err = cmd.Run()
if err != nil {
return nil, err
}
// Create gzip-compressed archive for the package files
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.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
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, fmt.Errorf("files.tar.gz archive could not be created: %s", err)
}
// Clone source package info
pkgInfo := *pkg
// Set package type to binary
pkgInfo.Type = "binary"
// Set package architecture
pkgInfo.Arch = pkg.OutputArch
pkgInfo.OutputArch = ""
// Remove split package and downloads fields
pkgInfo.SplitPackages = nil
pkgInfo.Downloads = nil
// Marshal package info
pkgInfoBytes, err := yaml.Marshal(pkgInfo)
if err != nil {
return nil, err
}
pkgInfoBytes = append(pkgInfoBytes, '\n')
// Create pkg.info file
err = os.WriteFile(path.Join(tempDirectory, "pkg.info"), pkgInfoBytes, 0644)
if err != nil {
return nil, err
}
// Change pkg.info file owner
err = os.Chown(path.Join(tempDirectory, "pkg.info"), uid, gid)
if err != nil {
return nil, err
}
// Get files to include in BPM archive
bpmArchiveFiles := make([]string, 0)
bpmArchiveFiles = append(bpmArchiveFiles, "pkg.info", "pkg.files", "files.tar.gz") // Base files
bpmArchiveFiles = append(bpmArchiveFiles, packageScripts...) // Package scripts
// 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.Dir = tempDirectory
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
currentDir, err := os.Getwd()
if err != nil {
return nil, err
}
cmd.Env = append(env, "CURRENT_DIR="+currentDir)
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, fmt.Errorf("BPM archive could not be created: %s", err)
}
// Remove pkg.info file
err = os.Remove(path.Join(tempDirectory, "pkg.info"))
if err != nil {
return nil, err
}
// Set output filename
outputFilename := path.Join(outputDirectory, fmt.Sprintf("%s-%s-%d-%s.bpm", pkgInfo.Name, pkgInfo.Version, pkgInfo.Revision, pkgInfo.Arch))
// Move final BPM archive
err = os.Rename(path.Join(tempDirectory, "final-archive.bpm"), outputFilename)
if err != nil {
return nil, err
}
// Set final BPM archive owner
err = os.Chown(outputFilename, os.Getuid(), os.Getgid())
if err != nil {
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
}
// Delete temporary directory
if !keepCompilationFiles {
os.RemoveAll(tempDirectory)
}
return outputBpmPackages, nil
}
func downloadPackageFiles(pkgInfo *PackageInfo, tempDirectory string, verbose bool) error {
// Get UID and GID to use for compilation
var uid, gid int
if os.Getuid() == 0 {
_uid, err := strconv.ParseInt(rootCompilationUID, 10, 32)
if err != nil {
return fmt.Errorf("could not convert UID '%s' to int", rootCompilationUID)
}
_gid, err := strconv.ParseInt(rootCompilationGID, 10, 32)
if err != nil {
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 _, download := range pkgInfo.Downloads {
// Replace variables
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 ""
}
}
downloadUrl, err := envsubst.Eval(strings.TrimSpace(download.Url), replaceVars)
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
}
// Print source file content
err = printTarballContent(file)
if err != nil {
return err
}
}
// Print source.sh content
err = printTarballContent("source.sh")
if err != nil {
return err
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package bpmlib
import (
"os"
"gopkg.in/yaml.v3"
)
type MainBPMConfigStruct struct {
IgnorePackages []string `yaml:"ignore_packages"`
IgnorePaths []string `yaml:"ignore_paths"`
ShowSourcePackageContents string `yaml:"show_source_package_contents"`
CleanupMakeDependencies bool `yaml:"cleanup_make_dependencies"`
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 {
PrivilegeEscalatorCmd string `yaml:"privilege_escalator_cmd"`
CompilationJobs int `yaml:"compilation_jobs"`
CompilationEnvironment []string `yaml:"compilation_env"`
}
var MainBPMConfig MainBPMConfigStruct
var CompilationBPMConfig CompilationBPMConfigStruct
func ReadConfig() (err error) {
var file *os.File
// Set default config options
MainBPMConfig = MainBPMConfigStruct{
ShowSourcePackageContents: "always",
CleanupMakeDependencies: true,
}
// Read main BPM config
file, err = os.Open("/etc/bpm.conf")
if err != nil {
return err
}
err = yaml.NewDecoder(file).Decode(&MainBPMConfig)
if err != nil {
return err
}
file.Close()
// Read compilation BPM config
if _, err := os.Stat("/etc/bpm-compilation.conf"); err == nil {
file, err = os.Open("/etc/bpm-compilation.conf")
if err != nil {
return err
}
err = yaml.NewDecoder(file).Decode(&CompilationBPMConfig)
if err != nil {
return err
}
file.Close()
}
// Remove disabled databases from memory
for i := len(MainBPMConfig.Databases) - 1; i >= 0; i-- {
if MainBPMConfig.Databases[i].Disabled != nil && *MainBPMConfig.Databases[i].Disabled {
MainBPMConfig.Databases = append(MainBPMConfig.Databases[:i], MainBPMConfig.Databases[i+1:]...)
}
}
return nil
}
+597
View File
@@ -0,0 +1,597 @@
package bpmlib
import (
"bytes"
"errors"
"fmt"
"io"
"maps"
"net/http"
"net/url"
"os"
"path"
"slices"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
type VerificationLevel int
const (
VerificationLevelNone VerificationLevel = iota
VerificationLevelAll
VerificationLevelTrusted
)
type BPMDatabase struct {
DatabaseVersion int `yaml:"database_version"`
Entries map[string]*BPMDatabaseEntry `yaml:"entries"`
VirtualPackages map[string][]*BPMDatabaseEntry
Name string
VerificationLevel VerificationLevel
Source string
}
type BPMDatabaseEntry struct {
Info *PackageInfo `yaml:"info"`
Filepath string `yaml:"filepath"`
DownloadSize int64 `yaml:"download_size"`
InstalledSize int64 `yaml:"installed_size"`
Database *BPMDatabase
}
var BPMDatabases = make(map[string]*BPMDatabase)
func (db *BPMDatabase) ContainsPackage(pkg string) bool {
_, ok := db.Entries[pkg]
return ok
}
func (db *configDatabase) ReadLocalDatabase() error {
dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb"
if _, err := os.Stat(dbFile); err != nil {
return nil
}
bytes, err := os.ReadFile(dbFile)
if err != nil {
return err
}
// Unmarshal yaml
database := &BPMDatabase{}
err = yaml.Unmarshal(bytes, database)
if err != nil {
return err
}
// 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() {
delete(database.Entries, entryName)
// Handle split packages
for _, splitPkg := range entry.Info.SplitPackages {
// Turn split package into json data
splitPkgJson, err := yaml.Marshal(splitPkg)
if err != nil {
return err
}
// Clone all main package fields onto split package
splitPkgClone := *entry.Info
// Set split package field of split package to nil
splitPkgClone.SplitPackages = nil
// Unmarshal json data back to struct
err = yaml.Unmarshal(splitPkgJson, &splitPkgClone)
if err != nil {
return err
}
// Force set split package version, revision and URL
splitPkgClone.Version = entry.Info.Version
splitPkgClone.Revision = entry.Info.Revision
splitPkgClone.Url = entry.Info.Url
// Create entry for split package
database.Entries[splitPkg.Name] = &BPMDatabaseEntry{
Info: &splitPkgClone,
Filepath: entry.Filepath,
DownloadSize: entry.DownloadSize,
InstalledSize: 0,
Database: database,
}
// Add virtual packages to database
for _, p := range splitPkg.Provides {
database.VirtualPackages[p] = append(database.VirtualPackages[p], database.Entries[splitPkg.Name])
}
}
} else {
// Add virtual packages to database
for _, p := range entry.Info.Provides {
database.VirtualPackages[p] = append(database.VirtualPackages[p], entry)
}
}
}
BPMDatabases[db.Name] = database
return nil
}
func (db *configDatabase) SyncLocalDatabaseFile() error {
dbFile := "/var/lib/bpm/databases/" + db.Name + ".bpmdb"
// Get URL to database
u, err := url.JoinPath(db.Source, "database.bpmdb")
if err != nil {
return err
}
// Retrieve data from URL
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 progress bar
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
err = yaml.Unmarshal(buffer.Bytes(), &BPMDatabase{})
if err != nil {
return fmt.Errorf("could not decode database: %s", err)
}
// Create parent directories to database file
err = os.MkdirAll(path.Dir(dbFile), 0755)
if err != nil {
return err
}
// Create file and save database data
out, err := os.Create(dbFile)
if err != nil {
return err
}
defer out.Close()
_, err = out.Write(buffer.Bytes())
return nil
}
func ReadLocalDatabaseFiles() (err error) {
for _, db := range MainBPMConfig.Databases {
// Read database
err = db.ReadLocalDatabase()
if err != nil {
return err
}
}
return nil
}
func GetDatabaseEntry(str string) (*BPMDatabaseEntry, *BPMDatabase, error) {
split := strings.Split(str, "/")
if len(split) == 1 {
pkgName := strings.TrimSpace(split[0])
if pkgName == "" {
return nil, nil, errors.New("could not find database entry for this package")
}
for _, db := range BPMDatabases {
if db.ContainsPackage(pkgName) {
return db.Entries[pkgName], db, nil
}
}
return nil, nil, errors.New("could not find database entry for this package")
} else if len(split) == 2 {
dbName := strings.TrimSpace(split[0])
pkgName := strings.TrimSpace(split[1])
if dbName == "" || pkgName == "" {
return nil, nil, errors.New("could not find database entry for this package")
}
db := BPMDatabases[dbName]
if db == nil || !db.ContainsPackage(pkgName) {
return nil, nil, errors.New("could not find database entry for this package")
}
return db.Entries[pkgName], db, nil
} else {
return nil, nil, errors.New("could not find database entry for this package")
}
}
func FindReplacement(pkg string) *BPMDatabaseEntry {
for _, db := range BPMDatabases {
for _, entry := range db.Entries {
for _, replaced := range entry.Info.Replaces {
if replaced == pkg {
return entry
}
}
}
}
return nil
}
func GetDatabaseVirtualPackageEntry(vpkg string) (providers []*BPMDatabaseEntry) {
for _, db := range BPMDatabases {
providers = append(providers, db.VirtualPackages[vpkg]...)
}
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) {
// Check if package exists in database
if !db.ContainsPackage(pkg) {
return "", errors.New("could not fetch package '" + pkg + "'")
}
// Get package url from database
entry := db.Entries[pkg]
u, err := url.JoinPath(db.Source, entry.Filepath)
if err != nil {
return "", err
}
// 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 {
return "", err
}
// Download and verify signature if required
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())
}
+247
View File
@@ -0,0 +1,247 @@
package bpmlib
import (
"slices"
"strings"
)
func (pkgInfo *PackageInfo) GetPackageDependants(rootDir string, skipMultipleProviders bool) (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 dependencies include pkgName
if slices.ContainsFunc(installedPkg.Depends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == pkgInfo.Name
}) {
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
}
// Loop through each virtual package
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
if slices.ContainsFunc(installedPkg.Depends, func(n string) bool {
n, _, _ = SplitPkgNameAndVersion(n)
return n == vpkg
}) {
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
}
}
}
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
}
}
+54
View File
@@ -0,0 +1,54 @@
package bpmlib
import (
"fmt"
"slices"
"strings"
)
type PackageNotFoundErr struct {
packages []string
}
func (e PackageNotFoundErr) Error() string {
slices.Sort(e.packages)
return "The following packages were not found in any databases: " + strings.Join(e.packages, ", ")
}
type DependencyNotFoundErr struct {
dependencies []string
}
func (e DependencyNotFoundErr) Error() string {
slices.Sort(e.dependencies)
return "The following dependencies were not found in any databases: " + strings.Join(e.dependencies, ", ")
}
type PackageConflictErr struct {
pkg string
conflicts []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, ", "))
}
type PackageScriptErr struct {
err error
packageName string
packageScript 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)
}
type PackageRemovalDependencyErr struct {
RequiredPackages map[string][]string
}
func (e PackageRemovalDependencyErr) Error() string {
return "removing these package would break other installed packages"
}
+560
View File
@@ -0,0 +1,560 @@
package bpmlib
import (
"errors"
"fmt"
"log"
"maps"
"os"
"path"
"slices"
"strings"
)
// 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, reinstallPackages bool, installRuntimeDependencies, forceInstallation, runChecks bool, verbose bool, packages ...string) (operation *BPMOperation, err error) {
// Setup operation struct
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
ModifiedFiles: make(map[string]string),
RunChecks: runChecks,
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Remove duplicates from packages
packages = removeDuplicates(packages)
// Resolve packages
pkgsNotFound := make([]string, 0)
for _, pkg := range packages {
if stat, err := os.Stat(pkg); err == nil && !stat.IsDir() {
bpmpkg, err := ReadPackage(pkg)
if err != nil {
return nil, fmt.Errorf("could not read package: %s", err)
}
if bpmpkg.PkgInfo.Type == "source" && bpmpkg.PkgInfo.IsSplitPackage() {
for _, splitPkg := range bpmpkg.PkgInfo.SplitPackages {
if !reinstallPackages && IsPackageInstalled(splitPkg.Name, rootDir) && GetPackageInfo(splitPkg.Name, rootDir).GetFullVersion() == splitPkg.GetFullVersion() {
continue
}
// Set package installation reason
installationReason := forceInstallationReason
if installationReason == InstallationReasonUnknown {
if IsPackageInstalled(splitPkg.Name, rootDir) {
installationReason = GetPackage(splitPkg.Name, rootDir).LocalInfo.GetInstallationReason()
} else {
installationReason = InstallationReasonManual
}
}
operation.Actions = append(operation.Actions, &InstallPackageAction{
File: pkg,
InstallationReason: installationReason,
BpmPackage: bpmpkg,
SplitPackageToInstall: splitPkg.Name,
})
}
continue
}
if !reinstallPackages && IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) && GetPackageInfo(bpmpkg.PkgInfo.Name, rootDir).GetFullVersion() == bpmpkg.PkgInfo.GetFullVersion() {
continue
}
// Set package installation reason
installationReason := forceInstallationReason
if installationReason == InstallationReasonUnknown {
if IsPackageInstalled(bpmpkg.PkgInfo.Name, rootDir) {
installationReason = GetPackage(bpmpkg.PkgInfo.Name, rootDir).LocalInfo.GetInstallationReason()
} else {
installationReason = InstallationReasonManual
}
}
operation.Actions = append(operation.Actions, &InstallPackageAction{
File: pkg,
InstallationReason: installationReason,
BpmPackage: bpmpkg,
})
} else {
// Split package name and required version
pkgName, _, _ := SplitPkgNameAndVersion(pkg)
var entry *BPMDatabaseEntry
if e, _, err := GetDatabaseEntry(pkgName); err == nil {
entry = e
} else if providers := GetVirtualPackageInfo(pkgName, rootDir); len(providers) > 0 {
entry, _, err = GetDatabaseEntry(providers[0].Name)
if err != nil {
pkgsNotFound = append(pkgsNotFound, pkg)
continue
}
} else if providers := GetDatabaseVirtualPackageEntry(pkgName); len(providers) > 0 {
entry = providers[0]
} else {
pkgsNotFound = append(pkgsNotFound, pkg)
continue
}
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
}
// Set package installation reason
installationReason := forceInstallationReason
if installationReason == InstallationReasonUnknown {
if IsPackageInstalled(entry.Info.Name, rootDir) {
installationReason = GetPackage(entry.Info.Name, rootDir).LocalInfo.GetInstallationReason()
} else {
installationReason = InstallationReasonManual
}
}
operation.Actions = append(operation.Actions, &FetchPackageAction{
InstallationReason: installationReason,
DatabaseEntry: entry,
})
}
}
// Return error if not all packages are found
if len(pkgsNotFound) != 0 {
return nil, PackageNotFoundErr{pkgsNotFound}
}
// Resolve dependencies
operation.ResolveDependencies(installRuntimeDependencies)
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
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)
}
}
// Check whether compiling source packages on different root directory
if rootDir != "/" {
sourcePackages := make([]string, 0)
for _, action := range operation.Actions {
switch action := action.(type) {
case *InstallPackageAction:
if action.BpmPackage.PkgInfo.Type == "source" {
sourcePackages = append(sourcePackages, action.BpmPackage.PkgInfo.Name)
}
case *FetchPackageAction:
if action.DatabaseEntry.Info.Type == "source" {
sourcePackages = append(sourcePackages, action.DatabaseEntry.Info.Name)
}
}
}
// Return error if source packages are present in the operation
if len(sourcePackages) != 0 {
return nil, fmt.Errorf("cannot compile source packages in different root directory: %s", strings.Join(sourcePackages, ", "))
}
}
return operation, nil
}
// RemovePackages removes the specified packages from the given root directory
func RemovePackages(rootDir string, force, cleanupDependencies bool, packages ...string) (operation *BPMOperation, err error) {
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
ModifiedFiles: make(map[string]string),
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Remove duplicates from packages
packages = removeDuplicates(packages)
// Search for packages
for _, pkg := range packages {
bpmpkg := GetPackage(pkg, rootDir)
if providers := GetVirtualPackageInfo(pkg, rootDir); len(providers) > 0 {
bpmpkg = GetPackage(providers[0].Name, rootDir)
}
if bpmpkg == nil {
continue
}
operation.Actions = append(operation.Actions, &RemovePackageAction{BpmPackage: bpmpkg})
}
// Do package cleanup
if cleanupDependencies {
err := operation.Cleanup(MainBPMConfig.CleanupMakeDependencies)
if err != nil {
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
}
// CleanupPackages finds packages installed as dependencies which are no longer required by the rest of the system in the given root directory
func CleanupPackages(cleanupMakeDepends bool, rootDir string) (operation *BPMOperation, err error) {
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
ModifiedFiles: make(map[string]string),
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Do package cleanup
err = operation.Cleanup(cleanupMakeDepends)
if err != nil {
return nil, fmt.Errorf("could not perform cleanup for operation: %s", err)
}
return operation, nil
}
func CleanupCache(rootDir string, cleanupCompilationFiles, cleanupCompiledPackages, cleanupFetchedPackages, verbose bool) error {
if cleanupCompilationFiles {
globalCompilationCacheDir := path.Join(rootDir, "var/cache/bpm/compilation")
// Ensure path exists and is a directory
if stat, err := os.Stat(globalCompilationCacheDir); err == nil && stat.IsDir() {
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", globalCompilationCacheDir)
}
err = os.RemoveAll(globalCompilationCacheDir)
if err != nil {
return err
}
}
// Get home directories of users in root directory
homeDirs, err := os.ReadDir(path.Join(rootDir, "home"))
if err != nil {
return err
}
// Loop through all home directories
for _, homeDir := range homeDirs {
// Skip if not a directory
if !homeDir.IsDir() {
continue
}
localCompilationDir := path.Join(rootDir, "home", homeDir.Name(), ".cache/bpm/compilation")
// Ensure path exists and is a directory
if stat, err := os.Stat(localCompilationDir); err != nil || !stat.IsDir() {
continue
}
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", localCompilationDir)
}
err = os.RemoveAll(localCompilationDir)
if err != nil {
return err
}
}
}
if cleanupCompiledPackages {
dirToRemove := path.Join(rootDir, "var/cache/bpm/compiled")
// Ensure path exists and is a directory
if stat, err := os.Stat(dirToRemove); err == nil && stat.IsDir() {
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", dirToRemove)
}
err = os.RemoveAll(dirToRemove)
if err != nil {
return err
}
}
}
if cleanupFetchedPackages {
dirToRemove := path.Join(rootDir, "var/cache/bpm/fetched")
// Ensure path exists and is a directory
if stat, err := os.Stat(dirToRemove); err == nil && stat.IsDir() {
// Delete directory
if verbose {
log.Printf("Removing directory (%s)\n", dirToRemove)
}
err = os.RemoveAll(dirToRemove)
if err != nil {
return err
}
}
}
return nil
}
// UpdatePackages fetches the newest versions of all installed packages from
func UpdatePackages(rootDir string, syncDatabase, allowDowngrades, forceInstallation, runChecks, verbose bool) (operation *BPMOperation, err error) {
// Sync databases
if syncDatabase {
err := SyncDatabase(verbose)
if err != nil {
return nil, fmt.Errorf("could not sync local database: %s", err)
}
if verbose {
fmt.Println("All package databases synced successfully!")
}
// Reload config and local databases
err = ReadConfig()
if err != nil {
return nil, fmt.Errorf("could not read BPM config: %s", err)
}
err = ReadLocalDatabaseFiles()
if err != nil {
return nil, fmt.Errorf("could not read local databases: %s", err)
}
}
// Get installed packages and check for updates
pkgs, err := GetInstalledPackages(rootDir)
if err != nil {
return nil, fmt.Errorf("could not get installed packages: %s", err)
}
operation = &BPMOperation{
Actions: make([]OperationAction, 0),
UnresolvedDepends: make([]string, 0),
ModifiedFiles: make(map[string]string),
RunChecks: runChecks,
RootDir: rootDir,
compiledPackages: make(map[string]string),
}
// Search for packages
pkgsNotFound := make([]string, 0)
for _, pkg := range pkgs {
if rootDir == "/" && slices.Contains(MainBPMConfig.IgnorePackages, pkg) {
continue
}
var entry *BPMDatabaseEntry
// Check if installed package can be replaced and install that instead
if e := FindReplacement(pkg); e != nil {
entry = e
} else if entry, _, err = GetDatabaseEntry(pkg); err != nil {
continue
}
installedInfo := GetPackageInfo(pkg, rootDir)
if installedInfo == nil {
return nil, fmt.Errorf("could not get package info for package (%s)", pkg)
} else {
comparison := CompareVersions(entry.Info.GetFullVersion(), installedInfo.GetFullVersion())
if (!allowDowngrades && comparison > 0) || (allowDowngrades && comparison != 0) {
operation.Actions = append(operation.Actions, &FetchPackageAction{
InstallationReason: GetPackage(pkg, rootDir).LocalInfo.GetInstallationReason(),
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,
})
}
}
}
// Return error if not all packages are found
if len(pkgsNotFound) != 0 {
return nil, PackageNotFoundErr{pkgsNotFound}
}
// Replace obsolete packages
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
}
// SyncDatabase syncs all databases declared in /etc/bpm.conf
func SyncDatabase(verbose bool) (err error) {
for _, db := range MainBPMConfig.Databases {
if verbose {
fmt.Printf("Fetching package database file for database (%s)...\n", db.Name)
}
err := db.SyncLocalDatabaseFile()
if err != nil {
return err
}
}
return nil
}
+17
View File
@@ -0,0 +1,17 @@
module github.com/EnumeratedDev/bpm/src/bpmlib
go 1.24.0
require (
github.com/drone/envsubst v1.0.3
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
)
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
)
+30
View File
@@ -0,0 +1,30 @@
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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+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
}
+137
View File
@@ -0,0 +1,137 @@
package bpmlib
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"syscall"
"gopkg.in/yaml.v3"
)
type BPMHook struct {
SourcePath string
SourceContent string
TriggerActions []string `yaml:"trigger_actions"`
TriggerPreOperation bool `yaml:"trigger_pre_operation"`
Targets []string `yaml:"targets"`
Run string `yaml:"run"`
PassTargets bool `yaml:"pass_targets"`
}
// createHook returns a BPMHook instance based on the content of the given string
func createHook(sourcePath string) (*BPMHook, error) {
// Read hook from source path
bytes, err := os.ReadFile(sourcePath)
if err != nil {
return nil, err
}
// Create base hook structure
hook := &BPMHook{
SourcePath: sourcePath,
SourceContent: string(bytes),
TriggerActions: nil,
Targets: nil,
Run: "",
}
// Unmarshal yaml string
err = yaml.Unmarshal(bytes, hook)
if err != nil {
return nil, err
}
// Ensure hook is valid
if err := hook.IsValid(); err != nil {
return nil, err
}
return hook, nil
}
// IsValid ensures hook is valid
func (hook *BPMHook) IsValid() error {
ValidOperations := []string{"install", "upgrade", "remove"}
// Return error if any trigger operation is not valid or none are given
if len(hook.TriggerActions) == 0 {
return errors.New("no trigger operations specified")
}
for _, operation := range hook.TriggerActions {
if !slices.Contains(ValidOperations, operation) {
return errors.New("trigger operation '" + operation + "' is not valid")
}
}
if len(hook.Run) == 0 {
return errors.New("command to run is empty")
}
// Return nil as hook is valid
return nil
}
// Execute hook if all conditions are met
func (hook *BPMHook) Execute(modifiedFiles map[string]string, preOperation bool, verbose bool, rootDir string) error {
// Check if any targets are met
targetsMet := make([]string, 0)
for _, target := range hook.Targets {
for modifiedFile, action := range modifiedFiles {
// Check if this hook is triggered by this file's action
if !slices.Contains(hook.TriggerActions, action) {
continue
}
// Check if file has already been checked
if slices.Contains(targetsMet, modifiedFile) {
continue
}
if matched, _ := filepath.Match(target, modifiedFile); !matched {
continue
}
targetsMet = append(targetsMet, modifiedFile)
}
}
if len(targetsMet) == 0 {
return nil
}
// Execute the command
splitCommand := strings.Split(hook.Run, " ")
cmd := exec.Command(splitCommand[0], splitCommand[1:]...)
// Setup subprocess environment
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
if rootDir != "/" {
cmd.SysProcAttr = &syscall.SysProcAttr{Chroot: rootDir}
}
if !verbose {
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()
if err != nil {
return err
}
return nil
}
+400
View File
@@ -0,0 +1,400 @@
package bpmlib
import (
"fmt"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
var persistentDataVersion int = 1
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
if _, ok := localPackageInformation[rootDir]; ok {
return nil
}
tempPackageInformation := make(map[string]*PackageInfo)
tempInstalledVirtualPackages := make(map[string][]*PackageInfo)
// Get paths
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
items, err := os.ReadDir(installedDir)
if os.IsNotExist(err) {
localPackageInformation[rootDir] = make(map[string]*PackageInfo)
return nil
}
if err != nil {
return err
}
// Loop through each subdirectory
for _, item := range items {
// Skip if not a directory
if !item.IsDir() {
continue
}
// Read package info
infoData, err := os.ReadFile(path.Join(installedDir, item.Name(), "info"))
if err != nil {
return err
}
info, err := ReadPackageInfo(string(infoData))
if err != nil {
return err
}
// Add package to slice
tempPackageInformation[info.Name] = info
// Add virtual packages
for _, vpkg := range info.Provides {
tempInstalledVirtualPackages[vpkg] = append(tempInstalledVirtualPackages[vpkg], info)
}
}
localPackageInformation[rootDir] = tempPackageInformation
installedVirtualPackages[rootDir] = tempInstalledVirtualPackages
return nil
}
func GetInstalledPackages(rootDir string) (ret []string, err error) {
// Initialize local package information
err = InitializeLocalPackageInformation(rootDir)
if err != nil {
return nil, err
}
// Loop through each package and add it to slice
for _, pkgInfo := range localPackageInformation[rootDir] {
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
}
func IsPackageInstalled(pkg, rootDir string) bool {
// Initialize local package information
err := InitializeLocalPackageInformation(rootDir)
if err != nil {
return false
}
if _, ok := localPackageInformation[rootDir][pkg]; !ok {
return false
}
return true
}
func GetVirtualPackageInfo(vpkg, rootDir string) []*PackageInfo {
err := InitializeLocalPackageInformation(rootDir)
if err != nil {
return nil
}
providers := installedVirtualPackages[rootDir][vpkg]
slices.SortFunc(providers, func(a, b *PackageInfo) int {
return strings.Compare(a.Name, b.Name)
})
return providers
}
func GetPackageInfo(pkg string, rootDir string) *PackageInfo {
err := InitializeLocalPackageInformation(rootDir)
if err != nil {
return nil
}
return localPackageInformation[rootDir][pkg]
}
func GetPackage(pkg, rootDir string) *BPMPackage {
pkgInfo := GetPackageInfo(pkg, rootDir)
if pkgInfo == nil {
return nil
}
files := getPackageFiles(pkgInfo.Name, rootDir)
localInfo := getPackageLocalInfo(pkgInfo.Name, rootDir)
return &BPMPackage{
PkgInfo: pkgInfo,
PkgFiles: files,
LocalInfo: localInfo,
}
}
func GetAllPackageFiles(rootDir string, excludePackages ...string) (map[string][]*BPMPackage, error) {
ret := make(map[string][]*BPMPackage)
pkgNames, err := GetInstalledPackages(rootDir)
if err != nil {
return nil, err
}
for _, pkgName := range pkgNames {
if slices.Contains(excludePackages, pkgName) {
continue
}
bpmpkg := GetPackage(pkgName, rootDir)
if bpmpkg == nil {
return nil, fmt.Errorf("could not get BPM package (%s)", pkgName)
}
for _, entry := range bpmpkg.PkgFiles {
ret[entry.Path] = append(ret[entry.Path], bpmpkg)
}
}
return ret, nil
}
func getPackageFiles(pkg, rootDir string) []*PackageFileEntry {
var pkgFiles []*PackageFileEntry
installedDir := path.Join(rootDir, "var/lib/bpm/installed/")
pkgDir := path.Join(installedDir, pkg)
files := path.Join(pkgDir, "files")
if _, err := os.Stat(installedDir); os.IsNotExist(err) {
return nil
}
if _, err := os.Stat(pkgDir); os.IsNotExist(err) {
return nil
}
bs, err := os.ReadFile(files)
if err != nil {
return nil
}
for _, line := range strings.Split(string(bs), "\n") {
if strings.TrimSpace(line) == "" {
continue
}
stringEntry := strings.Split(strings.TrimSpace(line), " ")
if len(stringEntry) < 5 {
pkgFiles = append(pkgFiles, &PackageFileEntry{
Path: strings.TrimSuffix(line, "/"),
OctalPerms: 0,
UserID: 0,
GroupID: 0,
SizeInBytes: 0,
})
continue
}
uid, err := strconv.ParseInt(stringEntry[len(stringEntry)-4], 0, 32)
if err != nil {
return nil
}
octalPerms, err := strconv.ParseInt(stringEntry[len(stringEntry)-3], 0, 32)
if err != nil {
return nil
}
gid, err := strconv.ParseInt(stringEntry[len(stringEntry)-2], 0, 32)
if err != nil {
return nil
}
size, err := strconv.ParseInt(stringEntry[len(stringEntry)-1], 0, 64)
if err != nil {
return nil
}
pkgFiles = append(pkgFiles, &PackageFileEntry{
Path: strings.TrimSuffix(strings.Join(stringEntry[:len(stringEntry)-4], " "), "/"),
OctalPerms: uint32(octalPerms),
UserID: int(uid),
GroupID: int(gid),
SizeInBytes: size,
})
}
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
}
+833
View File
@@ -0,0 +1,833 @@
package bpmlib
import (
"errors"
"fmt"
"log"
"os"
"path"
"slices"
"strings"
"text/tabwriter"
)
type BPMOperation struct {
Actions []OperationAction
UnresolvedDepends []string
ModifiedFiles map[string]string
CompilationJobs int
RunChecks bool
RootDir string
compiledPackages map[string]string
hasFetchedPackages bool
}
func (operation *BPMOperation) GetTotalDownloadSize() int64 {
var ret int64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).DatabaseEntry.DownloadSize
}
}
return ret
}
func (operation *BPMOperation) GetTotalInstalledSize() int64 {
var ret int64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize()
} else if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).DatabaseEntry.InstalledSize
}
}
return ret
}
func (operation *BPMOperation) GetFinalActionSize(rootDir string) int64 {
var ret int64 = 0
for _, action := range operation.Actions {
if action.GetActionType() == "install" {
ret += action.(*InstallPackageAction).BpmPackage.GetInstalledSize()
if IsPackageInstalled(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir) {
ret -= GetPackage(action.(*InstallPackageAction).BpmPackage.PkgInfo.Name, rootDir).GetInstalledSize()
}
} else if action.GetActionType() == "fetch" {
ret += action.(*FetchPackageAction).DatabaseEntry.InstalledSize
if IsPackageInstalled(action.(*FetchPackageAction).DatabaseEntry.Info.Name, rootDir) {
ret -= action.(*FetchPackageAction).DatabaseEntry.InstalledSize
}
} else if action.GetActionType() == "remove" {
ret -= action.(*RemovePackageAction).BpmPackage.GetInstalledSize()
}
}
return ret
}
func (operation *BPMOperation) ResolveDependencies(installRuntimeDepends bool) {
// Discover resolved virtual packages
resolvedVirtualPackages := make(map[string]string)
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 _, 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 = removeDuplicates(operation.UnresolvedDepends)
for _, resolvedPkg := range resolved {
if ActionSliceIndex(newActions, resolvedPkg.DatabaseEntry.Info.Name) == -1 { // Dependency not in actions slice
var action OperationAction = &FetchPackageAction{
InstallationReason: resolvedPkg.InstallationReason,
DatabaseEntry: resolvedPkg.DatabaseEntry,
}
newActions = append(newActions, action)
for _, vpkg := range resolvedPkg.DatabaseEntry.Info.Provides {
if _, ok := resolvedVirtualPackages[vpkg]; !ok {
resolvedVirtualPackages[vpkg] = resolvedPkg.DatabaseEntry.Info.Name
}
}
// Check if can move original action
if i := ActionSliceIndex(operation.Actions, resolvedPkg.DatabaseEntry.Info.Name); i != -1 {
newActions[len(newActions)-1] = operation.Actions[i]
}
}
}
if ActionSliceIndex(newActions, pkgInfo.Name) == -1 {
newActions = append(newActions, value)
}
}
operation.Actions = newActions
}
func (operation *BPMOperation) Cleanup(cleanupMakeDepends bool) error {
// Get all installed packages
installedPackageNames, err := GetInstalledPackages(operation.RootDir)
if err != nil {
return fmt.Errorf("could not get installed packages: %s", err)
}
installedPackages := make([]*PackageInfo, len(installedPackageNames))
for i, value := range installedPackageNames {
pkgInfo := GetPackageInfo(value, operation.RootDir)
if pkgInfo == nil {
return errors.New("could not find installed package (" + value + ")")
}
installedPackages[i] = pkgInfo
}
// Get packages to remove
removeActions := make(map[string]*RemovePackageAction)
for _, action := range slices.Clone(operation.Actions) {
if action.GetActionType() == "remove" {
removeActions[action.(*RemovePackageAction).BpmPackage.PkgInfo.Name] = action.(*RemovePackageAction)
}
}
// Run BFS on all manually installed packages
visited := make([]string, 0)
for _, pkg := range installedPackages {
if getPackageLocalInfo(pkg.Name, operation.RootDir).GetInstallationReason() != InstallationReasonManual {
continue
}
queue := make([]*PackageInfo, 0)
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
}
// Mark package as visited
visited = append(visited, v.Name)
// Get all package dependencies
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)
}
}
}
}
// Remove all packages that were not discovered after running BFS
for _, pkg := range installedPackageNames {
// Do not add package removal action if there already is one
if _, ok := removeActions[pkg]; ok {
continue
}
if !slices.Contains(visited, pkg) {
bpmpkg := GetPackage(pkg, operation.RootDir)
if bpmpkg == nil {
return errors.New("Error: could not find installed package (" + pkg + ")")
}
operation.Actions = append(operation.Actions, &RemovePackageAction{BpmPackage: bpmpkg})
}
}
return nil
}
func (operation *BPMOperation) ReplaceObsoletePackages() {
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 _, r := range pkgInfo.Replaces {
if bpmpkg := GetPackage(r, operation.RootDir); bpmpkg != nil && ActionSliceIndex(operation.Actions, bpmpkg.PkgInfo.Name) == -1 {
var action OperationAction = &RemovePackageAction{
BpmPackage: bpmpkg,
}
operation.Actions = slices.Insert(operation.Actions, 0, action)
}
}
}
}
func (operation *BPMOperation) CheckForConflicts() map[string][]string {
conflicts := make(map[string][]string)
// Get installed packages
installedPackages := localPackageInformation[operation.RootDir]
// Get packages to be removed
removedPackages := make([]string, 0)
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" {
pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo
} else if value.GetActionType() == "fetch" {
pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info
} else {
continue
}
// Check for conflicts with installed packages
for _, installedPkg := range installedPackages {
// Skip if package is to be removed
if slices.Contains(removedPackages, installedPkg.Name) {
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+")")
}
}
}
}
return conflicts
}
func (operation *BPMOperation) ShowOperationSummary() {
if len(operation.Actions) == 0 {
fmt.Println("No action needs to be taken")
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 {
var pkgInfo *PackageInfo
var installationReason = InstallationReasonUnknown
if value.GetActionType() == "install" {
installationReason = value.(*InstallPackageAction).InstallationReason
pkgInfo = value.(*InstallPackageAction).BpmPackage.PkgInfo
if value.(*InstallPackageAction).SplitPackageToInstall != "" {
pkgInfo = pkgInfo.GetSplitPackageInfo(value.(*InstallPackageAction).SplitPackageToInstall)
}
} else if value.GetActionType() == "fetch" {
installationReason = value.(*FetchPackageAction).InstallationReason
pkgInfo = value.(*FetchPackageAction).DatabaseEntry.Info
} else {
pkgInfo = value.(*RemovePackageAction).BpmPackage.PkgInfo
fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Remove", "-", "-")
continue
}
installationReasonStr := ""
switch installationReason {
case InstallationReasonManual:
installationReasonStr = "Manual"
case InstallationReasonDependency:
installationReasonStr = "Dependency"
case InstallationReasonMakeDependency:
installationReasonStr = "Make dependency"
default:
installationReasonStr = "Unknown"
}
installedInfo := GetPackageInfo(pkgInfo.Name, operation.RootDir)
if installedInfo == nil {
fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%t\n", pkgInfo.Name, pkgInfo.GetFullVersion(), "Install", installationReasonStr, pkgInfo.Type == "source")
} else {
comparison := CompareVersions(pkgInfo.GetFullVersion(), installedInfo.GetFullVersion())
if comparison < 0 {
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 {
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 {
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 != "/" {
fmt.Println("Warning: Operating in " + operation.RootDir)
}
if operation.GetTotalDownloadSize() > 0 {
fmt.Printf("%s will be downloaded to complete this operation\n", BytesToHumanReadable(operation.GetTotalDownloadSize()))
}
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)))
} 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)), "-"))
}
}
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
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) 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
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "fetch"
}) {
fmt.Println("Fetching packages from available databases...")
// Create map for fetched packages
fetchedPackages := make(map[string]string)
for i, action := range operation.Actions {
if action.GetActionType() != "fetch" {
continue
}
// Get database entry
entry := action.(*FetchPackageAction).DatabaseEntry
// Create bpmpkg variable
var bpmpkg *BPMPackage
// Check if package has already been fetched from download link
if fetchedFilepath, ok := fetchedPackages[entry.Filepath]; !ok {
// Fetch package from database
fetchedPackage, err := entry.Database.FetchPackage(entry.Info.Name)
if err != nil {
return fmt.Errorf("could not fetch package (%s): %s\n", entry.Info.Name, err)
}
// Read fetched package
bpmpkg, err = ReadPackage(fetchedPackage)
if err != nil {
return fmt.Errorf("could not fetch package (%s): %s\n", entry.Info.Name, err)
}
// Add fetched package to map
fetchedPackages[entry.Filepath] = fetchedPackage
} else {
// Read fetched package
bpmpkg, err = ReadPackage(fetchedFilepath)
if err != nil {
return fmt.Errorf("could not read package (%s): %s\n", entry.Info.Name, err)
}
// 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() {
operation.Actions[i] = &InstallPackageAction{
File: fetchedPackages[entry.Filepath],
InstallationReason: action.(*FetchPackageAction).InstallationReason,
BpmPackage: bpmpkg,
SplitPackageToInstall: entry.Info.Name,
}
} else {
operation.Actions[i] = &InstallPackageAction{
File: fetchedPackages[entry.Filepath],
InstallationReason: action.(*FetchPackageAction).InstallationReason,
BpmPackage: bpmpkg,
}
}
}
}
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
words := make([]string, 0)
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "install"
}) {
words = append(words, "Installing")
}
if slices.ContainsFunc(operation.Actions, func(action OperationAction) bool {
return action.GetActionType() == "remove"
}) {
words = append(words, "Removing")
}
if len(words) == 0 {
return nil
}
fmt.Printf("%s packages...\n", strings.Join(words, "/"))
// Installing/Removing packages from system
for _, action := range operation.Actions {
if action.GetActionType() == "remove" {
pkgInfo := action.(*RemovePackageAction).BpmPackage.PkgInfo
err := removePackage(pkgInfo.Name, verbose, operation.RootDir)
if err != nil {
return fmt.Errorf("could not remove package (%s): %s\n", pkgInfo.Name, err)
}
} else if action.GetActionType() == "install" {
value := action.(*InstallPackageAction)
fileToInstall := value.File
bpmpkg := value.BpmPackage
var err error
// Compile package if type is 'source'
if bpmpkg.PkgInfo.Type == "source" {
// Get path to compiled package directory
compiledDir := path.Join(operation.RootDir, "/var/cache/bpm/compiled/")
// Create compiled package directory if not exists
if _, err := os.Stat(compiledDir); err != nil {
err := os.MkdirAll(compiledDir, 0755)
if err != nil {
return err
}
}
// Get package name to install
pkgNameToInstall := bpmpkg.PkgInfo.Name
if bpmpkg.PkgInfo.IsSplitPackage() {
pkgNameToInstall = value.SplitPackageToInstall
}
// Compile source package if not compiled already
if _, ok := operation.compiledPackages[pkgNameToInstall]; !ok {
outputBpmPackages, err := CompileSourcePackage(value.File, compiledDir, operation.CompilationJobs, !operation.RunChecks, false, verbose)
if err != nil {
return fmt.Errorf("could not compile source package (%s): %s\n", value.File, err)
}
// Add compiled packages to slice
for pkgName, pkgFile := range outputBpmPackages {
operation.compiledPackages[pkgName] = pkgFile
}
}
// Set values
fileToInstall = operation.compiledPackages[pkgNameToInstall]
bpmpkg, err = ReadPackage(fileToInstall)
if err != nil {
return fmt.Errorf("could not read package (%s): %s\n", fileToInstall, err)
}
}
if value.InstallationReason != InstallationReasonManual {
err = installPackage(fileToInstall, value.InstallationReason, operation.RootDir, verbose, true)
} else {
err = installPackage(fileToInstall, value.InstallationReason, operation.RootDir, verbose, force)
}
if err != nil {
return fmt.Errorf("could not install package (%s): %s\n", bpmpkg.PkgInfo.Name, err)
}
}
}
return nil
}
type OperationAction interface {
GetActionType() string
}
type InstallPackageAction struct {
File string
InstallationReason InstallationReason
SplitPackageToInstall string
BpmPackage *BPMPackage
}
func (action *InstallPackageAction) GetActionType() string {
return "install"
}
type FetchPackageAction struct {
InstallationReason InstallationReason
DatabaseEntry *BPMDatabaseEntry
}
func (action *FetchPackageAction) GetActionType() string {
return "fetch"
}
type RemovePackageAction struct {
BpmPackage *BPMPackage
}
func (action *RemovePackageAction) GetActionType() string {
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
}
File diff suppressed because it is too large Load Diff
+209
View File
@@ -0,0 +1,209 @@
package bpmlib
import (
"archive/tar"
"errors"
"io"
"os"
"path"
"strings"
)
type tarballFileReader struct {
tarReader *tar.Reader
file *os.File
}
func listTarballContent(tarballPath string) (content []string, err error) {
file, err := os.Open(tarballPath)
if err != nil {
return nil, err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
switch header.Typeflag {
case tar.TypeDir:
continue
default:
content = append(content, header.Name)
}
}
return content, nil
}
func readTarballFile(tarballPath, fileToExtract string) (*tarballFileReader, error) {
file, err := os.Open(tarballPath)
if err != nil {
return nil, err
}
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if header.Name == fileToExtract {
if header.Typeflag != tar.TypeReg {
return nil, errors.New("file to extract must be a regular file")
}
return &tarballFileReader{
tarReader: tr,
file: file,
}, nil
}
}
return nil, errors.New("could not file in tarball")
}
func extractTarballFile(tarballPath, fileToExtract string, workingDirectory string, uid, gid int) (err error) {
file, err := os.Open(tarballPath)
if err != nil {
return err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
// Skip if filename does not match
if header.Name != fileToExtract {
continue
}
// Trim directory name from header name
header.Name = strings.Split(header.Name, "/")[len(strings.Split(header.Name, "/"))-1]
outputPath := path.Join(workingDirectory, header.Name)
switch header.Typeflag {
case tar.TypeReg:
// Create file and set permissions
file, err = os.Create(outputPath)
if err != nil {
return err
}
err := file.Chmod(header.FileInfo().Mode())
if err != nil {
return err
}
if uid >= 0 && gid >= 0 {
err = file.Chown(uid, gid)
if err != nil {
return err
}
}
// Copy data to file
_, err = io.Copy(file, tr)
if err != nil {
return err
}
// Close file
file.Close()
default:
continue
}
}
return nil
}
func extractTarballDirectory(tarballPath, directoryToExtract, workingDirectory string, uid, gid int) (err error) {
file, err := os.Open(tarballPath)
if err != nil {
return err
}
defer file.Close()
tr := tar.NewReader(file)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if strings.HasPrefix(header.Name, directoryToExtract+"/") {
// Skip directory to extract
if strings.TrimRight(header.Name, "/") == workingDirectory {
continue
}
// Trim directory name from header name
header.Name = strings.TrimPrefix(header.Name, directoryToExtract+"/")
outputPath := path.Join(workingDirectory, header.Name)
switch header.Typeflag {
case tar.TypeDir:
// Create directory
err := os.MkdirAll(outputPath, 0755)
if err != nil {
return err
}
// Set directory owner
if uid >= 0 && gid >= 0 {
err = os.Chown(outputPath, uid, gid)
if err != nil {
return err
}
}
case tar.TypeReg:
// Create file and set permissions
file, err = os.Create(outputPath)
if err != nil {
return err
}
err := file.Chmod(header.FileInfo().Mode())
if err != nil {
return err
}
if uid >= 0 && gid >= 0 {
err = file.Chown(uid, gid)
if err != nil {
return err
}
}
// Copy data to file
_, err = io.Copy(file, tr)
if err != nil {
return err
}
// Close file
file.Close()
default:
continue
}
}
}
return nil
}
+143
View File
@@ -0,0 +1,143 @@
package bpmlib
import (
"fmt"
"io"
"math"
"os"
"path"
"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 {
uname := syscall.Utsname{}
err := syscall.Uname(&uname)
if err != nil {
return "unknown"
}
var byteString [65]byte
var indexLength int
for ; uname.Machine[indexLength] != 0; indexLength++ {
byteString[indexLength] = uint8(uname.Machine[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 {
for i, v := range s {
if v == r {
return append(s[:i], s[i+1:]...)
}
}
return s
}
func BytesToHumanReadable(b int64) string {
bf := float64(b)
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} {
if math.Abs(bf) < 1024.0 {
return fmt.Sprintf("%3.1f%sB", bf, unit)
}
bf /= 1024.0
}
return fmt.Sprintf("%.1fYiB", bf)
}
func removeDuplicates[T comparable](sliceList []T) []T {
allKeys := make(map[T]bool)
list := []T{}
for _, item := range sliceList {
if _, value := allKeys[item]; !value {
allKeys[item] = true
list = append(list, item)
}
}
return list
}
-49
View File
@@ -1,49 +0,0 @@
package utils
import (
"gopkg.in/yaml.v3"
"log"
"os"
)
type BPMConfigStruct struct {
CompilationEnv []string `yaml:"compilation_env"`
SilentCompilation bool `yaml:"silent_compilation"`
BinaryOutputDir string `yaml:"binary_output_dir"`
CompilationDir string `yaml:"compilation_dir"`
Repositories []*Repository `yaml:"repositories"`
}
var BPMConfig BPMConfigStruct
func ReadConfig() {
if _, err := os.Stat("/etc/bpm.conf"); os.IsNotExist(err) {
log.Fatal(err)
}
bytes, err := os.ReadFile("/etc/bpm.conf")
if err != nil {
log.Fatal(err)
}
BPMConfig = BPMConfigStruct{
CompilationEnv: make([]string, 0),
SilentCompilation: false,
BinaryOutputDir: "/var/lib/bpm/compiled/",
CompilationDir: "/var/tmp/",
}
err = yaml.Unmarshal(bytes, &BPMConfig)
if err != nil {
log.Fatal(err)
}
for i := len(BPMConfig.Repositories) - 1; i >= 0; i-- {
if BPMConfig.Repositories[i].Disabled != nil && *BPMConfig.Repositories[i].Disabled {
BPMConfig.Repositories = append(BPMConfig.Repositories[:i], BPMConfig.Repositories[i+1:]...)
}
}
for _, repo := range BPMConfig.Repositories {
repo.Entries = make(map[string]*RepositoryEntry)
err := repo.ReadLocalDatabase()
if err != nil {
log.Fatal(err)
}
}
}
-58
View File
@@ -1,58 +0,0 @@
package utils
import (
"io"
"os"
"os/exec"
"strings"
)
func GetArch() string {
output, err := exec.Command("/usr/bin/uname", "-m").Output()
if err != nil {
return ""
}
return strings.TrimSpace(string(output))
}
func copyFileContents(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
return
}
func stringSliceRemove(s []string, r string) []string {
for i, v := range s {
if v == r {
return append(s[:i], s[i+1:]...)
}
}
return s
}
func stringSliceRemoveEmpty(s []string) []string {
var r []string
for _, str := range s {
if str != "" {
r = append(r, str)
}
}
return r
}
File diff suppressed because it is too large Load Diff
-166
View File
@@ -1,166 +0,0 @@
package utils
import (
"errors"
"gopkg.in/yaml.v3"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
)
type Repository struct {
Name string `yaml:"name"`
Source string `yaml:"source"`
Disabled *bool `yaml:"disabled"`
Entries map[string]*RepositoryEntry
}
type RepositoryEntry struct {
Info *PackageInfo `yaml:"info"`
Download string `yaml:"download"`
}
func (repo *Repository) ContainsPackage(pkg string) bool {
_, ok := repo.Entries[pkg]
return ok
}
func (repo *Repository) ReadLocalDatabase() error {
repoFile := "/var/lib/bpm/repositories/" + repo.Name + ".bpmdb"
if _, err := os.Stat(repoFile); err != nil {
return nil
}
bytes, err := os.ReadFile(repoFile)
if err != nil {
return err
}
data := string(bytes)
for _, b := range strings.Split(data, "---") {
entry := RepositoryEntry{
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: "",
}
err := yaml.Unmarshal([]byte(b), &entry)
if err != nil {
return err
}
repo.Entries[entry.Info.Name] = &entry
}
return nil
}
func (repo *Repository) SyncLocalDatabase() error {
repoFile := "/var/lib/bpm/repositories/" + repo.Name + ".bpmdb"
err := os.MkdirAll(path.Dir(repoFile), 0755)
if err != nil {
return err
}
u, err := url.JoinPath(repo.Source, "database.bpmdb")
if err != nil {
return err
}
resp, err := http.Get(u)
if err != nil {
return err
}
defer resp.Body.Close()
out, err := os.Create(repoFile)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return nil
}
func GetRepository(name string) *Repository {
for _, repo := range BPMConfig.Repositories {
if repo.Name == name {
return repo
}
}
return nil
}
func GetRepositoryEntry(str string) (*RepositoryEntry, *Repository, error) {
split := strings.Split(str, "/")
if len(split) == 1 {
pkgName := strings.TrimSpace(split[0])
if pkgName == "" {
return nil, nil, errors.New("could not find repository entry for this package")
}
for _, repo := range BPMConfig.Repositories {
if repo.ContainsPackage(pkgName) {
return repo.Entries[pkgName], repo, nil
}
}
return nil, nil, errors.New("could not find repository entry for this package")
} else if len(split) == 2 {
repoName := strings.TrimSpace(split[0])
pkgName := strings.TrimSpace(split[1])
if repoName == "" || pkgName == "" {
return nil, nil, errors.New("could not find repository entry for this package")
}
repo := GetRepository(repoName)
if repo == nil || !repo.ContainsPackage(pkgName) {
return nil, nil, errors.New("could not find repository entry for this package")
}
return repo.Entries[pkgName], repo, nil
} else {
return nil, nil, errors.New("could not find repository entry for this package")
}
}
func (repo *Repository) FetchPackage(pkg string) (string, error) {
if !repo.ContainsPackage(pkg) {
return "", errors.New("could not fetch package '" + pkg + "'")
}
entry := repo.Entries[pkg]
URL, err := url.JoinPath(repo.Source, entry.Download)
if err != nil {
return "", err
}
resp, err := http.Get(URL)
if err != nil {
return "", err
}
defer resp.Body.Close()
err = os.MkdirAll("/var/cache/bpm/packages/", 0755)
if err != nil {
return "", err
}
out, err := os.Create("/var/cache/bpm/packages/" + path.Base(entry.Download))
if err != nil {
return "", err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return "/var/cache/bpm/packages/" + path.Base(entry.Download), nil
}