From bb8e6e79b20509ef5257e97594be077cb948c4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B2=D0=B3=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=A5=D1=80?= =?UTF-8?q?=D0=B0=D0=BC=D0=BE=D0=B2?= Date: Mon, 22 Jan 2024 13:36:06 +0300 Subject: [PATCH] Initial commit --- .gitignore | 5 + .goreleaser.yaml | 99 +++ .woodpecker.yml | 9 + LICENSE | 674 ++++++++++++++++++ Makefile | 20 + README.md | 77 +++ assets/logo.png | Bin 0 -> 28389 bytes build.go | 100 +++ docs/README.md | 8 + docs/configuration.md | 44 ++ docs/packages/README.md | 5 + docs/packages/adding-packages.md | 27 + docs/packages/build-scripts.md | 499 ++++++++++++++ docs/packages/conventions.md | 36 + docs/usage.md | 209 ++++++ fix.go | 64 ++ gen.go | 45 ++ go.mod | 126 ++++ go.sum | 613 +++++++++++++++++ helper.go | 86 +++ info.go | 106 +++ install.go | 122 ++++ internal/cliutils/prompt.go | 172 +++++ internal/config/config.go | 79 +++ internal/config/lang.go | 67 ++ internal/config/paths.go | 108 +++ internal/config/version.go | 5 + internal/cpu/cpu.go | 117 ++++ internal/db/db.go | 346 ++++++++++ internal/db/db_test.go | 250 +++++++ internal/dl/dl.go | 379 ++++++++++ internal/dl/file.go | 264 +++++++ internal/dl/git.go | 198 ++++++ internal/dl/torrent.go | 90 +++ internal/dlcache/dlcache.go | 93 +++ internal/dlcache/dlcache_test.go | 76 ++ internal/osutils/move.go | 110 +++ internal/overrides/overrides.go | 223 ++++++ internal/overrides/overrides_test.go | 195 ++++++ internal/pager/highlighting.go | 37 + internal/pager/pager.go | 143 ++++ internal/shutils/decoder/decoder.go | 223 ++++++ internal/shutils/decoder/decoder_test.go | 224 ++++++ internal/shutils/handlers/exec.go | 63 ++ internal/shutils/handlers/exec_test.go | 124 ++++ internal/shutils/handlers/fakeroot.go | 114 +++ internal/shutils/handlers/nop.go | 55 ++ internal/shutils/handlers/nop_test.go | 58 ++ internal/shutils/handlers/restricted.go | 80 +++ internal/shutils/helpers/helpers.go | 284 ++++++++ internal/translations/files/lure.en.toml | 155 +++++ internal/translations/files/lure.ru.toml | 151 ++++ internal/translations/translations.go | 56 ++ internal/types/build.go | 70 ++ internal/types/config.go | 38 + internal/types/repo.go | 26 + list.go | 108 +++ main.go | 115 ++++ pkg/build/build.go | 838 +++++++++++++++++++++++ pkg/build/install.go | 72 ++ pkg/distro/osrelease.go | 118 ++++ pkg/gen/funcs.go | 13 + pkg/gen/pip.go | 84 +++ pkg/gen/tmpls/pip.tmpl.sh | 31 + pkg/loggerctx/log.go | 29 + pkg/manager/apk.go | 166 +++++ pkg/manager/apt.go | 152 ++++ pkg/manager/dnf.go | 160 +++++ pkg/manager/managers.go | 124 ++++ pkg/manager/pacman.go | 159 +++++ pkg/manager/yum.go | 160 +++++ pkg/manager/zypper.go | 160 +++++ pkg/repos/find.go | 83 +++ pkg/repos/find_test.go | 148 ++++ pkg/repos/pull.go | 441 ++++++++++++ pkg/repos/pull_test.go | 109 +++ pkg/search/search.go | 167 +++++ repo.go | 162 +++++ scripts/completion/bash | 21 + scripts/completion/zsh | 20 + scripts/install.sh | 99 +++ upgrade.go | 131 ++++ 82 files changed, 11517 insertions(+) create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 .woodpecker.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets/logo.png create mode 100644 build.go create mode 100644 docs/README.md create mode 100644 docs/configuration.md create mode 100644 docs/packages/README.md create mode 100644 docs/packages/adding-packages.md create mode 100644 docs/packages/build-scripts.md create mode 100644 docs/packages/conventions.md create mode 100644 docs/usage.md create mode 100644 fix.go create mode 100644 gen.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 helper.go create mode 100644 info.go create mode 100644 install.go create mode 100644 internal/cliutils/prompt.go create mode 100644 internal/config/config.go create mode 100644 internal/config/lang.go create mode 100644 internal/config/paths.go create mode 100644 internal/config/version.go create mode 100644 internal/cpu/cpu.go create mode 100644 internal/db/db.go create mode 100644 internal/db/db_test.go create mode 100644 internal/dl/dl.go create mode 100644 internal/dl/file.go create mode 100644 internal/dl/git.go create mode 100644 internal/dl/torrent.go create mode 100644 internal/dlcache/dlcache.go create mode 100644 internal/dlcache/dlcache_test.go create mode 100644 internal/osutils/move.go create mode 100644 internal/overrides/overrides.go create mode 100644 internal/overrides/overrides_test.go create mode 100644 internal/pager/highlighting.go create mode 100644 internal/pager/pager.go create mode 100644 internal/shutils/decoder/decoder.go create mode 100644 internal/shutils/decoder/decoder_test.go create mode 100644 internal/shutils/handlers/exec.go create mode 100644 internal/shutils/handlers/exec_test.go create mode 100644 internal/shutils/handlers/fakeroot.go create mode 100644 internal/shutils/handlers/nop.go create mode 100644 internal/shutils/handlers/nop_test.go create mode 100644 internal/shutils/handlers/restricted.go create mode 100644 internal/shutils/helpers/helpers.go create mode 100644 internal/translations/files/lure.en.toml create mode 100644 internal/translations/files/lure.ru.toml create mode 100644 internal/translations/translations.go create mode 100644 internal/types/build.go create mode 100644 internal/types/config.go create mode 100644 internal/types/repo.go create mode 100644 list.go create mode 100644 main.go create mode 100644 pkg/build/build.go create mode 100644 pkg/build/install.go create mode 100644 pkg/distro/osrelease.go create mode 100644 pkg/gen/funcs.go create mode 100644 pkg/gen/pip.go create mode 100644 pkg/gen/tmpls/pip.tmpl.sh create mode 100644 pkg/loggerctx/log.go create mode 100644 pkg/manager/apk.go create mode 100644 pkg/manager/apt.go create mode 100644 pkg/manager/dnf.go create mode 100644 pkg/manager/managers.go create mode 100644 pkg/manager/pacman.go create mode 100644 pkg/manager/yum.go create mode 100644 pkg/manager/zypper.go create mode 100644 pkg/repos/find.go create mode 100644 pkg/repos/find_test.go create mode 100644 pkg/repos/pull.go create mode 100644 pkg/repos/pull_test.go create mode 100644 pkg/search/search.go create mode 100644 repo.go create mode 100644 scripts/completion/bash create mode 100644 scripts/completion/zsh create mode 100644 scripts/install.sh create mode 100644 upgrade.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a33a32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/lure +/lure-api-server +/cmd/lure-api-server/lure-api-server +/dist/ +/internal/config/version.txt diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..41b9f02 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,99 @@ +before: + hooks: + - go mod tidy +builds: + - id: lure + env: + - CGO_ENABLED=0 + binary: lure + ldflags: + - -X go.elara.ws/lure/internal/config.Version={{.Version}} + goos: + - linux + goarch: + - amd64 + - 386 + - arm64 + - arm + - riscv64 +archives: + - name_template: >- + {{- .ProjectName}}- + {{- .Version}}- + {{- .Os}}- + {{- if .Arch | eq "amd64"}}x86_64 + {{- else if .Arch | eq "386"}}i386 + {{- else if .Arch | eq "arm64"}}aarch64 + {{- else }}{{ .Arch }}{{ end -}} + files: + - scripts/completion/* +nfpms: + - id: lure + package_name: linux-user-repository + file_name_template: >- + {{- .PackageName}}- + {{- .Version}}- + {{- .Os}}- + {{- if .Arch | eq "amd64"}}x86_64 + {{- else if .Arch | eq "386"}}i386 + {{- else if .Arch | eq "arm64"}}aarch64 + {{- else }}{{ .Arch }}{{ end -}} + description: "Linux User REpository" + homepage: 'https://lure.sh' + maintainer: 'Elara Musayelyan ' + license: GPLv3 + formats: + - apk + - deb + - rpm + - archlinux + provides: + - linux-user-repository + conflicts: + - linux-user-repository + recommends: + - aria2 + contents: + - src: scripts/completion/bash + dst: /usr/share/bash-completion/completions/lure + - src: scripts/completion/zsh + dst: /usr/share/zsh/site-functions/_lure +aurs: + - name: linux-user-repository-bin + homepage: 'https://lure.sh' + description: "Linux User REpository" + maintainers: + - 'Elara Musayelyan ' + license: GPLv3 + private_key: '{{ .Env.AUR_KEY }}' + git_url: 'ssh://aur@aur.archlinux.org/linux-user-repository-bin.git' + provides: + - linux-user-repository + conflicts: + - linux-user-repository + depends: + - sudo + - pacman + optdepends: + - 'aria2: for downloading torrent sources' + package: |- + # binaries + install -Dm755 ./lure "${pkgdir}/usr/bin/lure" + + # completions + install -Dm755 ./scripts/completion/bash ${pkgdir}/usr/share/bash-completion/completions/lure + install -Dm755 ./scripts/completion/zsh ${pkgdir}/usr/share/zsh/site-functions/_lure +release: + gitea: + owner: lure + name: lure +gitea_urls: + api: 'https://gitea.elara.ws/api/v1/' + download: 'https://gitea.elara.ws' + skip_tls_verify: false +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc \ No newline at end of file diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..dee2b41 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,9 @@ +platform: linux/amd64 +pipeline: + release: + image: goreleaser/goreleaser + commands: + - goreleaser release + secrets: [ gitea_token, aur_key ] + when: + event: tag \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1840fd9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type 'show c' for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1dc85cb --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +PREFIX ?= /usr/local +GIT_VERSION = $(shell git describe --tags ) + +lure: + CGO_ENABLED=0 go build -ldflags="-X 'go.elara.ws/lure/internal/config.Version=$(GIT_VERSION)'" + +clean: + rm -f lure + +install: lure installmisc + install -Dm755 lure $(DESTDIR)$(PREFIX)/bin/lure + +installmisc: + install -Dm755 scripts/completion/bash $(DESTDIR)$(PREFIX)/share/bash-completion/completions/lure + install -Dm755 scripts/completion/zsh $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_lure + +uninstall: + rm -f /usr/local/bin/lure + +.PHONY: install clean uninstall installmisc lure \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3fad48 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +LURE Logo + +# LURE (Linux User REpository) + +[![Go Report Card](https://goreportcard.com/badge/go.elara.ws/lure)](https://goreportcard.com/report/go.elara.ws/lure) +[![status-badge](https://ci.elara.ws/api/badges/lure/lure/status.svg)](https://ci.elara.ws/lure/lure) +[![SWH](https://archive.softwareheritage.org/badge/origin/https://gitea.elara.ws/lure/lure.git/)](https://archive.softwareheritage.org/browse/origin/?origin_url=https://gitea.elara.ws/lure/lure.git) +[![linux-user-repository-bin AUR package](https://img.shields.io/aur/version/linux-user-repository-bin?label=linux-user-repository-bin&logo=archlinux)](https://aur.archlinux.org/packages/linux-user-repository-bin/) + +LURE is a distro-agnostic build system for Linux, similar to the [AUR](https://wiki.archlinux.org/title/Arch_User_Repository). It is currently in **beta**. Most major bugs have been fixed, and most major features have been added. LURE is ready for general use, but may still break or change occasionally. + +LURE is written in pure Go and has zero dependencies after building. The only things LURE requires are a command for privilege elevation such as `sudo`, `doas`, etc. as well as a supported package manager. Currently, LURE supports `apt`, `pacman`, `apk`, `dnf`, `yum`, and `zypper`. If a supported package manager exists on your system, it will be detected and used automatically. + +--- + +## Installation + +### Install script + +The LURE install script will automatically download and install the appropriate LURE package on your system. To use it, simply run the following command: + +```bash +curl -fsSL lure.sh/install | bash +``` + +**IMPORTANT**: This will download and run the script from https://lure.sh/install. Please look through any script you download from the internet (including this one) before running it. + +### Packages + +Distro packages and binary archives are provided at the latest Gitea release: https://gitea.elara.ws/lure/lure/releases/latest + +LURE is also available on the AUR as [linux-user-repository-bin](https://aur.archlinux.org/packages/linux-user-repository-bin) + +### Building from source + +To build LURE from source, you'll need Go 1.18 or newer. Once Go is installed, clone this repo and run: + +```shell +sudo make install +``` + +--- + +## Why? + +LURE was created because packaging software for multiple Linux distros can be difficult and error-prone, and installing those packages can be a nightmare for users unless they're available in their distro's official repositories. It automates the process of building and installing unofficial packages. + +--- + +## Documentation + +The documentation for LURE is in the [docs](docs) directory in this repo. + +--- + +## Web Interface + +LURE has an open source web interface, licensed under the AGPLv3 (https://gitea.elara.ws/lure/lure-web), and it's available at https://lure.sh/. + +--- + +## Repositories + +LURE's repos are git repositories that contain a directory for each package, with a `lure.sh` file inside. The `lure.sh` file tells LURE how to build the package and information about it. `lure.sh` scripts are similar to the AUR's PKGBUILD scripts. + +--- + +## Acknowledgements + +Thanks to the following projects for making LURE possible: + +- https://github.com/mvdan/sh +- https://github.com/go-git/go-git +- https://github.com/mholt/archiver +- https://github.com/goreleaser/nfpm +- https://github.com/charmbracelet/bubbletea +- https://gitlab.com/cznic/sqlite \ No newline at end of file diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ecd7b5145274a6186eaad128c5423c7778f9b7c8 GIT binary patch literal 28389 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

ph$KM6?l>%wAeWGF9Er3CV>(Sob37 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 literal 0 HcmV?d00001 diff --git a/build.go b/build.go new file mode 100644 index 0000000..db0421a --- /dev/null +++ b/build.go @@ -0,0 +1,100 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "os" + "path/filepath" + + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/osutils" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/build" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/manager" + "lure.sh/lure/pkg/repos" +) + +var buildCmd = &cli.Command{ + Name: "build", + Usage: "Build a local package", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "script", + Aliases: []string{"s"}, + Value: "lure.sh", + Usage: "Path to the build script", + }, + &cli.StringFlag{ + Name: "package", + Aliases: []string{"p"}, + Usage: "Name of the package to build and its repo (example: default/go-bin)", + }, + &cli.BoolFlag{ + Name: "clean", + Aliases: []string{"c"}, + Usage: "Build package from scratch even if there's an already built package available", + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + script := c.String("script") + if c.String("package") != "" { + script = filepath.Join(config.GetPaths(ctx).RepoDir, c.String("package"), "lure.sh") + } + + err := repos.Pull(ctx, config.Config(ctx).Repos) + if err != nil { + log.Fatal("Error pulling repositories").Err(err).Send() + } + + mgr := manager.Detect() + if mgr == nil { + log.Fatal("Unable to detect a supported package manager on the system").Send() + } + + pkgPaths, _, err := build.BuildPackage(ctx, types.BuildOpts{ + Script: script, + Manager: mgr, + Clean: c.Bool("clean"), + Interactive: c.Bool("interactive"), + }) + if err != nil { + log.Fatal("Error building package").Err(err).Send() + } + + wd, err := os.Getwd() + if err != nil { + log.Fatal("Error getting working directory").Err(err).Send() + } + + for _, pkgPath := range pkgPaths { + name := filepath.Base(pkgPath) + err = osutils.Move(pkgPath, filepath.Join(wd, name)) + if err != nil { + log.Fatal("Error moving the package").Err(err).Send() + } + } + + return nil + }, +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e0c027d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,8 @@ +# LURE Docs + +- [Configuration](configuration.md) +- [Usage](usage.md) +- [Packages](packages) + - [Build Scripts](packages/build-scripts.md) + - [Package Conventions](packages/conventions.md) + - [Adding Packages to LURE's repo](packages/adding-packages.md) \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..089b7d6 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,44 @@ +# Configuration + +This page describes the configuration of LURE + +--- + +## Table of Contents + +- [Config file](#config-file) + - [rootCmd](#rootcmd) + - [repo](#repo) + +--- + +## File locations + +| Path | Description +| --: | :-- +| ~/.config/lure/lure.toml | Config file +| ~/.cache/lure/pkgs | here the packages are built and stored +| ~/.cache/lure/repo | here are the git repos with all the `lure.sh` files +| | Example: `~/.cache/lure/repo/default/itd-bin/lure.sh` + +--- + +## Config file + +### rootCmd + +The `rootCmd` field in the config specifies which command should be used for privilege elevation. The default value is `sudo`. + +### repo + +The `repo` array in the config specifies which repos are added to LURE. Each repo must have a name and URL. A repo looks like this in the config: + +```toml +[[repo]] +name = 'default' +url = 'https://github.com/Elara6331/lure-repo.git' +``` + +The `default` repo is added by default. Any amount of repos may be added. + +--- diff --git a/docs/packages/README.md b/docs/packages/README.md new file mode 100644 index 0000000..a8187e8 --- /dev/null +++ b/docs/packages/README.md @@ -0,0 +1,5 @@ +# LURE Docs > Packages + +- [Build Scripts](build-scripts.md) +- [Package Conventions](conventions.md) +- [Adding Packages to LURE's repo](adding-packages.md) \ No newline at end of file diff --git a/docs/packages/adding-packages.md b/docs/packages/adding-packages.md new file mode 100644 index 0000000..55ecdeb --- /dev/null +++ b/docs/packages/adding-packages.md @@ -0,0 +1,27 @@ +# Adding Packages to LURE's repo + +## Requirements + +- `go` (1.18+) +- `git` +- `lure-analyzer` + - `go install go.elara.ws/lure-repo-bot/cmd/lure-analyzer@latest` +- `shfmt` + - May be available in distro repos + - `go install mvdan.cc/sh/v3/cmd/shfmt@latest` + +--- + +## How to test a package + +To test packages you can first create [a `lure.sh` shell file](./build-scripts.md) and then run the `lure build` comand to build the local `lure.sh` file into a package for your distro (more info about the `build` command [here](./usage.md#build)). You can then install this file to your distro and test it. + +## How to submit a package + +LURE's repo is hosted on Github at https://github.com/Elara6331/lure-repo. In it, there are multiple directories each containing a `lure.sh` file. In order to add a package to LURE's repo, simply create a PR with a [build script](./build-scripts.md) and place it in a directory with the same name as the package. + +Upon submitting the PR, [lure-repo-bot](https://github.com/Elara6331/lure-repo-bot) will pull your PR and analyze it, providing suggestions for fixes as review comments. If there are no problems, the bot will approve your changes. If there are issues, re-request review from the bot after you've finished applying the fixes and it will automatically review the PR again. + +All scripts submitted to the LURE repo should be formatted with `shfmt`. If they are not properly formatted, Github Actions will add suggestions in the "Files Changed" tab of the PR. + +Once your PR is merged, LURE will pull the changed repo and your package will be available for people to install. diff --git a/docs/packages/build-scripts.md b/docs/packages/build-scripts.md new file mode 100644 index 0000000..d1f4500 --- /dev/null +++ b/docs/packages/build-scripts.md @@ -0,0 +1,499 @@ +# LURE Build Scripts + +LURE uses build scripts similar to the AUR's PKGBUILDs. This is the documentation for those scripts. + +--- + +## Table of Contents + +- [Distro Overrides](#distro-overrides) +- [Variables](#variables) + - [name](#name) + - [version](#version) + - [release](#release) + - [epoch](#epoch) + - [desc](#desc) + - [homepage](#homepage) + - [maintainer](#maintainer) + - [architectures](#architectures) + - [licenses](#licenses) + - [provides](#provides) + - [conflicts](#conflicts) + - [deps](#deps) + - [build_deps](#build_deps) + - [opt_deps](#opt_deps) + - [replaces](#replaces) + - [sources](#sources) + - [checksums](#checksums) + - [backup](#backup) + - [scripts](#scripts) +- [Functions](#functions) + - [prepare](#prepare) + - [version](#version-1) + - [build](#build) + - [package](#package) +- [Environment Variables](#environment-variables) + - [DISTRO_NAME](#distro_name) + - [DISTRO_PRETTY_NAME](#distro_pretty_name) + - [DISTRO_ID](#distro_id) + - [DISTRO_VERSION_ID](#distro_version_id) + - [ARCH](#arch) + - [NCPU](#ncpu) +- [Helper Commands](#helper-commands) + - [install-binary](#install-binary) + - [install-systemd](#install-systemd) + - [install-systemd-user](#install-systemd-user) + - [install-config](#install-config) + - [install-license](#install-license) + - [install-completion](#install-completion) + - [install-manual](#install-manual) + - [install-desktop](#install-desktop) + - [install-library](#install-library) + - [git-version](#git-version) + +--- + +## Distro Overrides + +Allowing LURE to run on different distros provides some challenges. For example, some distros use different names for their packages. This is solved using distro overrides. Any variable or function used in a LURE build script may be overridden based on distro and CPU architecture. The way you do this is by appending the distro and/or architecture to the end of the name. For example, [ITD](https://gitea.elara.ws/Elara6331/itd) depends on the `pactl` command as well as DBus and BlueZ. These are named somewhat differently on different distros. For ITD, I use the following for the dependencies: + +```bash +deps=('dbus' 'bluez' 'pulseaudio-utils') +deps_arch=('dbus' 'bluez' 'libpulse') +deps_opensuse=('dbus-1' 'bluez' 'pulseaudio-utils') +``` + +Appending `arch` and `opensuse` to the end causes LURE to use the appropriate array based on the distro. If on Arch Linux, it will use `deps_arch`. If on OpenSUSE, it will use `deps_opensuse`, and if on anything else, it will use `deps`. + +Names are checked in the following order: + +- $name_$architecture_$distro +- $name_$distro +- $name_$architecture +- $name + +Distro detection is performed by reading the `/usr/lib/os-release` and `/etc/os-release` files. + +### Like distros + +Inside the `os-release` file, there is a list of "like" distros. LURE takes this into account. For example, if a script contains `deps_debian` but not `deps_ubuntu`, Ubuntu builds will use `deps_debian` because Ubuntu is based on debian. + +Most specificity is preferred, so if both `deps_debian` and `deps_ubuntu` is provided, Ubuntu and all Ubuntu-based distros will use `deps_ubuntu` while Debian and all Debian-based distros +that are not Ubuntu-based will use `deps_debian`. + +Like distros are disabled when using the `LURE_DISTRO` environment variable. + +## Variables + +Any variables marked with `(*)` are required + +### name (*) + +The `name` variable contains the name of the package described by the script. + +### version (*) + +The `version` variable contains the version of the package. This should be the same as the version used by the author upstream. + +Versions are compared using the [rpmvercmp](https://fedoraproject.org/wiki/Archive:Tools/RPM/VersionComparison) algorithm. + +### release (*) + +The `release` number is meant to differentiate between different builds of the same package version, such as if the script is changed but the version stays the same. The `release` must be an integer. + +### epoch + +The `epoch` number forces the package to be considered newer than versions with a lower epoch. It is meant to be used if the versioning scheme can't be used to determine which package is newer. Its use is discouraged and it should only be used if necessary. The `epoch` must be a positive integer. + +### desc + +The `desc` field contains the description for the package. It should not contain any newlines. + +### homepage + +The `homepage` field contains the URL to the website of the project packaged by this script. + +### maintainer + +The `maintainer` field contains the name and email address of the person maintaining the package. Example: + +```text +Elara Musayelyan +``` + +While LURE does not require this field to be set, Debian has deprecated unset maintainer fields, and may disallow their use in `.deb` packages in the future. + +### architectures + +The `architectures` array contains all the architectures that this package supports. These match Go's GOARCH list, except for a few differences. + +The `all` architecture will be translated to the proper term for the packaging format. For example, it will be changed to `noarch` if building a `.rpm`, or `any` if building an Arch package. + +Since multiple variations of the `arm` architecture exist, the following values should be used: + +`arm5`: armv5 +`arm6`: armv6 +`arm7`: armv7 + +LURE will attempt to detect which variant your system is using by checking for the existence of various CPU features. If this yields the wrong result or if you simply want to build for a different variant, the `LURE_ARM_VARIANT` variable should be set to the ARM variant you want. Example: + +```shell +LURE_ARM_VARIANT=arm5 lure install ... +``` + +### licenses + +The `licenses` array contains the licenses used by this package. In order to standardize license names, values should be [SPDX Identifiers](https://spdx.org/licenses/) such as `Apache-2.0`, `MIT`, and `GPL-3.0-only`. If the project uses a license that is not standardized in SPDX, use the value `Custom`. If the project has multiple nonstandard licenses, include `Custom` as many times as there are nonstandard licenses. + +### provides + +The `provides` array specifies what features the package provides. For example, if two packages build `ffmpeg` with different build flags, they should both have `ffmpeg` in the `provides` array. + +### conflicts + +The `conflicts` array contains names of packages that conflict with the one built by this script. If two different packages contain the executable for `ffmpeg`, they cannot be installed at the same time, so they conflict. The `provides` array will also be checked, so this array generally contains the same values as `provides`. + +### deps + +The `deps` array contains the dependencies for the package. LURE repos will be checked first, and if the packages exist there, they will be built and installed. Otherwise, they will be installed from the system repos by your package manager. + +### build_deps + +The `build_deps` array contains the dependencies that are required to build the package. They will be installed before the build starts. Similarly to the `deps` array, LURE repos will be checked first. + +### opt_deps + +The `opt_deps` array contains optional dependencies for the package. A description can be added after ": ", but it's not required. + +```bash +opt_deps_arch=( + 'git: Download git repositories' + 'aria2: Download files' +) +``` + +### replaces + +The `replaces` array contains the packages that are replaced by this package. Generally, if package managers find a package with a `replaces` field set, they will remove the listed package(s) and install that one instead. This is only useful if the packages are being stored in a repo for your package manager. + +### sources + +The `sources` array contains URLs which are downloaded into `$srcdir` before the build starts. + +If the URL provided is an archive or compressed file, it will be extracted. To disable this, add the `~archive=false` query parameter. Example: + +Extracted: +```text +https://example.com/archive.tar.gz +``` + +Not extracted: +```text +https://example.com/archive.tar.gz?~archive=false +``` + +If the URL scheme starts with `git+`, the source will be downloaded as a git repo. The git download mode supports multiple parameters: + +- `~rev`: Specify which revision of the repo to check out. +- `~depth`: Specify what depth should be used when cloning the repo. Must be an integer. +- `~name`: Specify the name of the directory into which the git repo should be cloned. +- `~recursive`: If set to true, submodules will be cloned recursively. It is false by default. + +Examples: + +```text +git+https://gitea.elara.ws/Elara6331/itd?~rev=resource-loading&~depth=1 +``` + +```text +git+https://gitea.elara.ws/lure/lure?~rev=v0.0.1&~recursive=true +``` + +### checksums + +The `checksums` array must be the same length as the `sources` array. It contains checksums for the source files. The files are checked against the checksums and the build fails if they don't match. + +By default, checksums are expected to be sha256. To change the algorithm, add it before the hash with a colon in between. For example, `md5:bc0c6f5dcd06bddbca9a0163e4c9f2e1`. The following algorithms are currently supported: `sha256`, `sha224`, `sha512`, `sha384`, `sha1`, and `md5`. + +To skip the check for a particular source, set the corresponding checksum to `SKIP`. + +### backup + +The `backup` array contains files that should be backed up when upgrading and removing. The exact behavior of this depends on your package manager. All files within this array must be full destination paths. For example, if there's a config called `config` in `/etc` that you want to back up, you'd set it like so: + +```bash +backup=('/etc/config') +``` + +### scripts + +The `scripts` variable contains a Bash associative array that specifies the location of various scripts relative to the build script. Example: + +```bash +scripts=( + ['preinstall']='preinstall.sh' + ['postinstall']='postinstall.sh' + ['preremove']='preremove.sh' + ['postremove']='postremove.sh' + ['preupgrade']='preupgrade.sh' + ['postupgrade']='postupgrade.sh' + ['pretrans']='pretrans.sh' + ['posttrans']='posttrans.sh' +) +``` + +Note: The quotes are required due to limitations with the bash parser used. + +The `preupgrade` and `postupgrade` scripts are only available in `.apk` and Arch Linux packages. + +The `pretrans` and `posttrans` scripts are only available in `.rpm` packages. + +The rest of the scripts are available in all packages. + +--- + +## Functions + +This section documents user-defined functions that can be added to build scripts. Any functions marked with `(*)` are required. + +All functions are executed in the `$srcdir` directory + +### version + +The `version()` function updates the `version` variable. This allows for automatically deriving the version from sources. This is most useful for git packages, which usually don't need to be changed, so their `version` variable stays the same. + +An example of using this for git: + +```bash +version() { + cd "$srcdir/itd" + printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +} +``` + +The AUR equivalent is the [`pkgver()` function](https://wiki.archlinux.org/title/VCS_package_guidelines#The_pkgver()_function) + +### prepare + +The `prepare()` function is meant to prepare the sources for building and packaging. This is the function in which patches should be applied, for example, by the `patch` command, and where tools like `go generate` should be executed. + +### build + +The `build()` function is where the package is actually built. Use the same commands that would be used to manually compile the software. Often, this function is just one line: + +```bash +build() { + make +} +``` + +### package (*) + +The `package()` function is where the built files are placed into the directory that will be used by LURE to build the package. + +Any files that should be installed on the filesystem should go in the `$pkgdir` directory in this function. For example, if you have a binary called `bin` that should be placed in `/usr/bin` and a config file called `bin.cfg` that should be placed in `/etc`, the `package()` function might look like this: + +```bash +package() { + install -Dm755 bin ${pkgdir}/usr/bin/bin + install -Dm644 bin.cfg ${pkgdir}/etc/bin.cfg +} +``` + +--- + +## Environment Variables + +LURE exposes several values as environment variables for use in build scripts. + +### DISTRO_NAME + +The `DISTRO_NAME` variable is the name of the distro as defined in its `os-release` file. + +For example, it's set to `Fedora Linux` in a Fedora 36 docker image + +### DISTRO_PRETTY_NAME + +The `DISTRO_PRETTY_NAME` variable is the "pretty" name of the distro as defined in its `os-release` file. + +For example, it's set to `Fedora Linux 36 (Container Image)` in a Fedora 36 docker image + +### DISTRO_ID + +The `DISTRO_ID` variable is the identifier of the distro as defined in its `os-release` file. This is the same as what LURE uses for overrides. + +For example, it's set to `fedora` in a Fedora 36 docker image + +### DISTRO_ID_LIKE + +The `DISTRO_ID_LIKE` variable contains identifiers of similar distros to the one running, separated by spaces. + +For example, it's set to `opensuse suse` in an OpenSUSE Tumbleweed docker image and `rhel fedora` in a CentOS 8 docker image. + +### DISTRO_VERSION_ID + +The `DISTRO_VERSION_ID` variable is the version identifier of the distro as defined in its `os-release` file. + +For example, it's set to `36` in a Fedora 36 docker image and `11` in a Debian Bullseye docker image + +### ARCH + +The `ARCH` variable is the architecture of the machine running the script. It uses the same naming convention as the values in the `architectures` array + +### NCPU + +The `NCPU` variable is the amount of CPUs available on the machine running the script. It will be set to `8` on a quad core machine with hyperthreading, for example. + +--- + +## Helper Commands + +LURE provides various commands to help packagers create proper cross-distro packages. These commands should be used wherever possible instead of doing the tasks manually. + +### install-binary + +`install-binary` accepts 1-2 arguments. The first argument is the binary you'd like to install. The second is the filename that should be used. + +If the filename argument is not provided, tha name of the input file will be used. + +Examples: + +```bash +install-binary ./itd +install-binary ./itd itd-2 +``` + +### install-systemd + +`install-systemd` installs regular systemd system services (see `install-systemd-user` for user services) + +It accepts 1-2 arguments. The first argument is the service you'd like to install. The second is the filename that should be used. + +If the filename argument is not provided, tha name of the input file will be used. + +Examples: + +```bash +install-systemd ./syncthing@.service +install-systemd-user ./syncthing@.service sync-thing@.service +``` + +### install-systemd-user + +`install-systemd-user` installs systemd user services (services like `itd` meant to be started with `--user`). + +It accepts 1-2 arguments. The first argument is the service you'd like to install. The second is the filename that should be used. + +If the filename argument is not provided, tha name of the input file will be used. + +Examples: + +```bash +install-systemd-user ./itd.service +install-systemd-user ./itd.service infinitime-daemon.service +``` + +### install-config + +`install-config` installs configuration files into the `/etc` directory + +It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used. + +If the filename argument is not provided, tha name of the input file will be used. + +Examples: + +```bash +install-config ./itd.toml +install-config ./itd.example.toml itd.toml +``` + +### install-license + +`install-license` installs a license file + +It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used. + +If the filename argument is not provided, tha name of the input file will be used. + +Examples: + +```bash +install-license ./LICENSE itd/LICENSE +``` + +### install-completion + +`install-completion` installs shell completions + +It currently supports `bash`, `zsh`, and `fish` + +Completions are read from stdin, so they can either be piped in or retrieved from files + +Two arguments are required for this function. The first one is the name of the shell and the second is the name of the completion. + +Examples: + +```bash +./k9s completion fish | install-completion fish k9s +install-completion bash k9s <./k9s/completions/k9s.bash +``` + +### install-manual + +`install-manual` installs manpages. It accepts a single argument, which is the path to the manpage. + +The install path will be determined based on the number at the end of the filename. If a number cannot be extracted, an error will be returned. + +Examples: + +```bash +install-manual ./man/strelaysrv.1 +install-manual ./mdoc.7 +``` + +### install-desktop + +`install-desktop` installs desktop files for applications. It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used. + +If the filename argument is not provided, tha name of the input file will be used. + +Examples: + +```bash +install-desktop ./${name}/share/admc.desktop +install-desktop ./${name}/share/admc.desktop admc-app.desktop +``` + +### install-library + +`install-library` installs shared and static libraries to the correct location. + +This is the most important helper as it contains logic to figure out where to install libraries based on the target distro and CPU architecture. It should almost always be used to install all libraries. + +It accepts 1-2 arguments. The first argument is the config you'd like to install. The second is the filename that should be used. + +If the filename argument is not provided, tha name of the input file will be used. + +Examples: + +```bash +install-library ./${name}/build/libadldap.so +``` + +### git-version + +`git-version` returns a version number based on the git revision of a repository. + +If an argument is provided, it will be used as the path to the repo. Otherwise, the current directory will be used. + +The version number will be the amount of revisions, a dot, and the short hash of the current revision. For example: `118.e4b8348`. + +The AUR's convention includes an `r` at the beginning of the version number. This is ommitted because some distros expect the version number to start with a digit. + +Examples: + +```bash +git-version +git-version "$srcdir/itd" +``` \ No newline at end of file diff --git a/docs/packages/conventions.md b/docs/packages/conventions.md new file mode 100644 index 0000000..2367562 --- /dev/null +++ b/docs/packages/conventions.md @@ -0,0 +1,36 @@ +# Package Conventions + +## General + +Packages should have the name(s) of what they contain in their `provides` and `conflicts` arrays. That way, they can be installed by users without needing to know the full package name. For example, there are two LURE packages for ITD: `itd-bin`, and `itd-git`. Both of them have provides and conflicts arrays specifying the two commands they install: `itd`, and `itctl`. This means that if a user wants to install ITD, they simply have to type `lure in itd` and LURE will prompt them for which one they want to install. + +## Binary packages + +Packages that install download and install precompiled binaries should have a `-bin` suffix. + +## Git packages + +Packages that build and install programs from source code cloned directly from Git should have a `-git` suffix. + +The versions of these packages should consist of the amount of revisions followed by the current revision, separated by a period. For example: `183.80187b0`. Note that unlike the AUR, there is no `r` at the beginning. This is because some package managers refuse to install packages whose version numbers don't start with a digit. + +This version number can be obtained using the following command: + +```bash +printf "%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" +``` + +The `version()` function for such packages should use the LURE-provided `git-version` helper command, like so: + +```bash +version() { + cd "$srcdir/$name" + git-version +} +``` + +This uses LURE's embedded Git implementation, which ensures that the user doesn't need Git installed on their system in order to install `-git` packages. + +## Other packages + +Packages that download sources for a specific version of a program should not have any suffix, even if those sources are downloaded from Git. \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..39c5d6a --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,209 @@ +# Usage + +## Table of Contents + +- [Commands](#commands) + - [install](#install) + - [remove](#remove) + - [upgrade](#upgrade) + - [info](#info) + - [list](#list) + - [build](#build) + - [addrepo](#addrepo) + - [removerepo](#removerepo) + - [refresh](#refresh) + - [fix](#fix) + - [version](#version) +- [Environment Variables](#environment-variables) + - [LURE_DISTRO](#lure_distro) + - [LURE_PKG_FORMAT](#lure_pkg_format) + - [LURE_ARM_VARIANT](#lure_arm_variant) + +--- + +## Commands + +### install + +The install command installs a package from the LURE repos. Any packages that aren't found in LURE's repos get forwarded to the system package manager for installation. + +The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard. + +If multiple packages are found, you will be prompted to select which you want to install. + +By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build. + +Examples: + +```shell +lure in itd-bin # only finds itd-bin +lure in itd # finds itd-bin and itd-git +lure in it% # finds itd-bin, itd-git, and itgui-git +lure in -c itd-bin +``` + +### remove + +The remove command is for convenience. All it does is forwards the remove command to the system package manager. + +Example: + +```shell +lure rm firefox +``` + +### upgrade + +The upgrade command looks through the packages installed on your system and sees if any of them match LURE repo packages. If they do, their versions are compared using the `rpmvercmp` algorithm. If LURE repos contain a newer version, the package is upgraded. + +By default, if a package has already been built, LURE will install the cached package rather than re-build it. Use the `-c` or `--clean` flag to force a re-build. + +Example: + +```shell +lure up +``` + +### info + +The info command displays information about a package in LURE's repos. + +The package arguments do not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard. + +If multiple packages are found, you will be prompted to select which you want to show. + +Example: + +```shell +lure info itd-bin # only finds itd-bin +lure info itd # finds itd-bin and itd-git +lure info it% # finds itd-bin, itd-git, and itgui-git +``` + +### list + +The list command lists all LURE repo packages as well as their versions + +This command accepts a single optional argument. This argument is a pattern to filter found packages against. + +The pattern does not have to be exact. LURE will check the `provides` array if an exact match is not found. There is also support for using "%" as a wildcard. + +There is a `-I` or `--installed` flag that filters out any packages that are not installed on the system + +Examples: + +```shell +lure ls # lists all LURE packages +lure ls -I # lists all installed packages +lure ls i% # lists all packages starting with "i" +lure ls %d # lists all packages ending with "d" +lure ls -I i% # lists all installed packages that start with "i" +``` + +### build + +The build command builds a package using a `lure.sh` build script in the current directory. The path to the script can be changed with the `-s` flag. + +Example: + +```shell +lure build +``` + +### addrepo + +The addrepo command adds a repository to LURE if it doesn't already exist. The `-n` flag sets the name of the repository, and the `-u` flag is the URL to the repository. Both are required. + +Example: + +```shell +lure ar -n default -u https://github.com/Elara6331/lure-repo +``` + +### removerepo + +The removerepo command removes a repository from LURE and deletes its contents if it exists. The `-n` flag specifies the name of the repo to be deleted. + +Example: + +```shell +lure rr -n default +``` + +### refresh + +The refresh command pulls all changes from all LURE repos that have changed. + +Example: + +```shell +lure ref +``` + +### fix + +The fix command attempts to fix issues with LURE by deleting and rebuilding LURE's cache + +Example: + +```shell +lure fix +``` + +### version + +The version command returns the current LURE version and exits + +Example: + +```shell +lure version +``` + +--- + +## Environment Variables + +### LURE_DISTRO + +The `LURE_DISTRO` environment variable should be set to the distro for which the package should be built. It tells LURE which overrides to use. Values should be the same as the `ID` field in `/etc/os-release` or `/usr/lib/os-release`. Possible values include: + +- `arch` +- `alpine` +- `opensuse` +- `debian` + +### LURE_PKG_FORMAT + +The `LURE_PKG_FORMAT` environment variable should be set to the packaging format that should be used. Valid values are: + +- `archlinux` +- `apk` +- `rpm` +- `deb` + +### LURE_ARM_VARIANT + +The `LURE_ARM_VARIANT` environment variable dictates which ARM variant to build for, if LURE is running on an ARM system. Possible values include: + +- `arm5` +- `arm6` +- `arm7` + +--- + +## Cross-packaging for other Distributions + +You can create packages for different distributions +setting the environment variables `LURE_DISTRO` and `LURE_PKG_FORMAT` as mentioned above. + +Examples: + +``` +LURE_DISTRO=arch LURE_PKG_FORMAT=archlinux lure build +LURE_DISTRO=alpine LURE_PKG_FORMAT=apk lure build +LURE_DISTRO=opensuse LURE_PKG_FORMAT=rpm lure build +LURE_DISTRO=debian LURE_PKG_FORMAT=deb lure build +``` + +--- \ No newline at end of file diff --git a/fix.go b/fix.go new file mode 100644 index 0000000..49adf89 --- /dev/null +++ b/fix.go @@ -0,0 +1,64 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "os" + + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/repos" +) + +var fixCmd = &cli.Command{ + Name: "fix", + Usage: "Attempt to fix problems with LURE", + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + db.Close() + paths := config.GetPaths(ctx) + + log.Info("Removing cache directory").Send() + + err := os.RemoveAll(paths.CacheDir) + if err != nil { + log.Fatal("Unable to remove cache directory").Err(err).Send() + } + + log.Info("Rebuilding cache").Send() + + err = os.MkdirAll(paths.CacheDir, 0o755) + if err != nil { + log.Fatal("Unable to create new cache directory").Err(err).Send() + } + + err = repos.Pull(ctx, config.Config(ctx).Repos) + if err != nil { + log.Fatal("Error pulling repos").Err(err).Send() + } + + log.Info("Done").Send() + + return nil + }, +} diff --git a/gen.go b/gen.go new file mode 100644 index 0000000..b14cfbb --- /dev/null +++ b/gen.go @@ -0,0 +1,45 @@ +package main + +import ( + "os" + + "github.com/urfave/cli/v2" + "lure.sh/lure/pkg/gen" +) + +var genCmd = &cli.Command{ + Name: "generate", + Usage: "Generate a LURE script from a template", + Aliases: []string{"gen"}, + Subcommands: []*cli.Command{ + genPipCmd, + }, +} + +var genPipCmd = &cli.Command{ + Name: "pip", + Usage: "Generate a LURE script for a pip module", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Required: true, + }, + &cli.StringFlag{ + Name: "version", + Aliases: []string{"v"}, + Required: true, + }, + &cli.StringFlag{ + Name: "description", + Aliases: []string{"d"}, + }, + }, + Action: func(c *cli.Context) error { + return gen.Pip(os.Stdout, gen.PipOptions{ + Name: c.String("name"), + Version: c.String("version"), + Description: c.String("description"), + }) + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e935d54 --- /dev/null +++ b/go.mod @@ -0,0 +1,126 @@ +module lure.sh/lure + +go 1.21 + +toolchain go1.21.3 + +require ( + github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/PuerkitoBio/purell v1.2.0 + github.com/alecthomas/chroma/v2 v2.9.1 + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.8.0 + github.com/go-git/go-billy/v5 v5.5.0 + github.com/go-git/go-git/v5 v5.9.0 + github.com/goreleaser/nfpm/v2 v2.33.0 + github.com/jmoiron/sqlx v1.3.5 + github.com/mattn/go-isatty v0.0.19 + github.com/mholt/archiver/v4 v4.0.0-alpha.8 + github.com/mitchellh/mapstructure v1.5.0 + github.com/muesli/reflow v0.3.0 + github.com/pelletier/go-toml/v2 v2.1.0 + github.com/schollz/progressbar/v3 v3.13.1 + github.com/urfave/cli/v2 v2.25.7 + github.com/vmihailenco/msgpack/v5 v5.3.5 + go.elara.ws/logger v0.0.0-20230421022458-e80700db2090 + go.elara.ws/translate v0.0.0-20230421025926-32ccfcd110e6 + go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 + golang.org/x/crypto v0.13.0 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 + golang.org/x/sys v0.12.0 + golang.org/x/text v0.13.0 + gopkg.in/yaml.v3 v3.0.1 + lure.sh/fakeroot v0.0.0-20231024000108-b130d64a68ee + modernc.org/sqlite v1.25.0 + mvdan.cc/sh/v3 v3.7.0 +) + +require ( + dario.cat/mergo v1.0.0 // indirect + github.com/AlekSi/pointer v1.2.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect + github.com/bodgit/plumbing v1.2.0 // indirect + github.com/bodgit/sevenzip v1.3.0 // indirect + github.com/bodgit/windows v1.0.0 // indirect + github.com/cavaliergopher/cpio v1.0.1 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/connesc/cipherio v0.2.1 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect + github.com/dsnet/compress v0.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/rpmpack v0.5.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gookit/color v1.5.1 // indirect + github.com/goreleaser/chglog v0.5.0 // indirect + github.com/goreleaser/fileglob v1.3.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect + github.com/shopspring/decimal v1.2.0 // indirect + github.com/skeema/knownhosts v1.2.0 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/ulikunitz/xz v0.5.11 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect + go4.org v0.0.0-20200411211856-f5505b9728dd // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/term v0.12.0 // indirect + golang.org/x/tools v0.13.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + lukechampine.com/uint128 v1.2.0 // indirect + modernc.org/cc/v3 v3.40.0 // indirect + modernc.org/ccgo/v3 v3.16.13 // indirect + modernc.org/libc v1.24.1 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.6.0 // indirect + modernc.org/opt v0.1.3 // indirect + modernc.org/strutil v1.1.3 // indirect + modernc.org/token v1.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e61ed83 --- /dev/null +++ b/go.sum @@ -0,0 +1,613 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= +github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= +github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s= +github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs= +github.com/PuerkitoBio/purell v1.2.0 h1:/Jdm5QfyM8zdlqT6WVZU4cfP23sot6CEHA4CS49Ezig= +github.com/PuerkitoBio/purell v1.2.0/go.mod h1:OhLRTaaIzhvIyofkJfB24gokC7tM42Px5UhoT32THBk= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= +github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/chroma/v2 v2.9.1 h1:0O3lTQh9FxazJ4BYE/MOi/vDGuHn7B+6Bu902N2UZvU= +github.com/alecthomas/chroma/v2 v2.9.1/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= +github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= +github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM= +github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8= +github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY= +github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM= +github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA= +github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/caarlos0/go-rpmutils v0.2.1-0.20211112020245-2cd62ff89b11 h1:IRrDwVlWQr6kS1U8/EtyA1+EHcc4yl8pndcqXWrEamg= +github.com/caarlos0/go-rpmutils v0.2.1-0.20211112020245-2cd62ff89b11/go.mod h1:je2KZ+LxaCNvCoKg32jtOIULcFogJKcL1ZWUaIBjKj0= +github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8= +github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk= +github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= +github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= +github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= +github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= +github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= +github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= +github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/rpmpack v0.5.0 h1:L16KZ3QvkFGpYhmp23iQip+mx1X39foEsqszjMNBm8A= +github.com/google/rpmpack v0.5.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gookit/color v1.5.1 h1:Vjg2VEcdHpwq+oY63s/ksHrgJYCTo0bwWvmmYWdE9fQ= +github.com/gookit/color v1.5.1/go.mod h1:wZFzea4X8qN6vHOSP2apMb4/+w/orMznEzYsIHPaqKM= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/goreleaser/chglog v0.5.0 h1:Sk6BMIpx8+vpAf8KyPit34OgWui8c7nKTMHhYx88jJ4= +github.com/goreleaser/chglog v0.5.0/go.mod h1:Ri46M3lrMuv76FHszs3vtABR8J8k1w9JHYAzxeeOl28= +github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+k+7I= +github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU= +github.com/goreleaser/nfpm/v2 v2.33.0 h1:yBv6jgkPwih4va/S42rceSjJ2Znt3Og/Ntc76oP0tfI= +github.com/goreleaser/nfpm/v2 v2.33.0/go.mod h1:8wwWWvJWmn84xo/Sqiv0aMvEGTHlHZTXTEuVSgQpkIM= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= +github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= +github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= +github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= +github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/smartystreets/assertions v1.13.1 h1:Ef7KhSmjZcK6AVf9YbJdvPYG9avaF0ZxudX+ThRdWfU= +github.com/smartystreets/assertions v1.13.1/go.mod h1:cXr/IwVfSo/RbCSPhoAPv73p3hlSdrBH/b3SdnW/LMY= +github.com/smartystreets/goconvey v1.8.0 h1:Oi49ha/2MURE0WexF052Z0m+BNSGirfjg5RL+JXWq3w= +github.com/smartystreets/goconvey v1.8.0/go.mod h1:EdX8jtrTIj26jmjCOVNMVSIYAtgexqXKHOXW2Dx9JLg= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= +gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= +go.elara.ws/logger v0.0.0-20230421022458-e80700db2090 h1:RVC8XvWo6Yw4HUshqx4TSzuBDScDghafU6QFRJ4xPZg= +go.elara.ws/logger v0.0.0-20230421022458-e80700db2090/go.mod h1:qng49owViqsW5Aey93lwBXONw20oGbJIoLVscB16mPM= +go.elara.ws/translate v0.0.0-20230421025926-32ccfcd110e6 h1:4xCBxLPBn3Y2DuIcj8zQ1tQOFLrpu6tEIGUWn/Q6zPM= +go.elara.ws/translate v0.0.0-20230421025926-32ccfcd110e6/go.mod h1:NmfCFqwq7X/aqa/ZVkIysj17JyMEY4Bb5E921kMswNo= +go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 h1:Ep54XceQlKhcCHl9awG+wWP4kz4kIP3c3Lzw/Gc/zwY= +go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4/go.mod h1:/7PNW7nFnDR5W7UXZVc04gdVLR/wBNgkm33KgIz0OBk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU= +go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +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/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lure.sh/fakeroot v0.0.0-20231024000108-b130d64a68ee h1:kSXIuMid56Q29WEl7EQb5QUtmGqQqAu66EZ2G0OSUfU= +lure.sh/fakeroot v0.0.0-20231024000108-b130d64a68ee/go.mod h1:/v0u0AZ+wbzUWhV02KzciOf1KFNh7/7rbkz5Z0b5gDA= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= +modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= +modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA= +modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= +mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= +mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..b6d71e5 --- /dev/null +++ b/helper.go @@ -0,0 +1,86 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/cpu" + "lure.sh/lure/internal/shutils/helpers" + "lure.sh/lure/pkg/distro" + "lure.sh/lure/pkg/loggerctx" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" +) + +var helperCmd = &cli.Command{ + Name: "helper", + Usage: "Run a LURE helper command", + ArgsUsage: ``, + Subcommands: []*cli.Command{helperListCmd}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "dest-dir", + Aliases: []string{"d"}, + Usage: "The directory that the install commands will install to", + Value: "dest", + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + if c.Args().Len() < 1 { + cli.ShowSubcommandHelpAndExit(c, 1) + } + + helper, ok := helpers.Helpers[c.Args().First()] + if !ok { + log.Fatal("No such helper command").Str("name", c.Args().First()).Send() + } + + wd, err := os.Getwd() + if err != nil { + log.Fatal("Error getting working directory").Err(err).Send() + } + + info, err := distro.ParseOSRelease(ctx) + if err != nil { + log.Fatal("Error getting working directory").Err(err).Send() + } + + hc := interp.HandlerContext{ + Env: expand.ListEnviron( + "pkgdir="+c.String("dest-dir"), + "DISTRO_ID="+info.ID, + "DISTRO_ID_LIKE="+strings.Join(info.Like, " "), + "ARCH="+cpu.Arch(), + ), + Dir: wd, + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + } + + return helper(hc, c.Args().First(), c.Args().Slice()[1:]) + }, + CustomHelpTemplate: cli.CommandHelpTemplate, + BashComplete: func(ctx *cli.Context) { + for name := range helpers.Helpers { + fmt.Println(name) + } + }, +} + +var helperListCmd = &cli.Command{ + Name: "list", + Usage: "List all the available helper commands", + Aliases: []string{"ls"}, + Action: func(ctx *cli.Context) error { + for name := range helpers.Helpers { + fmt.Println(name) + } + return nil + }, +} diff --git a/info.go b/info.go new file mode 100644 index 0000000..6778313 --- /dev/null +++ b/info.go @@ -0,0 +1,106 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/cliutils" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/overrides" + "lure.sh/lure/pkg/distro" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/repos" + "gopkg.in/yaml.v3" +) + +var infoCmd = &cli.Command{ + Name: "info", + Usage: "Print information about a package", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Aliases: []string{"a"}, + Usage: "Show all information, not just for the current distro", + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + args := c.Args() + if args.Len() < 1 { + log.Fatalf("Command info expected at least 1 argument, got %d", args.Len()).Send() + } + + err := repos.Pull(ctx, config.Config(ctx).Repos) + if err != nil { + log.Fatal("Error pulling repositories").Err(err).Send() + } + + found, _, err := repos.FindPkgs(ctx, args.Slice()) + if err != nil { + log.Fatal("Error finding packages").Err(err).Send() + } + + if len(found) == 0 { + os.Exit(1) + } + + pkgs := cliutils.FlattenPkgs(ctx, found, "show", c.Bool("interactive")) + + var names []string + all := c.Bool("all") + + if !all { + info, err := distro.ParseOSRelease(ctx) + if err != nil { + log.Fatal("Error parsing os-release file").Err(err).Send() + } + names, err = overrides.Resolve( + info, + overrides.DefaultOpts. + WithLanguages([]string{config.SystemLang()}), + ) + if err != nil { + log.Fatal("Error resolving overrides").Err(err).Send() + } + } + + for _, pkg := range pkgs { + if !all { + err = yaml.NewEncoder(os.Stdout).Encode(overrides.ResolvePackage(&pkg, names)) + if err != nil { + log.Fatal("Error encoding script variables").Err(err).Send() + } + } else { + err = yaml.NewEncoder(os.Stdout).Encode(pkg) + if err != nil { + log.Fatal("Error encoding script variables").Err(err).Send() + } + } + + fmt.Println("---") + } + + return nil + }, +} diff --git a/install.go b/install.go new file mode 100644 index 0000000..b398531 --- /dev/null +++ b/install.go @@ -0,0 +1,122 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/cliutils" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/build" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/manager" + "lure.sh/lure/pkg/repos" +) + +var installCmd = &cli.Command{ + Name: "install", + Usage: "Install a new package", + Aliases: []string{"in"}, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "clean", + Aliases: []string{"c"}, + Usage: "Build package from scratch even if there's an already built package available", + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + args := c.Args() + if args.Len() < 1 { + log.Fatalf("Command install expected at least 1 argument, got %d", args.Len()).Send() + } + + mgr := manager.Detect() + if mgr == nil { + log.Fatal("Unable to detect a supported package manager on the system").Send() + } + + err := repos.Pull(ctx, config.Config(ctx).Repos) + if err != nil { + log.Fatal("Error pulling repositories").Err(err).Send() + } + + found, notFound, err := repos.FindPkgs(ctx, args.Slice()) + if err != nil { + log.Fatal("Error finding packages").Err(err).Send() + } + + pkgs := cliutils.FlattenPkgs(ctx, found, "install", c.Bool("interactive")) + build.InstallPkgs(ctx, pkgs, notFound, types.BuildOpts{ + Manager: mgr, + Clean: c.Bool("clean"), + Interactive: c.Bool("interactive"), + }) + return nil + }, + BashComplete: func(c *cli.Context) { + log := loggerctx.From(c.Context) + result, err := db.GetPkgs(c.Context, "true") + if err != nil { + log.Fatal("Error getting packages").Err(err).Send() + } + defer result.Close() + + for result.Next() { + var pkg db.Package + err = result.StructScan(&pkg) + if err != nil { + log.Fatal("Error iterating over packages").Err(err).Send() + } + + fmt.Println(pkg.Name) + } + }, +} + +var removeCmd = &cli.Command{ + Name: "remove", + Usage: "Remove an installed package", + Aliases: []string{"rm"}, + Action: func(c *cli.Context) error { + log := loggerctx.From(c.Context) + + args := c.Args() + if args.Len() < 1 { + log.Fatalf("Command remove expected at least 1 argument, got %d", args.Len()).Send() + } + + mgr := manager.Detect() + if mgr == nil { + log.Fatal("Unable to detect a supported package manager on the system").Send() + } + + err := mgr.Remove(nil, c.Args().Slice()...) + if err != nil { + log.Fatal("Error removing packages").Err(err).Send() + } + + return nil + }, +} diff --git a/internal/cliutils/prompt.go b/internal/cliutils/prompt.go new file mode 100644 index 0000000..7299936 --- /dev/null +++ b/internal/cliutils/prompt.go @@ -0,0 +1,172 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cliutils + +import ( + "context" + "os" + "strings" + + "github.com/AlecAivazis/survey/v2" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/pager" + "lure.sh/lure/internal/translations" + "lure.sh/lure/pkg/loggerctx" +) + +// YesNoPrompt asks the user a yes or no question, using def as the default answer +func YesNoPrompt(ctx context.Context, msg string, interactive, def bool) (bool, error) { + if interactive { + var answer bool + err := survey.AskOne( + &survey.Confirm{ + Message: translations.Translator(ctx).TranslateTo(msg, config.Language(ctx)), + Default: def, + }, + &answer, + ) + return answer, err + } else { + return def, nil + } +} + +// PromptViewScript asks the user if they'd like to see a script, +// shows it if they answer yes, then asks if they'd still like to +// continue, and exits if they answer no. +func PromptViewScript(ctx context.Context, script, name, style string, interactive bool) error { + log := loggerctx.From(ctx) + + if !interactive { + return nil + } + + scriptPrompt := translations.Translator(ctx).TranslateTo("Would you like to view the build script for", config.Language(ctx)) + " " + name + view, err := YesNoPrompt(ctx, scriptPrompt, interactive, false) + if err != nil { + return err + } + + if view { + err = ShowScript(script, name, style) + if err != nil { + return err + } + + cont, err := YesNoPrompt(ctx, "Would you still like to continue?", interactive, false) + if err != nil { + return err + } + + if !cont { + log.Fatal(translations.Translator(ctx).TranslateTo("User chose not to continue after reading script", config.Language(ctx))).Send() + } + } + + return nil +} + +// ShowScript uses the built-in pager to display a script at a +// given path, in the given syntax highlighting style. +func ShowScript(path, name, style string) error { + scriptFl, err := os.Open(path) + if err != nil { + return err + } + defer scriptFl.Close() + + str, err := pager.SyntaxHighlightBash(scriptFl, style) + if err != nil { + return err + } + + pgr := pager.New(name, str) + return pgr.Run() +} + +// FlattenPkgs attempts to flatten the a map of slices of packages into a single slice +// of packages by prompting the user if multiple packages match. +func FlattenPkgs(ctx context.Context, found map[string][]db.Package, verb string, interactive bool) []db.Package { + log := loggerctx.From(ctx) + var outPkgs []db.Package + for _, pkgs := range found { + if len(pkgs) > 1 && interactive { + choice, err := PkgPrompt(ctx, pkgs, verb, interactive) + if err != nil { + log.Fatal("Error prompting for choice of package").Send() + } + outPkgs = append(outPkgs, choice) + } else if len(pkgs) == 1 || !interactive { + outPkgs = append(outPkgs, pkgs[0]) + } + } + return outPkgs +} + +// PkgPrompt asks the user to choose between multiple packages. +func PkgPrompt(ctx context.Context, options []db.Package, verb string, interactive bool) (db.Package, error) { + if !interactive { + return options[0], nil + } + + names := make([]string, len(options)) + for i, option := range options { + names[i] = option.Repository + "/" + option.Name + " " + option.Version + } + + prompt := &survey.Select{ + Options: names, + Message: translations.Translator(ctx).TranslateTo("Choose which package to "+verb, config.Language(ctx)), + } + + var choice int + err := survey.AskOne(prompt, &choice) + if err != nil { + return db.Package{}, err + } + + return options[choice], nil +} + +// ChooseOptDepends asks the user to choose between multiple optional dependencies. +// The user may choose multiple items. +func ChooseOptDepends(ctx context.Context, options []string, verb string, interactive bool) ([]string, error) { + if !interactive { + return []string{}, nil + } + + prompt := &survey.MultiSelect{ + Options: options, + Message: translations.Translator(ctx).TranslateTo("Choose which optional package(s) to install", config.Language(ctx)), + } + + var choices []int + err := survey.AskOne(prompt, &choices) + if err != nil { + return nil, err + } + + out := make([]string, len(choices)) + for i, choiceIndex := range choices { + out[i], _, _ = strings.Cut(options[choiceIndex], ": ") + } + + return out, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..4f462f1 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,79 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "context" + "os" + "sync" + + "github.com/pelletier/go-toml/v2" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/loggerctx" +) + +var defaultConfig = &types.Config{ + RootCmd: "sudo", + PagerStyle: "native", + IgnorePkgUpdates: []string{}, + Repos: []types.Repo{ + { + Name: "default", + URL: "https://github.com/lure-sh/lure-repo.git", + }, + }, +} + +var ( + configMtx sync.Mutex + config *types.Config +) + +// Config returns a LURE configuration struct. +// The first time it's called, it'll load the config from a file. +// Subsequent calls will just return the same value. +func Config(ctx context.Context) *types.Config { + configMtx.Lock() + defer configMtx.Unlock() + log := loggerctx.From(ctx) + + if config == nil { + cfgFl, err := os.Open(GetPaths(ctx).ConfigPath) + if err != nil { + log.Warn("Error opening config file, using defaults").Err(err).Send() + return defaultConfig + } + defer cfgFl.Close() + + // Copy the default configuration into config + defCopy := *defaultConfig + config = &defCopy + config.Repos = nil + + err = toml.NewDecoder(cfgFl).Decode(config) + if err != nil { + log.Warn("Error decoding config file, using defaults").Err(err).Send() + // Set config back to nil so that we try again next time + config = nil + return defaultConfig + } + } + + return config +} diff --git a/internal/config/lang.go b/internal/config/lang.go new file mode 100644 index 0000000..d89ffa9 --- /dev/null +++ b/internal/config/lang.go @@ -0,0 +1,67 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "context" + "os" + "strings" + "sync" + + "lure.sh/lure/pkg/loggerctx" + "golang.org/x/text/language" +) + +var ( + langMtx sync.Mutex + lang language.Tag + langSet bool +) + +// Language returns the system language. +// The first time it's called, it'll detect the langauge based on +// the $LANG environment variable. +// Subsequent calls will just return the same value. +func Language(ctx context.Context) language.Tag { + langMtx.Lock() + defer langMtx.Unlock() + log := loggerctx.From(ctx) + if !langSet { + syslang := SystemLang() + tag, err := language.Parse(syslang) + if err != nil { + log.Fatal("Error parsing system language").Err(err).Send() + } + base, _ := tag.Base() + lang = language.Make(base.String()) + langSet = true + } + return lang +} + +// SystemLang returns the system language based on +// the $LANG environment variable. +func SystemLang() string { + lang := os.Getenv("LANG") + lang, _, _ = strings.Cut(lang, ".") + if lang == "" || lang == "C" { + lang = "en" + } + return lang +} diff --git a/internal/config/paths.go b/internal/config/paths.go new file mode 100644 index 0000000..54f944a --- /dev/null +++ b/internal/config/paths.go @@ -0,0 +1,108 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package config + +import ( + "context" + "os" + "path/filepath" + "sync" + + "github.com/pelletier/go-toml/v2" + "lure.sh/lure/pkg/loggerctx" +) + +// Paths contains various paths used by LURE +type Paths struct { + ConfigDir string + ConfigPath string + CacheDir string + RepoDir string + PkgsDir string + DBPath string +} + +var ( + pathsMtx sync.Mutex + paths *Paths +) + +// GetPaths returns a Paths struct. +// The first time it's called, it'll generate the struct +// using information from the system. +// Subsequent calls will return the same value. +func GetPaths(ctx context.Context) *Paths { + pathsMtx.Lock() + defer pathsMtx.Unlock() + + log := loggerctx.From(ctx) + if paths == nil { + paths = &Paths{} + + cfgDir, err := os.UserConfigDir() + if err != nil { + log.Fatal("Unable to detect user config directory").Err(err).Send() + } + + paths.ConfigDir = filepath.Join(cfgDir, "lure") + + err = os.MkdirAll(paths.ConfigDir, 0o755) + if err != nil { + log.Fatal("Unable to create LURE config directory").Err(err).Send() + } + + paths.ConfigPath = filepath.Join(paths.ConfigDir, "lure.toml") + + if _, err := os.Stat(paths.ConfigPath); err != nil { + cfgFl, err := os.Create(paths.ConfigPath) + if err != nil { + log.Fatal("Unable to create LURE config file").Err(err).Send() + } + + err = toml.NewEncoder(cfgFl).Encode(&defaultConfig) + if err != nil { + log.Fatal("Error encoding default configuration").Err(err).Send() + } + + cfgFl.Close() + } + + cacheDir, err := os.UserCacheDir() + if err != nil { + log.Fatal("Unable to detect cache directory").Err(err).Send() + } + + paths.CacheDir = filepath.Join(cacheDir, "lure") + paths.RepoDir = filepath.Join(paths.CacheDir, "repo") + paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs") + + err = os.MkdirAll(paths.RepoDir, 0o755) + if err != nil { + log.Fatal("Unable to create repo cache directory").Err(err).Send() + } + + err = os.MkdirAll(paths.PkgsDir, 0o755) + if err != nil { + log.Fatal("Unable to create package cache directory").Err(err).Send() + } + + paths.DBPath = filepath.Join(paths.CacheDir, "db") + } + return paths +} diff --git a/internal/config/version.go b/internal/config/version.go new file mode 100644 index 0000000..33cf91a --- /dev/null +++ b/internal/config/version.go @@ -0,0 +1,5 @@ +package config + +// Version contains the version of LURE. If the version +// isn't known, it'll be set to "unknown" +var Version = "unknown" diff --git a/internal/cpu/cpu.go b/internal/cpu/cpu.go new file mode 100644 index 0000000..18cf561 --- /dev/null +++ b/internal/cpu/cpu.go @@ -0,0 +1,117 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package cpu + +import ( + "os" + "runtime" + "strconv" + "strings" + + "golang.org/x/exp/slices" + "golang.org/x/sys/cpu" +) + +// armVariant checks which variant of ARM lure is running +// on, by using the same detection method as Go itself +func armVariant() string { + armEnv := os.Getenv("LURE_ARM_VARIANT") + // ensure value has "arm" prefix, such as arm5 or arm6 + if strings.HasPrefix(armEnv, "arm") { + return armEnv + } + + if cpu.ARM.HasVFPv3 { + return "arm7" + } else if cpu.ARM.HasVFP { + return "arm6" + } else { + return "arm5" + } +} + +// Arch returns the canonical CPU architecture of the system +func Arch() string { + arch := os.Getenv("LURE_ARCH") + if arch == "" { + arch = runtime.GOARCH + } + if arch == "arm" { + arch = armVariant() + } + return arch +} + +func IsCompatibleWith(target string, list []string) bool { + if target == "all" || slices.Contains(list, "all") { + return true + } + + for _, arch := range list { + if strings.HasPrefix(target, "arm") && strings.HasPrefix(arch, "arm") { + targetVer, err := getARMVersion(target) + if err != nil { + return false + } + + archVer, err := getARMVersion(arch) + if err != nil { + return false + } + + if targetVer >= archVer { + return true + } + } + + if target == arch { + return true + } + } + + return false +} + +func CompatibleArches(arch string) ([]string, error) { + if strings.HasPrefix(arch, "arm") { + ver, err := getARMVersion(arch) + if err != nil { + return nil, err + } + + if ver > 5 { + var out []string + for i := ver; i >= 5; i-- { + out = append(out, "arm"+strconv.Itoa(i)) + } + return out, nil + } + } + + return []string{arch}, nil +} + +func getARMVersion(arch string) (int, error) { + // Extract the version number from ARM architecture + version := strings.TrimPrefix(arch, "arm") + if version == "" { + return 5, nil // Default to arm5 if version is not specified + } + return strconv.Atoi(version) +} diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 0000000..df11bd7 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,346 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package db + +import ( + "context" + "database/sql" + "database/sql/driver" + "encoding/json" + "errors" + "fmt" + "sync" + + "github.com/jmoiron/sqlx" + "lure.sh/lure/internal/config" + "lure.sh/lure/pkg/loggerctx" + "golang.org/x/exp/slices" + "modernc.org/sqlite" +) + +// CurrentVersion is the current version of the database. +// The database is reset if its version doesn't match this. +const CurrentVersion = 2 + +func init() { + sqlite.MustRegisterScalarFunction("json_array_contains", 2, jsonArrayContains) +} + +// Package is a LURE package's database representation +type Package struct { + Name string `sh:"name,required" db:"name"` + Version string `sh:"version,required" db:"version"` + Release int `sh:"release,required" db:"release"` + Epoch uint `sh:"epoch" db:"epoch"` + Description JSON[map[string]string] `db:"description"` + Homepage JSON[map[string]string] `db:"homepage"` + Maintainer JSON[map[string]string] `db:"maintainer"` + Architectures JSON[[]string] `sh:"architectures" db:"architectures"` + Licenses JSON[[]string] `sh:"license" db:"licenses"` + Provides JSON[[]string] `sh:"provides" db:"provides"` + Conflicts JSON[[]string] `sh:"conflicts" db:"conflicts"` + Replaces JSON[[]string] `sh:"replaces" db:"replaces"` + Depends JSON[map[string][]string] `db:"depends"` + BuildDepends JSON[map[string][]string] `db:"builddepends"` + OptDepends JSON[map[string][]string] `db:"optdepends"` + Repository string `db:"repository"` +} + +type version struct { + Version int `db:"version"` +} + +var ( + mu sync.Mutex + conn *sqlx.DB + closed = true +) + +// DB returns the LURE database. +// The first time it's called, it opens the SQLite database file. +// Subsequent calls return the same connection. +func DB(ctx context.Context) *sqlx.DB { + log := loggerctx.From(ctx) + if conn != nil && !closed { + return getConn() + } + _, err := open(ctx, config.GetPaths(ctx).DBPath) + if err != nil { + log.Fatal("Error opening database").Err(err).Send() + } + return getConn() +} + +func getConn() *sqlx.DB { + mu.Lock() + defer mu.Unlock() + return conn +} + +func open(ctx context.Context, dsn string) (*sqlx.DB, error) { + db, err := sqlx.Open("sqlite", dsn) + if err != nil { + return nil, err + } + + mu.Lock() + conn = db + closed = false + mu.Unlock() + + err = initDB(ctx, dsn) + if err != nil { + return nil, err + } + + return db, nil +} + +// Close closes the database +func Close() error { + closed = true + if conn != nil { + return conn.Close() + } else { + return nil + } +} + +// initDB initializes the database +func initDB(ctx context.Context, dsn string) error { + log := loggerctx.From(ctx) + conn = conn.Unsafe() + _, err := conn.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS pkgs ( + name TEXT NOT NULL, + repository TEXT NOT NULL, + version TEXT NOT NULL, + release INT NOT NULL, + epoch INT, + description TEXT CHECK(description = 'null' OR (JSON_VALID(description) AND JSON_TYPE(description) = 'object')), + homepage TEXT CHECK(homepage = 'null' OR (JSON_VALID(homepage) AND JSON_TYPE(homepage) = 'object')), + maintainer TEXT CHECK(maintainer = 'null' OR (JSON_VALID(maintainer) AND JSON_TYPE(maintainer) = 'object')), + architectures TEXT CHECK(architectures = 'null' OR (JSON_VALID(architectures) AND JSON_TYPE(architectures) = 'array')), + licenses TEXT CHECK(licenses = 'null' OR (JSON_VALID(licenses) AND JSON_TYPE(licenses) = 'array')), + provides TEXT CHECK(provides = 'null' OR (JSON_VALID(provides) AND JSON_TYPE(provides) = 'array')), + conflicts TEXT CHECK(conflicts = 'null' OR (JSON_VALID(conflicts) AND JSON_TYPE(conflicts) = 'array')), + replaces TEXT CHECK(replaces = 'null' OR (JSON_VALID(replaces) AND JSON_TYPE(replaces) = 'array')), + depends TEXT CHECK(depends = 'null' OR (JSON_VALID(depends) AND JSON_TYPE(depends) = 'object')), + builddepends TEXT CHECK(builddepends = 'null' OR (JSON_VALID(builddepends) AND JSON_TYPE(builddepends) = 'object')), + optdepends TEXT CHECK(optdepends = 'null' OR (JSON_VALID(optdepends) AND JSON_TYPE(optdepends) = 'object')), + UNIQUE(name, repository) + ); + + CREATE TABLE IF NOT EXISTS lure_db_version ( + version INT NOT NULL + ); + `) + if err != nil { + return err + } + + ver, ok := GetVersion(ctx) + if ok && ver != CurrentVersion { + log.Warn("Database version mismatch; resetting").Int("version", ver).Int("expected", CurrentVersion).Send() + reset(ctx) + return initDB(ctx, dsn) + } else if !ok { + log.Warn("Database version does not exist. Run lure fix if something isn't working.").Send() + return addVersion(ctx, CurrentVersion) + } + + return nil +} + +// reset drops all the database tables +func reset(ctx context.Context) error { + _, err := DB(ctx).ExecContext(ctx, "DROP TABLE IF EXISTS pkgs;") + if err != nil { + return err + } + _, err = DB(ctx).ExecContext(ctx, "DROP TABLE IF EXISTS lure_db_version;") + return err +} + +// IsEmpty returns true if the database has no packages in it, otherwise it returns false. +func IsEmpty(ctx context.Context) bool { + var count int + err := DB(ctx).GetContext(ctx, &count, "SELECT count(1) FROM pkgs;") + if err != nil { + return true + } + return count == 0 +} + +// GetVersion returns the database version and a boolean indicating +// whether the database contained a version number +func GetVersion(ctx context.Context) (int, bool) { + var ver version + err := DB(ctx).GetContext(ctx, &ver, "SELECT * FROM lure_db_version LIMIT 1;") + if err != nil { + return 0, false + } + return ver.Version, true +} + +func addVersion(ctx context.Context, ver int) error { + _, err := DB(ctx).ExecContext(ctx, `INSERT INTO lure_db_version(version) VALUES (?);`, ver) + return err +} + +// InsertPackage adds a package to the database +func InsertPackage(ctx context.Context, pkg Package) error { + _, err := DB(ctx).NamedExecContext(ctx, ` + INSERT OR REPLACE INTO pkgs ( + name, + repository, + version, + release, + epoch, + description, + homepage, + maintainer, + architectures, + licenses, + provides, + conflicts, + replaces, + depends, + builddepends, + optdepends + ) VALUES ( + :name, + :repository, + :version, + :release, + :epoch, + :description, + :homepage, + :maintainer, + :architectures, + :licenses, + :provides, + :conflicts, + :replaces, + :depends, + :builddepends, + :optdepends + ); + `, pkg) + return err +} + +// GetPkgs returns a result containing packages that match the where conditions +func GetPkgs(ctx context.Context, where string, args ...any) (*sqlx.Rows, error) { + stream, err := DB(ctx).QueryxContext(ctx, "SELECT * FROM pkgs WHERE "+where, args...) + if err != nil { + return nil, err + } + return stream, nil +} + +// GetPkg returns a single package that matches the where conditions +func GetPkg(ctx context.Context, where string, args ...any) (*Package, error) { + out := &Package{} + err := DB(ctx).GetContext(ctx, out, "SELECT * FROM pkgs WHERE "+where+" LIMIT 1", args...) + return out, err +} + +// DeletePkgs deletes all packages matching the where conditions +func DeletePkgs(ctx context.Context, where string, args ...any) error { + _, err := DB(ctx).ExecContext(ctx, "DELETE FROM pkgs WHERE "+where, args...) + return err +} + +// jsonArrayContains is an SQLite function that checks if a JSON array +// in the database contains a given value +func jsonArrayContains(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) { + value, ok := args[0].(string) + if !ok { + return nil, errors.New("both arguments to json_array_contains must be strings") + } + + item, ok := args[1].(string) + if !ok { + return nil, errors.New("both arguments to json_array_contains must be strings") + } + + var array []string + err := json.Unmarshal([]byte(value), &array) + if err != nil { + return nil, err + } + + return slices.Contains(array, item), nil +} + +// JSON represents a JSON value in the database +type JSON[T any] struct { + Val T +} + +// NewJSON creates a new database JSON value +func NewJSON[T any](v T) JSON[T] { + return JSON[T]{Val: v} +} + +func (s *JSON[T]) Scan(val any) error { + if val == nil { + return nil + } + + switch val := val.(type) { + case string: + err := json.Unmarshal([]byte(val), &s.Val) + if err != nil { + return err + } + case sql.NullString: + if val.Valid { + err := json.Unmarshal([]byte(val.String), &s.Val) + if err != nil { + return err + } + } + default: + return errors.New("sqlite json types must be strings") + } + + return nil +} + +func (s JSON[T]) Value() (driver.Value, error) { + data, err := json.Marshal(s.Val) + if err != nil { + return nil, err + } + return string(data), nil +} + +func (s JSON[T]) MarshalYAML() (any, error) { + return s.Val, nil +} + +func (s JSON[T]) String() string { + return fmt.Sprint(s.Val) +} + +func (s JSON[T]) GoString() string { + return fmt.Sprintf("%#v", s.Val) +} diff --git a/internal/db/db_test.go b/internal/db/db_test.go new file mode 100644 index 0000000..9774c33 --- /dev/null +++ b/internal/db/db_test.go @@ -0,0 +1,250 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package db_test + +import ( + "reflect" + "strings" + "testing" + + "github.com/jmoiron/sqlx" + "lure.sh/lure/internal/db" +) + +var testPkg = db.Package{ + Name: "test", + Version: "0.0.1", + Release: 1, + Epoch: 2, + Description: db.NewJSON(map[string]string{ + "en": "Test package", + "ru": "Проверочный пакет", + }), + Homepage: db.NewJSON(map[string]string{ + "en": "https://lure.sh/", + }), + Maintainer: db.NewJSON(map[string]string{ + "en": "Elara Musayelyan ", + "ru": "Элара Мусаелян ", + }), + Architectures: db.NewJSON([]string{"arm64", "amd64"}), + Licenses: db.NewJSON([]string{"GPL-3.0-or-later"}), + Provides: db.NewJSON([]string{"test"}), + Conflicts: db.NewJSON([]string{"test"}), + Replaces: db.NewJSON([]string{"test-old"}), + Depends: db.NewJSON(map[string][]string{ + "": {"sudo"}, + }), + BuildDepends: db.NewJSON(map[string][]string{ + "": {"golang"}, + "arch": {"go"}, + }), + Repository: "default", +} + +func TestInit(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + _, err = db.DB().Exec("SELECT * FROM pkgs") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + ver, ok := db.GetVersion() + if !ok { + t.Errorf("Expected version to be present") + } else if ver != db.CurrentVersion { + t.Errorf("Expected version %d, got %d", db.CurrentVersion, ver) + } +} + +func TestInsertPackage(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + err = db.InsertPackage(testPkg) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + dbPkg := db.Package{} + err = sqlx.Get(db.DB(), &dbPkg, "SELECT * FROM pkgs WHERE name = 'test' AND repository = 'default'") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if !reflect.DeepEqual(testPkg, dbPkg) { + t.Errorf("Expected test package to be the same as database package") + } +} + +func TestGetPkgs(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + x1 := testPkg + x1.Name = "x1" + x2 := testPkg + x2.Name = "x2" + + err = db.InsertPackage(x1) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + err = db.InsertPackage(x2) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + result, err := db.GetPkgs("name LIKE 'x%'") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + for result.Next() { + var dbPkg db.Package + err = result.StructScan(&dbPkg) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + if !strings.HasPrefix(dbPkg.Name, "x") { + t.Errorf("Expected package name to start with 'x', got %s", dbPkg.Name) + } + } +} + +func TestGetPkg(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + x1 := testPkg + x1.Name = "x1" + x2 := testPkg + x2.Name = "x2" + + err = db.InsertPackage(x1) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + err = db.InsertPackage(x2) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + pkg, err := db.GetPkg("name LIKE 'x%' ORDER BY name") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if pkg.Name != "x1" { + t.Errorf("Expected x1 package, got %s", pkg.Name) + } + + if !reflect.DeepEqual(*pkg, x1) { + t.Errorf("Expected x1 to be %v, got %v", x1, *pkg) + } +} + +func TestDeletePkgs(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + x1 := testPkg + x1.Name = "x1" + x2 := testPkg + x2.Name = "x2" + + err = db.InsertPackage(x1) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + err = db.InsertPackage(x2) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + err = db.DeletePkgs("name = 'x1'") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + var dbPkg db.Package + err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE name LIKE 'x%' ORDER BY name LIMIT 1;") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + if dbPkg.Name != "x2" { + t.Errorf("Expected x2 package, got %s", dbPkg.Name) + } +} + +func TestJsonArrayContains(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + x1 := testPkg + x1.Name = "x1" + x2 := testPkg + x2.Name = "x2" + x2.Provides.Val = append(x2.Provides.Val, "x") + + err = db.InsertPackage(x1) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + err = db.InsertPackage(x2) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + var dbPkg db.Package + err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE json_array_contains(provides, 'x');") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if dbPkg.Name != "x2" { + t.Errorf("Expected x2 package, got %s", dbPkg.Name) + } +} diff --git a/internal/dl/dl.go b/internal/dl/dl.go new file mode 100644 index 0000000..6921ecd --- /dev/null +++ b/internal/dl/dl.go @@ -0,0 +1,379 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// Package dl contains abstractions for downloadingfiles and directories +// from various sources. +package dl + +import ( + "context" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "errors" + "fmt" + "hash" + "io" + "os" + "path/filepath" + "strings" + + "github.com/PuerkitoBio/purell" + "github.com/vmihailenco/msgpack/v5" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/blake2s" + "golang.org/x/exp/slices" + "lure.sh/lure/internal/dlcache" + "lure.sh/lure/pkg/loggerctx" +) + +const manifestFileName = ".lure_cache_manifest" + +// ErrChecksumMismatch occurs when the checksum of a downloaded file +// does not match the expected checksum provided in the Options struct. +var ( + ErrChecksumMismatch = errors.New("dl: checksums did not match") + ErrNoSuchHashAlgo = errors.New("dl: invalid hashing algorithm") +) + +// Downloaders contains all the downloaders in the order in which +// they should be checked +var Downloaders = []Downloader{ + GitDownloader{}, + TorrentDownloader{}, + FileDownloader{}, +} + +// Type represents the type of download (file or directory) +type Type uint8 + +const ( + TypeFile Type = iota + TypeDir +) + +func (t Type) String() string { + switch t { + case TypeFile: + return "file" + case TypeDir: + return "dir" + } + return "" +} + +// Options contains the options for downloading +// files and directories +type Options struct { + Hash []byte + HashAlgorithm string + Name string + URL string + Destination string + CacheDisabled bool + PostprocDisabled bool + Progress io.Writer + LocalDir string +} + +func (opts Options) NewHash() (hash.Hash, error) { + switch opts.HashAlgorithm { + case "", "sha256": + return sha256.New(), nil + case "sha224": + return sha256.New224(), nil + case "sha512": + return sha512.New(), nil + case "sha384": + return sha512.New384(), nil + case "sha1": + return sha1.New(), nil + case "md5": + return md5.New(), nil + case "blake2s-128": + return blake2s.New256(nil) + case "blake2s-256": + return blake2s.New256(nil) + case "blake2b-256": + return blake2b.New(32, nil) + case "blake2b-512": + return blake2b.New(64, nil) + default: + return nil, fmt.Errorf("%w: %s", ErrNoSuchHashAlgo, opts.HashAlgorithm) + } +} + +// Manifest holds information about the type and name +// of a downloaded file or directory. It is stored inside +// each cache directory for later use. +type Manifest struct { + Type Type + Name string +} + +type Downloader interface { + // Name returns the name of the downloader + Name() string + // MatchURL checks if the given URL matches + // the downloader. + MatchURL(string) bool + // Download downloads the object at the URL + // provided in the options, to the destination + // given in the options. It returns a type, + // a name for the downloaded object (this may be empty), + // and an error. + Download(Options) (Type, string, error) +} + +// UpdatingDownloader extends the Downloader interface +// with an Update method for protocols such as git, which +// allow for incremental updates without changing the URL. +type UpdatingDownloader interface { + Downloader + // Update checks for and performs any + // available updates for the object + // described in the options. It returns + // true if an update was performed, or + // false if no update was required. + Update(Options) (bool, error) +} + +// Download downloads a file or directory using the specified options. +// It first gets the appropriate downloader for the URL, then checks +// if caching is enabled. If caching is enabled, it attempts to get +// the cache directory for the URL and update it if necessary. +// If the source is found in the cache, it links it to the destination +// using hard links. If the source is not found in the cache, +// it downloads the source to a new cache directory and links it +// to the destination. +func Download(ctx context.Context, opts Options) (err error) { + log := loggerctx.From(ctx) + normalized, err := normalizeURL(opts.URL) + if err != nil { + return err + } + opts.URL = normalized + + d := getDownloader(opts.URL) + + if opts.CacheDisabled { + _, _, err = d.Download(opts) + return err + } + + var t Type + cacheDir, ok := dlcache.Get(ctx, opts.URL) + if ok { + var updated bool + if d, ok := d.(UpdatingDownloader); ok { + log.Info("Source can be updated, updating if required").Str("source", opts.Name).Str("downloader", d.Name()).Send() + + updated, err = d.Update(Options{ + Hash: opts.Hash, + HashAlgorithm: opts.HashAlgorithm, + Name: opts.Name, + URL: opts.URL, + Destination: cacheDir, + Progress: opts.Progress, + LocalDir: opts.LocalDir, + }) + if err != nil { + return err + } + } + + m, err := getManifest(cacheDir) + if err == nil { + t = m.Type + + dest := filepath.Join(opts.Destination, m.Name) + ok, err := handleCache(cacheDir, dest, m.Name, t) + if err != nil { + return err + } + + if ok && !updated { + log.Info("Source found in cache and linked to destination").Str("source", opts.Name).Stringer("type", t).Send() + return nil + } else if ok { + log.Info("Source updated and linked to destination").Str("source", opts.Name).Stringer("type", t).Send() + return nil + } + } else { + // If we cannot read the manifest, + // this cache entry is invalid and + // the source must be re-downloaded. + err = os.RemoveAll(cacheDir) + if err != nil { + return err + } + } + } + + log.Info("Downloading source").Str("source", opts.Name).Str("downloader", d.Name()).Send() + + cacheDir, err = dlcache.New(ctx, opts.URL) + if err != nil { + return err + } + + t, name, err := d.Download(Options{ + Hash: opts.Hash, + HashAlgorithm: opts.HashAlgorithm, + Name: opts.Name, + URL: opts.URL, + Destination: cacheDir, + Progress: opts.Progress, + LocalDir: opts.LocalDir, + }) + if err != nil { + return err + } + + err = writeManifest(cacheDir, Manifest{t, name}) + if err != nil { + return err + } + + dest := filepath.Join(opts.Destination, name) + _, err = handleCache(cacheDir, dest, name, t) + return err +} + +// writeManifest writes the manifest to the specified cache directory. +func writeManifest(cacheDir string, m Manifest) error { + fl, err := os.Create(filepath.Join(cacheDir, manifestFileName)) + if err != nil { + return err + } + defer fl.Close() + return msgpack.NewEncoder(fl).Encode(m) +} + +// getManifest reads the manifest from the specified cache directory. +func getManifest(cacheDir string) (m Manifest, err error) { + fl, err := os.Open(filepath.Join(cacheDir, manifestFileName)) + if err != nil { + return Manifest{}, err + } + defer fl.Close() + + err = msgpack.NewDecoder(fl).Decode(&m) + return +} + +// handleCache links the cache directory or a file within it to the destination +func handleCache(cacheDir, dest, name string, t Type) (bool, error) { + switch t { + case TypeFile: + cd, err := os.Open(cacheDir) + if err != nil { + return false, err + } + + names, err := cd.Readdirnames(0) + if err == io.EOF { + break + } else if err != nil { + return false, err + } + + cd.Close() + + if slices.Contains(names, name) { + err = os.Link(filepath.Join(cacheDir, name), dest) + if err != nil { + return false, err + } + return true, nil + } + case TypeDir: + err := linkDir(cacheDir, dest) + if err != nil { + return false, err + } + return true, nil + } + return false, nil +} + +// linkDir recursively walks through a directory, creating +// hard links for each file from the src directory to the +// dest directory. If it encounters a directory, it will +// create a directory with the same name and permissions +// in the dest directory, because hard links cannot be +// created for directories. +func linkDir(src, dest string) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.Name() == manifestFileName { + return nil + } + + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + + newPath := filepath.Join(dest, rel) + if info.IsDir() { + return os.MkdirAll(newPath, info.Mode()) + } + + return os.Link(path, newPath) + }) +} + +func getDownloader(u string) Downloader { + for _, d := range Downloaders { + if d.MatchURL(u) { + return d + } + } + return nil +} + +// normalizeURL normalizes a URL string, so that insignificant +// differences don't change the hash. +func normalizeURL(u string) (string, error) { + const normalizationFlags = purell.FlagRemoveTrailingSlash | + purell.FlagRemoveDefaultPort | + purell.FlagLowercaseHost | + purell.FlagLowercaseScheme | + purell.FlagRemoveDuplicateSlashes | + purell.FlagRemoveFragment | + purell.FlagRemoveUnnecessaryHostDots | + purell.FlagSortQuery | + purell.FlagDecodeHexHost | + purell.FlagDecodeOctalHost | + purell.FlagDecodeUnnecessaryEscapes | + purell.FlagRemoveEmptyPortSeparator + + u, err := purell.NormalizeURLString(u, normalizationFlags) + if err != nil { + return "", err + } + + // Fix magnet URLs after normalization + u = strings.Replace(u, "magnet://", "magnet:", 1) + return u, nil +} diff --git a/internal/dl/file.go b/internal/dl/file.go new file mode 100644 index 0000000..52723ce --- /dev/null +++ b/internal/dl/file.go @@ -0,0 +1,264 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dl + +import ( + "bytes" + "context" + "io" + "mime" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/mholt/archiver/v4" + "github.com/schollz/progressbar/v3" + "lure.sh/lure/internal/shutils/handlers" +) + +// FileDownloader downloads files using HTTP +type FileDownloader struct{} + +// Name always returns "file" +func (FileDownloader) Name() string { + return "file" +} + +// MatchURL always returns true, as FileDownloader +// is used as a fallback if nothing else matches +func (FileDownloader) MatchURL(string) bool { + return true +} + +// Download downloads a file using HTTP. If the file is +// compressed using a supported format, it will be extracted +func (FileDownloader) Download(opts Options) (Type, string, error) { + u, err := url.Parse(opts.URL) + if err != nil { + return 0, "", err + } + + query := u.Query() + + name := query.Get("~name") + query.Del("~name") + + archive := query.Get("~archive") + query.Del("~archive") + + u.RawQuery = query.Encode() + + var r io.ReadCloser + var size int64 + if u.Scheme == "local" { + localFl, err := os.Open(filepath.Join(opts.LocalDir, u.Path)) + if err != nil { + return 0, "", err + } + fi, err := localFl.Stat() + if err != nil { + return 0, "", err + } + size = fi.Size() + if name == "" { + name = fi.Name() + } + r = localFl + } else { + res, err := http.Get(u.String()) + if err != nil { + return 0, "", err + } + size = res.ContentLength + if name == "" { + name = getFilename(res) + } + r = res.Body + } + defer r.Close() + + opts.PostprocDisabled = archive == "false" + + path := filepath.Join(opts.Destination, name) + fl, err := os.Create(path) + if err != nil { + return 0, "", err + } + defer fl.Close() + + var bar io.WriteCloser + if opts.Progress != nil { + bar = progressbar.NewOptions64( + size, + progressbar.OptionSetDescription(name), + progressbar.OptionSetWriter(opts.Progress), + progressbar.OptionShowBytes(true), + progressbar.OptionSetWidth(10), + progressbar.OptionThrottle(65*time.Millisecond), + progressbar.OptionShowCount(), + progressbar.OptionOnCompletion(func() { + _, _ = io.WriteString(opts.Progress, "\n") + }), + progressbar.OptionSpinnerType(14), + progressbar.OptionFullWidth(), + progressbar.OptionSetRenderBlankState(true), + ) + defer bar.Close() + } else { + bar = handlers.NopRWC{} + } + + h, err := opts.NewHash() + if err != nil { + return 0, "", err + } + + var w io.Writer + if opts.Hash != nil { + w = io.MultiWriter(fl, h, bar) + } else { + w = io.MultiWriter(fl, bar) + } + + _, err = io.Copy(w, r) + if err != nil { + return 0, "", err + } + r.Close() + + if opts.Hash != nil { + sum := h.Sum(nil) + if !bytes.Equal(sum, opts.Hash) { + return 0, "", ErrChecksumMismatch + } + } + + if opts.PostprocDisabled { + return TypeFile, name, nil + } + + _, err = fl.Seek(0, io.SeekStart) + if err != nil { + return 0, "", err + } + + format, ar, err := archiver.Identify(name, fl) + if err == archiver.ErrNoMatch { + return TypeFile, name, nil + } else if err != nil { + return 0, "", err + } + + err = extractFile(ar, format, name, opts) + if err != nil { + return 0, "", err + } + + err = os.Remove(path) + return TypeDir, "", err +} + +// extractFile extracts an archive or decompresses a file +func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) { + fname := format.Name() + + switch format := format.(type) { + case archiver.Extractor: + err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error { + fr, err := f.Open() + if err != nil { + return err + } + defer fr.Close() + fi, err := f.Stat() + if err != nil { + return err + } + fm := fi.Mode() + + path := filepath.Join(opts.Destination, f.NameInArchive) + + err = os.MkdirAll(filepath.Dir(path), 0o755) + if err != nil { + return err + } + + if f.IsDir() { + err = os.Mkdir(path, 0o755) + if err != nil { + return err + } + } else { + outFl, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fm.Perm()) + if err != nil { + return err + } + defer outFl.Close() + + _, err = io.Copy(outFl, fr) + return err + } + return nil + }) + if err != nil { + return err + } + case archiver.Decompressor: + rc, err := format.OpenReader(r) + if err != nil { + return err + } + defer rc.Close() + + path := filepath.Join(opts.Destination, name) + path = strings.TrimSuffix(path, fname) + + outFl, err := os.Create(path) + if err != nil { + return err + } + + _, err = io.Copy(outFl, rc) + if err != nil { + return err + } + } + + return nil +} + +// getFilename attempts to parse the Content-Disposition +// HTTP response header and extract a filename. If the +// header does not exist, it will use the last element +// of the path. +func getFilename(res *http.Response) (name string) { + _, params, err := mime.ParseMediaType(res.Header.Get("Content-Disposition")) + if err != nil { + return path.Base(res.Request.URL.Path) + } + if filename, ok := params["filename"]; ok { + return filename + } else { + return path.Base(res.Request.URL.Path) + } +} diff --git a/internal/dl/git.go b/internal/dl/git.go new file mode 100644 index 0000000..f07109b --- /dev/null +++ b/internal/dl/git.go @@ -0,0 +1,198 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dl + +import ( + "errors" + "net/url" + "path" + "strconv" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" +) + +// GitDownloader downloads Git repositories +type GitDownloader struct{} + +// Name always returns "git" +func (GitDownloader) Name() string { + return "git" +} + +// MatchURL matches any URLs that start with "git+" +func (GitDownloader) MatchURL(u string) bool { + return strings.HasPrefix(u, "git+") +} + +// Download uses git to clone the repository from the specified URL. +// It allows specifying the revision, depth and recursion options +// via query string +func (GitDownloader) Download(opts Options) (Type, string, error) { + u, err := url.Parse(opts.URL) + if err != nil { + return 0, "", err + } + u.Scheme = strings.TrimPrefix(u.Scheme, "git+") + + query := u.Query() + + rev := query.Get("~rev") + query.Del("~rev") + + name := query.Get("~name") + query.Del("~name") + + depthStr := query.Get("~depth") + query.Del("~depth") + + recursive := query.Get("~recursive") + query.Del("~recursive") + + u.RawQuery = query.Encode() + + depth := 0 + if depthStr != "" { + depth, err = strconv.Atoi(depthStr) + if err != nil { + return 0, "", err + } + } + + co := &git.CloneOptions{ + URL: u.String(), + Depth: depth, + Progress: opts.Progress, + RecurseSubmodules: git.NoRecurseSubmodules, + } + + if recursive == "true" { + co.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth + } + + r, err := git.PlainClone(opts.Destination, false, co) + if err != nil { + return 0, "", err + } + + err = r.Fetch(&git.FetchOptions{ + RefSpecs: []config.RefSpec{"+refs/*:refs/*"}, + }) + if err != git.NoErrAlreadyUpToDate && err != nil { + return 0, "", err + } + + if rev != "" { + h, err := r.ResolveRevision(plumbing.Revision(rev)) + if err != nil { + return 0, "", err + } + + w, err := r.Worktree() + if err != nil { + return 0, "", err + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: *h, + }) + if err != nil { + return 0, "", err + } + } + + if name == "" { + name = strings.TrimSuffix(path.Base(u.Path), ".git") + } + + return TypeDir, name, nil +} + +// Update uses git to pull the repository and update it +// to the latest revision. It allows specifying the depth +// and recursion options via query string. It returns +// true if update was successful and false if the +// repository is already up-to-date +func (GitDownloader) Update(opts Options) (bool, error) { + u, err := url.Parse(opts.URL) + if err != nil { + return false, err + } + u.Scheme = strings.TrimPrefix(u.Scheme, "git+") + + query := u.Query() + query.Del("~rev") + + depthStr := query.Get("~depth") + query.Del("~depth") + + recursive := query.Get("~recursive") + query.Del("~recursive") + + u.RawQuery = query.Encode() + + r, err := git.PlainOpen(opts.Destination) + if err != nil { + return false, err + } + + w, err := r.Worktree() + if err != nil { + return false, err + } + + depth := 0 + if depthStr != "" { + depth, err = strconv.Atoi(depthStr) + if err != nil { + return false, err + } + } + + po := &git.PullOptions{ + Depth: depth, + Progress: opts.Progress, + RecurseSubmodules: git.NoRecurseSubmodules, + } + + if recursive == "true" { + po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth + } + + m, err := getManifest(opts.Destination) + manifestOK := err == nil + + err = w.Pull(po) + if errors.Is(err, git.NoErrAlreadyUpToDate) { + return false, nil + } else if err != nil { + return false, err + } + + if manifestOK { + err = writeManifest(opts.Destination, m) + if err != nil { + return true, err + } + } + + return true, nil +} diff --git a/internal/dl/torrent.go b/internal/dl/torrent.go new file mode 100644 index 0000000..c09931c --- /dev/null +++ b/internal/dl/torrent.go @@ -0,0 +1,90 @@ +package dl + +import ( + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" +) + +var ( + urlMatchRegex = regexp.MustCompile(`(magnet|torrent\+https?):.*`) + ErrAria2NotFound = errors.New("aria2 must be installed for torrent functionality") + ErrDestinationEmpty = errors.New("the destination directory is empty") +) + +type TorrentDownloader struct{} + +// Name always returns "file" +func (TorrentDownloader) Name() string { + return "torrent" +} + +// MatchURL returns true if the URL is a magnet link +// or an http(s) link with a "torrent+" prefix +func (TorrentDownloader) MatchURL(u string) bool { + return urlMatchRegex.MatchString(u) +} + +// Download downloads a file over the BitTorrent protocol. +func (TorrentDownloader) Download(opts Options) (Type, string, error) { + aria2Path, err := exec.LookPath("aria2c") + if err != nil { + return 0, "", ErrAria2NotFound + } + + opts.URL = strings.TrimPrefix(opts.URL, "torrent+") + + cmd := exec.Command(aria2Path, "--summary-interval=0", "--log-level=warn", "--seed-time=0", "--dir="+opts.Destination, opts.URL) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return 0, "", fmt.Errorf("aria2c returned an error: %w", err) + } + + err = removeTorrentFiles(opts.Destination) + if err != nil { + return 0, "", err + } + + return determineType(opts.Destination) +} + +func removeTorrentFiles(path string) error { + filePaths, err := filepath.Glob(filepath.Join(path, "*.torrent")) + if err != nil { + return err + } + + for _, filePath := range filePaths { + err = os.Remove(filePath) + if err != nil { + return err + } + } + + return nil +} + +func determineType(path string) (Type, string, error) { + files, err := os.ReadDir(path) + if err != nil { + return 0, "", err + } + + if len(files) > 1 { + return TypeDir, "", nil + } else if len(files) == 1 { + if files[0].IsDir() { + return TypeDir, files[0].Name(), nil + } else { + return TypeFile, files[0].Name(), nil + } + } + + return 0, "", ErrDestinationEmpty +} diff --git a/internal/dlcache/dlcache.go b/internal/dlcache/dlcache.go new file mode 100644 index 0000000..0064233 --- /dev/null +++ b/internal/dlcache/dlcache.go @@ -0,0 +1,93 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dlcache + +import ( + "context" + "crypto/sha1" + "encoding/hex" + "io" + "os" + "path/filepath" + + "lure.sh/lure/internal/config" +) + +// BasePath returns the base path of the download cache +func BasePath(ctx context.Context) string { + return filepath.Join(config.GetPaths(ctx).CacheDir, "dl") +} + +// New creates a new directory with the given ID in the cache. +// If a directory with the same ID already exists, +// it will be deleted before creating a new one. +func New(ctx context.Context, id string) (string, error) { + h, err := hashID(id) + if err != nil { + return "", err + } + itemPath := filepath.Join(BasePath(ctx), h) + + fi, err := os.Stat(itemPath) + if err == nil || (fi != nil && !fi.IsDir()) { + err = os.RemoveAll(itemPath) + if err != nil { + return "", err + } + } + + err = os.MkdirAll(itemPath, 0o755) + if err != nil { + return "", err + } + + return itemPath, nil +} + +// Get checks if an entry with the given ID +// already exists in the cache, and if so, +// returns the directory and true. If it +// does not exist, it returns an empty string +// and false. +func Get(ctx context.Context, id string) (string, bool) { + h, err := hashID(id) + if err != nil { + return "", false + } + itemPath := filepath.Join(BasePath(ctx), h) + + _, err = os.Stat(itemPath) + if err != nil { + return "", false + } + + return itemPath, true +} + +// hashID hashes the input ID with SHA1 +// and returns the hex string of the hashed +// ID. +func hashID(id string) (string, error) { + h := sha1.New() + _, err := io.WriteString(h, id) + if err != nil { + return "", err + } + return hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/internal/dlcache/dlcache_test.go b/internal/dlcache/dlcache_test.go new file mode 100644 index 0000000..85450a4 --- /dev/null +++ b/internal/dlcache/dlcache_test.go @@ -0,0 +1,76 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package dlcache_test + +import ( + "context" + "crypto/sha1" + "encoding/hex" + "io" + "os" + "path/filepath" + "testing" + + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/dlcache" +) + +func init() { + dir, err := os.MkdirTemp("/tmp", "lure-dlcache-test.*") + if err != nil { + panic(err) + } + config.GetPaths(context.Background()).RepoDir = dir +} + +func TestNew(t *testing.T) { + const id = "https://example.com" + dir, err := dlcache.New(id) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + exp := filepath.Join(dlcache.BasePath(), sha1sum(id)) + if dir != exp { + t.Errorf("Expected %s, got %s", exp, dir) + } + + fi, err := os.Stat(dir) + if err != nil { + t.Errorf("stat: expected no error, got %s", err) + } + + if !fi.IsDir() { + t.Errorf("Expected cache item to be a directory") + } + + dir2, ok := dlcache.Get(id) + if !ok { + t.Errorf("Expected Get() to return valid value") + } + if dir2 != dir { + t.Errorf("Expected %s from Get(), got %s", dir, dir2) + } +} + +func sha1sum(id string) string { + h := sha1.New() + _, _ = io.WriteString(h, id) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/internal/osutils/move.go b/internal/osutils/move.go new file mode 100644 index 0000000..e8405b3 --- /dev/null +++ b/internal/osutils/move.go @@ -0,0 +1,110 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package osutils + +import ( + "io" + "os" + "path/filepath" +) + +// Move attempts to use os.Rename and if that fails (such as for a cross-device move), +// it instead copies the source to the destination and then removes the source. +func Move(sourcePath, destPath string) error { + // Try to rename the source to the destination + err := os.Rename(sourcePath, destPath) + if err == nil { + return nil // Successful move + } + + // Rename failed, so copy the source to the destination + err = copyDirOrFile(sourcePath, destPath) + if err != nil { + return err + } + + // Copy successful, remove the original source + err = os.RemoveAll(sourcePath) + if err != nil { + return err + } + + return nil +} + +func copyDirOrFile(sourcePath, destPath string) error { + sourceInfo, err := os.Stat(sourcePath) + if err != nil { + return err + } + + if sourceInfo.IsDir() { + return copyDir(sourcePath, destPath, sourceInfo) + } else if sourceInfo.Mode().IsRegular() { + return copyFile(sourcePath, destPath, sourceInfo) + } else { + // ignore non-regular files + return nil + } +} + +func copyDir(sourcePath, destPath string, sourceInfo os.FileInfo) error { + err := os.MkdirAll(destPath, sourceInfo.Mode()) + if err != nil { + return err + } + + entries, err := os.ReadDir(sourcePath) + if err != nil { + return err + } + + for _, entry := range entries { + sourceEntry := filepath.Join(sourcePath, entry.Name()) + destEntry := filepath.Join(destPath, entry.Name()) + + err = copyDirOrFile(sourceEntry, destEntry) + if err != nil { + return err + } + } + + return nil +} + +func copyFile(sourcePath, destPath string, sourceInfo os.FileInfo) error { + sourceFile, err := os.Open(sourcePath) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, sourceInfo.Mode()) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + if err != nil { + return err + } + + return nil +} diff --git a/internal/overrides/overrides.go b/internal/overrides/overrides.go new file mode 100644 index 0000000..95008a3 --- /dev/null +++ b/internal/overrides/overrides.go @@ -0,0 +1,223 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package overrides + +import ( + "reflect" + "strings" + + "lure.sh/lure/internal/cpu" + "lure.sh/lure/internal/db" + "lure.sh/lure/pkg/distro" + "golang.org/x/exp/slices" + "golang.org/x/text/language" +) + +type Opts struct { + Name string + Overrides bool + LikeDistros bool + Languages []string + LanguageTags []language.Tag +} + +var DefaultOpts = &Opts{ + Overrides: true, + LikeDistros: true, + Languages: []string{"en"}, +} + +// Resolve generates a slice of possible override names in the order that they should be checked +func Resolve(info *distro.OSRelease, opts *Opts) ([]string, error) { + if opts == nil { + opts = DefaultOpts + } + + if !opts.Overrides { + return []string{opts.Name}, nil + } + + langs, err := parseLangs(opts.Languages, opts.LanguageTags) + if err != nil { + return nil, err + } + + architectures, err := cpu.CompatibleArches(cpu.Arch()) + if err != nil { + return nil, err + } + + distros := []string{info.ID} + if opts.LikeDistros { + distros = append(distros, info.Like...) + } + + var out []string + for _, lang := range langs { + for _, distro := range distros { + for _, arch := range architectures { + out = append(out, opts.Name+"_"+arch+"_"+distro+"_"+lang) + } + + out = append(out, opts.Name+"_"+distro+"_"+lang) + } + + for _, arch := range architectures { + out = append(out, opts.Name+"_"+arch+"_"+lang) + } + + out = append(out, opts.Name+"_"+lang) + } + + for _, distro := range distros { + for _, arch := range architectures { + out = append(out, opts.Name+"_"+arch+"_"+distro) + } + + out = append(out, opts.Name+"_"+distro) + } + + for _, arch := range architectures { + out = append(out, opts.Name+"_"+arch) + } + + out = append(out, opts.Name) + + for index, item := range out { + out[index] = strings.TrimPrefix(strings.ReplaceAll(item, "-", "_"), "_") + } + + return out, nil +} + +func (o *Opts) WithName(name string) *Opts { + out := &Opts{} + *out = *o + + out.Name = name + return out +} + +func (o *Opts) WithOverrides(v bool) *Opts { + out := &Opts{} + *out = *o + + out.Overrides = v + return out +} + +func (o *Opts) WithLikeDistros(v bool) *Opts { + out := &Opts{} + *out = *o + + out.LikeDistros = v + return out +} + +func (o *Opts) WithLanguages(langs []string) *Opts { + out := &Opts{} + *out = *o + + out.Languages = langs + return out +} + +func (o *Opts) WithLanguageTags(langs []string) *Opts { + out := &Opts{} + *out = *o + + out.Languages = langs + return out +} + +// ResolvedPackage is a LURE package after its overrides +// have been resolved +type ResolvedPackage struct { + Name string `sh:"name"` + Version string `sh:"version"` + Release int `sh:"release"` + Epoch uint `sh:"epoch"` + Description string `db:"description"` + Homepage string `db:"homepage"` + Maintainer string `db:"maintainer"` + Architectures []string `sh:"architectures"` + Licenses []string `sh:"license"` + Provides []string `sh:"provides"` + Conflicts []string `sh:"conflicts"` + Replaces []string `sh:"replaces"` + Depends []string `sh:"deps"` + BuildDepends []string `sh:"build_deps"` + OptDepends []string `sh:"opt_deps"` +} + +func ResolvePackage(pkg *db.Package, overrides []string) *ResolvedPackage { + out := &ResolvedPackage{} + outVal := reflect.ValueOf(out).Elem() + pkgVal := reflect.ValueOf(pkg).Elem() + + for i := 0; i < outVal.NumField(); i++ { + fieldVal := outVal.Field(i) + fieldType := fieldVal.Type() + pkgFieldVal := pkgVal.FieldByName(outVal.Type().Field(i).Name) + pkgFieldType := pkgFieldVal.Type() + + if strings.HasPrefix(pkgFieldType.String(), "db.JSON") { + pkgFieldVal = pkgFieldVal.FieldByName("Val") + pkgFieldType = pkgFieldVal.Type() + } + + if pkgFieldType.AssignableTo(fieldType) { + fieldVal.Set(pkgFieldVal) + continue + } + + if pkgFieldVal.Kind() == reflect.Map && pkgFieldType.Elem().AssignableTo(fieldType) { + for _, override := range overrides { + overrideVal := pkgFieldVal.MapIndex(reflect.ValueOf(override)) + if !overrideVal.IsValid() { + continue + } + + fieldVal.Set(overrideVal) + break + } + } + } + + return out +} + +func parseLangs(langs []string, tags []language.Tag) ([]string, error) { + out := make([]string, len(tags)+len(langs)) + for i, tag := range tags { + base, _ := tag.Base() + out[i] = base.String() + } + for i, lang := range langs { + tag, err := language.Parse(lang) + if err != nil { + return nil, err + } + base, _ := tag.Base() + out[len(tags)+i] = base.String() + } + slices.Sort(out) + out = slices.Compact(out) + return out, nil +} diff --git a/internal/overrides/overrides_test.go b/internal/overrides/overrides_test.go new file mode 100644 index 0000000..9ee841a --- /dev/null +++ b/internal/overrides/overrides_test.go @@ -0,0 +1,195 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package overrides_test + +import ( + "os" + "reflect" + "testing" + + "lure.sh/lure/internal/overrides" + "lure.sh/lure/pkg/distro" + "golang.org/x/text/language" +) + +var info = &distro.OSRelease{ + ID: "centos", + Like: []string{"rhel", "fedora"}, +} + +func TestResolve(t *testing.T) { + names, err := overrides.Resolve(info, nil) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + expected := []string{ + "amd64_centos_en", + "centos_en", + "amd64_rhel_en", + "rhel_en", + "amd64_fedora_en", + "fedora_en", + "amd64_en", + "en", + "amd64_centos", + "centos", + "amd64_rhel", + "rhel", + "amd64_fedora", + "fedora", + "amd64", + "", + } + + if !reflect.DeepEqual(names, expected) { + t.Errorf("expected %v, got %v", expected, names) + } +} + +func TestResolveName(t *testing.T) { + names, err := overrides.Resolve(info, &overrides.Opts{ + Name: "deps", + Overrides: true, + LikeDistros: true, + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + expected := []string{ + "deps_amd64_centos", + "deps_centos", + "deps_amd64_rhel", + "deps_rhel", + "deps_amd64_fedora", + "deps_fedora", + "deps_amd64", + "deps", + } + + if !reflect.DeepEqual(names, expected) { + t.Errorf("expected %v, got %v", expected, names) + } +} + +func TestResolveArch(t *testing.T) { + os.Setenv("LURE_ARCH", "arm7") + defer os.Setenv("LURE_ARCH", "") + + names, err := overrides.Resolve(info, &overrides.Opts{ + Name: "deps", + Overrides: true, + LikeDistros: true, + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + expected := []string{ + "deps_arm7_centos", + "deps_arm6_centos", + "deps_arm5_centos", + "deps_centos", + "deps_arm7_rhel", + "deps_arm6_rhel", + "deps_arm5_rhel", + "deps_rhel", + "deps_arm7_fedora", + "deps_arm6_fedora", + "deps_arm5_fedora", + "deps_fedora", + "deps_arm7", + "deps_arm6", + "deps_arm5", + "deps", + } + + if !reflect.DeepEqual(names, expected) { + t.Errorf("expected %v, got %v", expected, names) + } +} + +func TestResolveNoLikeDistros(t *testing.T) { + names, err := overrides.Resolve(info, &overrides.Opts{ + Overrides: true, + LikeDistros: false, + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + expected := []string{ + "amd64_centos", + "centos", + "amd64", + "", + } + + if !reflect.DeepEqual(names, expected) { + t.Errorf("expected %v, got %v", expected, names) + } +} + +func TestResolveNoOverrides(t *testing.T) { + names, err := overrides.Resolve(info, &overrides.Opts{ + Name: "deps", + Overrides: false, + LikeDistros: false, + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + expected := []string{"deps"} + + if !reflect.DeepEqual(names, expected) { + t.Errorf("expected %v, got %v", expected, names) + } +} + +func TestResolveLangs(t *testing.T) { + names, err := overrides.Resolve(info, &overrides.Opts{ + Overrides: true, + Languages: []string{"ru_RU", "en", "en_US"}, + LanguageTags: []language.Tag{language.BritishEnglish}, + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + expected := []string{ + "amd64_centos_en", + "centos_en", + "amd64_en", + "en", + "amd64_centos_ru", + "centos_ru", + "amd64_ru", + "ru", + "amd64_centos", + "centos", + "amd64", + "", + } + + if !reflect.DeepEqual(names, expected) { + t.Errorf("expected %v, got %v", expected, names) + } +} diff --git a/internal/pager/highlighting.go b/internal/pager/highlighting.go new file mode 100644 index 0000000..9e85366 --- /dev/null +++ b/internal/pager/highlighting.go @@ -0,0 +1,37 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package pager + +import ( + "bytes" + "io" + + "github.com/alecthomas/chroma/v2/quick" +) + +func SyntaxHighlightBash(r io.Reader, style string) (string, error) { + data, err := io.ReadAll(r) + if err != nil { + return "", err + } + + w := &bytes.Buffer{} + err = quick.Highlight(w, string(data), "bash", "terminal", style) + return w.String(), err +} diff --git a/internal/pager/pager.go b/internal/pager/pager.go new file mode 100644 index 0000000..04d6199 --- /dev/null +++ b/internal/pager/pager.go @@ -0,0 +1,143 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package pager + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/muesli/reflow/wordwrap" +) + +var ( + titleStyle lipgloss.Style + infoStyle lipgloss.Style +) + +func init() { + b1 := lipgloss.RoundedBorder() + b1.Right = "\u251C" + titleStyle = lipgloss.NewStyle().BorderStyle(b1).Padding(0, 1) + + b2 := lipgloss.RoundedBorder() + b2.Left = "\u2524" + infoStyle = titleStyle.Copy().BorderStyle(b2) +} + +type Pager struct { + model pagerModel +} + +func New(name, content string) *Pager { + return &Pager{ + model: pagerModel{ + name: name, + content: content, + }, + } +} + +func (p *Pager) Run() error { + prog := tea.NewProgram( + p.model, + tea.WithMouseCellMotion(), + tea.WithAltScreen(), + ) + + _, err := prog.Run() + return err +} + +type pagerModel struct { + name string + content string + ready bool + viewport viewport.Model +} + +func (pm pagerModel) Init() tea.Cmd { + return nil +} + +func (pm pagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + cmd tea.Cmd + cmds []tea.Cmd + ) + + switch msg := msg.(type) { + case tea.KeyMsg: + k := msg.String() + if k == "ctrl+c" || k == "q" || k == "esc" { + return pm, tea.Quit + } + case tea.WindowSizeMsg: + headerHeight := lipgloss.Height(pm.headerView()) + footerHeight := lipgloss.Height(pm.footerView()) + verticalMarginHeight := headerHeight + footerHeight + + if !pm.ready { + pm.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight) + pm.viewport.HighPerformanceRendering = true + pm.viewport.YPosition = headerHeight + 1 + pm.viewport.SetContent(wordwrap.String(pm.content, msg.Width)) + pm.ready = true + } else { + pm.viewport.Width = msg.Width + pm.viewport.Height = msg.Height - verticalMarginHeight + } + + cmds = append(cmds, viewport.Sync(pm.viewport)) + } + + // Handle keyboard and mouse events in the viewport + pm.viewport, cmd = pm.viewport.Update(msg) + cmds = append(cmds, cmd) + + return pm, tea.Batch(cmds...) +} + +func (pm pagerModel) View() string { + if !pm.ready { + return "\n Initializing..." + } + return fmt.Sprintf("%s\n%s\n%s", pm.headerView(), pm.viewport.View(), pm.footerView()) +} + +func (pm pagerModel) headerView() string { + title := titleStyle.Render(pm.name) + line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(title))) + return lipgloss.JoinHorizontal(lipgloss.Center, title, line) +} + +func (pm pagerModel) footerView() string { + info := infoStyle.Render(fmt.Sprintf("%3.f%%", pm.viewport.ScrollPercent()*100)) + line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(info))) + return lipgloss.JoinHorizontal(lipgloss.Center, line, info) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/internal/shutils/decoder/decoder.go b/internal/shutils/decoder/decoder.go new file mode 100644 index 0000000..26acdcf --- /dev/null +++ b/internal/shutils/decoder/decoder.go @@ -0,0 +1,223 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package decoder + +import ( + "context" + "errors" + "reflect" + "strings" + + "github.com/mitchellh/mapstructure" + "lure.sh/lure/internal/overrides" + "lure.sh/lure/pkg/distro" + "golang.org/x/exp/slices" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +var ErrNotPointerToStruct = errors.New("val must be a pointer to a struct") + +type VarNotFoundError struct { + name string +} + +func (nfe VarNotFoundError) Error() string { + return "required variable '" + nfe.name + "' could not be found" +} + +type InvalidTypeError struct { + name string + vartype string + exptype string +} + +func (ite InvalidTypeError) Error() string { + return "variable '" + ite.name + "' is of type " + ite.vartype + ", but " + ite.exptype + " is expected" +} + +// Decoder provides methods for decoding variable values +type Decoder struct { + info *distro.OSRelease + Runner *interp.Runner + // Enable distro overrides (true by default) + Overrides bool + // Enable using like distros for overrides + LikeDistros bool +} + +// New creates a new variable decoder +func New(info *distro.OSRelease, runner *interp.Runner) *Decoder { + return &Decoder{info, runner, true, len(info.Like) > 0} +} + +// DecodeVar decodes a variable to val using reflection. +// Structs should use the "sh" struct tag. +func (d *Decoder) DecodeVar(name string, val any) error { + variable := d.getVar(name) + if variable == nil { + return VarNotFoundError{name} + } + + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + WeaklyTypedInput: true, + DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) { + if strings.Contains(to.Type().String(), "db.JSON") { + valType := to.FieldByName("Val").Type() + if !from.Type().AssignableTo(valType) { + return nil, InvalidTypeError{name, from.Type().String(), valType.String()} + } + + to.FieldByName("Val").Set(from) + return to, nil + } + return from.Interface(), nil + }), + Result: val, + TagName: "sh", + }) + if err != nil { + return err + } + + switch variable.Kind { + case expand.Indexed: + return dec.Decode(variable.List) + case expand.Associative: + return dec.Decode(variable.Map) + default: + return dec.Decode(variable.Str) + } +} + +// DecodeVars decodes all variables to val using reflection. +// Structs should use the "sh" struct tag. +func (d *Decoder) DecodeVars(val any) error { + valKind := reflect.TypeOf(val).Kind() + if valKind != reflect.Pointer { + return ErrNotPointerToStruct + } else { + elemKind := reflect.TypeOf(val).Elem().Kind() + if elemKind != reflect.Struct { + return ErrNotPointerToStruct + } + } + + rVal := reflect.ValueOf(val).Elem() + + for i := 0; i < rVal.NumField(); i++ { + field := rVal.Field(i) + fieldType := rVal.Type().Field(i) + + if !fieldType.IsExported() { + continue + } + + name := fieldType.Name + tag := fieldType.Tag.Get("sh") + required := false + if tag != "" { + if strings.Contains(tag, ",") { + splitTag := strings.Split(tag, ",") + name = splitTag[0] + + if len(splitTag) > 1 { + if slices.Contains(splitTag, "required") { + required = true + } + } + } else { + name = tag + } + } + + newVal := reflect.New(field.Type()) + err := d.DecodeVar(name, newVal.Interface()) + if _, ok := err.(VarNotFoundError); ok && !required { + continue + } else if err != nil { + return err + } + + field.Set(newVal.Elem()) + } + + return nil +} + +type ScriptFunc func(ctx context.Context, opts ...interp.RunnerOption) error + +// GetFunc returns a function corresponding to a bash function +// with the given name +func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) { + fn := d.getFunc(name) + if fn == nil { + return nil, false + } + + return func(ctx context.Context, opts ...interp.RunnerOption) error { + sub := d.Runner.Subshell() + for _, opt := range opts { + opt(sub) + } + return sub.Run(ctx, fn) + }, true +} + +func (d *Decoder) getFunc(name string) *syntax.Stmt { + names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) + if err != nil { + return nil + } + + for _, fnName := range names { + fn, ok := d.Runner.Funcs[fnName] + if ok { + return fn + } + } + return nil +} + +// getVar gets a variable based on its name, taking into account +// override variables and nameref variables. +func (d *Decoder) getVar(name string) *expand.Variable { + names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) + if err != nil { + return nil + } + + for _, varName := range names { + val, ok := d.Runner.Vars[varName] + if ok { + // Resolve nameref variables + _, resolved := val.Resolve(expand.FuncEnviron(func(s string) string { + if val, ok := d.Runner.Vars[s]; ok { + return val.String() + } + return "" + })) + val = resolved + + return &val + } + } + return nil +} diff --git a/internal/shutils/decoder/decoder_test.go b/internal/shutils/decoder/decoder_test.go new file mode 100644 index 0000000..e885ab8 --- /dev/null +++ b/internal/shutils/decoder/decoder_test.go @@ -0,0 +1,224 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package decoder_test + +import ( + "bytes" + "context" + "errors" + "os" + "reflect" + "strings" + "testing" + + "lure.sh/lure/internal/shutils/decoder" + "lure.sh/lure/pkg/distro" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +type BuildVars struct { + Name string `sh:"name,required"` + Version string `sh:"version,required"` + Release int `sh:"release,required"` + Epoch uint `sh:"epoch"` + Description string `sh:"desc"` + Homepage string `sh:"homepage"` + Maintainer string `sh:"maintainer"` + Architectures []string `sh:"architectures"` + Licenses []string `sh:"license"` + Provides []string `sh:"provides"` + Conflicts []string `sh:"conflicts"` + Depends []string `sh:"deps"` + BuildDepends []string `sh:"build_deps"` + Replaces []string `sh:"replaces"` +} + +const testScript = ` + name='test' + version='0.0.1' + release=1 + epoch=2 + desc="Test package" + homepage='https://lure.arsenm.dev' + maintainer='Arsen Musayelyan ' + architectures=('arm64' 'amd64') + license=('GPL-3.0-or-later') + provides=('test') + conflicts=('test') + replaces=('test-old') + replaces_test_os=('test-legacy') + + deps=('sudo') + + build_deps=('golang') + build_deps_arch=('go') + + test() { + echo "Test" + } + + package() { + install-binary test + } +` + +var osRelease = &distro.OSRelease{ + ID: "test_os", + Like: []string{"arch"}, +} + +func TestDecodeVars(t *testing.T) { + ctx := context.Background() + + fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + runner, err := interp.New() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + err = runner.Run(ctx, fl) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + dec := decoder.New(osRelease, runner) + + var bv BuildVars + err = dec.DecodeVars(&bv) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + expected := BuildVars{ + Name: "test", + Version: "0.0.1", + Release: 1, + Epoch: 2, + Description: "Test package", + Homepage: "https://lure.arsenm.dev", + Maintainer: "Arsen Musayelyan ", + Architectures: []string{"arm64", "amd64"}, + Licenses: []string{"GPL-3.0-or-later"}, + Provides: []string{"test"}, + Conflicts: []string{"test"}, + Replaces: []string{"test-legacy"}, + Depends: []string{"sudo"}, + BuildDepends: []string{"go"}, + } + + if !reflect.DeepEqual(bv, expected) { + t.Errorf("Expected %v, got %v", expected, bv) + } +} + +func TestDecodeVarsMissing(t *testing.T) { + ctx := context.Background() + + const testScript = ` + name='test' + epoch=2 + desc="Test package" + homepage='https://lure.arsenm.dev' + maintainer='Arsen Musayelyan ' + architectures=('arm64' 'amd64') + license=('GPL-3.0-or-later') + provides=('test') + conflicts=('test') + replaces=('test-old') + replaces_test_os=('test-legacy') + + deps=('sudo') + + build_deps=('golang') + build_deps_arch=('go') + + test() { + echo "Test" + } + + package() { + install-binary test + } + ` + + fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + runner, err := interp.New() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + err = runner.Run(ctx, fl) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + dec := decoder.New(osRelease, runner) + + var bv BuildVars + err = dec.DecodeVars(&bv) + + var notFoundErr decoder.VarNotFoundError + if !errors.As(err, ¬FoundErr) { + t.Fatalf("Expected VarNotFoundError, got %T %v", err, err) + } +} + +func TestGetFunc(t *testing.T) { + ctx := context.Background() + + fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + runner, err := interp.New() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + err = runner.Run(ctx, fl) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + dec := decoder.New(osRelease, runner) + fn, ok := dec.GetFunc("test") + if !ok { + t.Fatalf("Expected test() function to exist") + } + + buf := &bytes.Buffer{} + err = fn(ctx, interp.StdIO(os.Stdin, buf, buf)) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if buf.String() != "Test\n" { + t.Fatalf(`Expected "Test\n", got %#v`, buf.String()) + } +} diff --git a/internal/shutils/handlers/exec.go b/internal/shutils/handlers/exec.go new file mode 100644 index 0000000..797a64b --- /dev/null +++ b/internal/shutils/handlers/exec.go @@ -0,0 +1,63 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package handlers + +import ( + "context" + "fmt" + "time" + + "mvdan.cc/sh/v3/interp" +) + +func InsufficientArgsError(cmd string, exp, got int) error { + argsWord := "arguments" + if exp == 1 { + argsWord = "argument" + } + + return fmt.Errorf("%s: command requires at least %d %s, got %d", cmd, exp, argsWord, got) +} + +type ExecFunc func(hc interp.HandlerContext, name string, args []string) error + +type ExecFuncs map[string]ExecFunc + +// ExecHandler returns a new ExecHandlerFunc that falls back to fallback +// if the command cannot be found in the map. If fallback is nil, the default +// handler is used. +func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc { + return func(ctx context.Context, args []string) error { + name := args[0] + + if fn, ok := ef[name]; ok { + hctx := interp.HandlerCtx(ctx) + if len(args) > 1 { + return fn(hctx, args[0], args[1:]) + } else { + return fn(hctx, args[0], nil) + } + } + + if fallback == nil { + fallback = interp.DefaultExecHandler(2 * time.Second) + } + return fallback(ctx, args) + } +} diff --git a/internal/shutils/handlers/exec_test.go b/internal/shutils/handlers/exec_test.go new file mode 100644 index 0000000..2bf34f8 --- /dev/null +++ b/internal/shutils/handlers/exec_test.go @@ -0,0 +1,124 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package handlers_test + +import ( + "context" + "strings" + "testing" + + "lure.sh/lure/internal/shutils/handlers" + "lure.sh/lure/internal/shutils/decoder" + "lure.sh/lure/pkg/distro" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +const testScript = ` + name='test' + version='0.0.1' + release=1 + epoch=2 + desc="Test package" + homepage='https://lure.sh' + maintainer='Elara Musayelyan ' + architectures=('arm64' 'amd64') + license=('GPL-3.0-or-later') + provides=('test') + conflicts=('test') + replaces=('test-old') + replaces_test_os=('test-legacy') + + deps=('sudo') + + build_deps=('golang') + build_deps_arch=('go') + + test() { + test-cmd "Hello, World" + test-fb + } + + package() { + install-binary test + } +` + +var osRelease = &distro.OSRelease{ + ID: "test_os", + Like: []string{"arch"}, +} + +func TestExecFuncs(t *testing.T) { + ctx := context.Background() + + fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + runner, err := interp.New() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + err = runner.Run(ctx, fl) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + dec := decoder.New(osRelease, runner) + fn, ok := dec.GetFunc("test") + if !ok { + t.Fatalf("Expected test() function to exist") + } + + eh := shutils.ExecFuncs{ + "test-cmd": func(hc interp.HandlerContext, name string, args []string) error { + if name != "test-cmd" { + t.Errorf("Expected name to be 'test-cmd', got '%s'", name) + } + + if len(args) < 1 { + t.Fatalf("Expected at least one argument, got %d", len(args)) + } + + if args[0] != "Hello, World" { + t.Errorf("Expected first argument to be 'Hello, World', got '%s'", args[0]) + } + + return nil + }, + } + + fbInvoked := false + fbHandler := func(context.Context, []string) error { + fbInvoked = true + return nil + } + + err = fn(ctx, interp.ExecHandler(eh.ExecHandler(fbHandler))) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + if !fbInvoked { + t.Errorf("Expected fallback handler to be invoked") + } +} diff --git a/internal/shutils/handlers/fakeroot.go b/internal/shutils/handlers/fakeroot.go new file mode 100644 index 0000000..6929b8b --- /dev/null +++ b/internal/shutils/handlers/fakeroot.go @@ -0,0 +1,114 @@ +package handlers + +import ( + "context" + "fmt" + "os" + "os/exec" + "runtime" + "strings" + "syscall" + "time" + + "lure.sh/fakeroot" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" +) + +// FakerootExecHandler was extracted from github.com/mvdan/sh/interp/handler.go +// and modified to run commands in a fakeroot environent. +func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc { + return func(ctx context.Context, args []string) error { + hc := interp.HandlerCtx(ctx) + path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0]) + if err != nil { + fmt.Fprintln(hc.Stderr, err) + return interp.NewExitStatus(127) + } + cmd := &exec.Cmd{ + Path: path, + Args: args, + Env: execEnv(hc.Env), + Dir: hc.Dir, + Stdin: hc.Stdin, + Stdout: hc.Stdout, + Stderr: hc.Stderr, + } + + err = fakeroot.Apply(cmd) + if err != nil { + return err + } + + err = cmd.Start() + if err == nil { + if done := ctx.Done(); done != nil { + go func() { + <-done + + if killTimeout <= 0 || runtime.GOOS == "windows" { + _ = cmd.Process.Signal(os.Kill) + return + } + + // TODO: don't temporarily leak this goroutine + // if the program stops itself with the + // interrupt. + go func() { + time.Sleep(killTimeout) + _ = cmd.Process.Signal(os.Kill) + }() + _ = cmd.Process.Signal(os.Interrupt) + }() + } + + err = cmd.Wait() + } + + switch x := err.(type) { + case *exec.ExitError: + // started, but errored - default to 1 if OS + // doesn't have exit statuses + if status, ok := x.Sys().(syscall.WaitStatus); ok { + if status.Signaled() { + if ctx.Err() != nil { + return ctx.Err() + } + return interp.NewExitStatus(uint8(128 + status.Signal())) + } + return interp.NewExitStatus(uint8(status.ExitStatus())) + } + return interp.NewExitStatus(1) + case *exec.Error: + // did not start + fmt.Fprintf(hc.Stderr, "%v\n", err) + return interp.NewExitStatus(127) + default: + return err + } + } +} + +// execEnv was extracted from github.com/mvdan/sh/interp/vars.go +func execEnv(env expand.Environ) []string { + list := make([]string, 0, 64) + env.Each(func(name string, vr expand.Variable) bool { + if !vr.IsSet() { + // If a variable is set globally but unset in the + // runner, we need to ensure it's not part of the final + // list. Seems like zeroing the element is enough. + // This is a linear search, but this scenario should be + // rare, and the number of variables shouldn't be large. + for i, kv := range list { + if strings.HasPrefix(kv, name+"=") { + list[i] = "" + } + } + } + if vr.Exported && vr.Kind == expand.String { + list = append(list, name+"="+vr.String()) + } + return true + }) + return list +} diff --git a/internal/shutils/handlers/nop.go b/internal/shutils/handlers/nop.go new file mode 100644 index 0000000..5a32e74 --- /dev/null +++ b/internal/shutils/handlers/nop.go @@ -0,0 +1,55 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package handlers + +import ( + "context" + "io" + "os" +) + +func NopReadDir(context.Context, string) ([]os.FileInfo, error) { + return nil, os.ErrNotExist +} + +func NopStat(context.Context, string, bool) (os.FileInfo, error) { + return nil, os.ErrNotExist +} + +func NopExec(context.Context, []string) error { + return nil +} + +func NopOpen(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) { + return NopRWC{}, nil +} + +type NopRWC struct{} + +func (NopRWC) Read([]byte) (int, error) { + return 0, io.EOF +} + +func (NopRWC) Write(b []byte) (int, error) { + return len(b), nil +} + +func (NopRWC) Close() error { + return nil +} diff --git a/internal/shutils/handlers/nop_test.go b/internal/shutils/handlers/nop_test.go new file mode 100644 index 0000000..0170cdb --- /dev/null +++ b/internal/shutils/handlers/nop_test.go @@ -0,0 +1,58 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package handlers_test + +import ( + "bytes" + "context" + "os" + "strings" + "testing" + + "lure.sh/lure/internal/shutils/handlers" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +func TestNopExec(t *testing.T) { + ctx := context.Background() + + fl, err := syntax.NewParser().Parse(strings.NewReader(`/bin/echo test`), "lure.sh") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + buf := &bytes.Buffer{} + runner, err := interp.New( + interp.ExecHandler(handlers.NopExec), + interp.StdIO(os.Stdin, buf, buf), + ) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + err = runner.Run(ctx, fl) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if buf.String() != "" { + t.Fatalf("Expected empty string, got %#v", buf.String()) + } +} diff --git a/internal/shutils/handlers/restricted.go b/internal/shutils/handlers/restricted.go new file mode 100644 index 0000000..3855abf --- /dev/null +++ b/internal/shutils/handlers/restricted.go @@ -0,0 +1,80 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package handlers + +import ( + "context" + "io" + "io/fs" + "path/filepath" + "strings" + "time" + + "golang.org/x/exp/slices" + "mvdan.cc/sh/v3/interp" +) + +func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc { + return func(ctx context.Context, s string) ([]fs.FileInfo, error) { + path := filepath.Clean(s) + for _, allowedPrefix := range allowedPrefixes { + if strings.HasPrefix(path, allowedPrefix) { + return interp.DefaultReadDirHandler()(ctx, s) + } + } + + return nil, fs.ErrNotExist + } +} + +func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc { + return func(ctx context.Context, s string, b bool) (fs.FileInfo, error) { + path := filepath.Clean(s) + for _, allowedPrefix := range allowedPrefixes { + if strings.HasPrefix(path, allowedPrefix) { + return interp.DefaultStatHandler()(ctx, s, b) + } + } + + return nil, fs.ErrNotExist + } +} + +func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc { + return func(ctx context.Context, s string, i int, fm fs.FileMode) (io.ReadWriteCloser, error) { + path := filepath.Clean(s) + for _, allowedPrefix := range allowedPrefixes { + if strings.HasPrefix(path, allowedPrefix) { + return interp.DefaultOpenHandler()(ctx, s, i, fm) + } + } + + return NopRWC{}, nil + } +} + +func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc { + return func(ctx context.Context, args []string) error { + if slices.Contains(allowedCmds, args[0]) { + return interp.DefaultExecHandler(2*time.Second)(ctx, args) + } + + return nil + } +} diff --git a/internal/shutils/helpers/helpers.go b/internal/shutils/helpers/helpers.go new file mode 100644 index 0000000..e101312 --- /dev/null +++ b/internal/shutils/helpers/helpers.go @@ -0,0 +1,284 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package helpers + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + "strings" + "unsafe" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/object" + "golang.org/x/exp/slices" + "lure.sh/lure/internal/shutils/handlers" + "mvdan.cc/sh/v3/interp" +) + +var ( + ErrNoPipe = errors.New("command requires data to be piped in") + ErrNoDetectManNum = errors.New("manual number cannot be detected from the filename") +) + +// Helpers contains all the helper commands +var Helpers = handlers.ExecFuncs{ + "install-binary": installHelperCmd("/usr/bin", 0o755), + "install-systemd-user": installHelperCmd("/usr/lib/systemd/user", 0o644), + "install-systemd": installHelperCmd("/usr/lib/systemd/system", 0o644), + "install-config": installHelperCmd("/etc", 0o644), + "install-license": installHelperCmd("/usr/share/licenses", 0o644), + "install-desktop": installHelperCmd("/usr/share/applications", 0o644), + "install-icon": installHelperCmd("/usr/share/pixmaps", 0o644), + "install-manual": installManualCmd, + "install-completion": installCompletionCmd, + "install-library": installLibraryCmd, + "git-version": gitVersionCmd, +} + +// Restricted contains restricted read-only helper commands +// that don't modify any state +var Restricted = handlers.ExecFuncs{ + "git-version": gitVersionCmd, +} + +func installHelperCmd(prefix string, perms os.FileMode) handlers.ExecFunc { + return func(hc interp.HandlerContext, cmd string, args []string) error { + if len(args) < 1 { + return handlers.InsufficientArgsError(cmd, 1, len(args)) + } + + from := resolvePath(hc, args[0]) + to := "" + if len(args) > 1 { + to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, args[1]) + } else { + to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from)) + } + + err := helperInstall(from, to, perms) + if err != nil { + return fmt.Errorf("%s: %w", cmd, err) + } + return nil + } +} + +func installManualCmd(hc interp.HandlerContext, cmd string, args []string) error { + if len(args) < 1 { + return handlers.InsufficientArgsError(cmd, 1, len(args)) + } + + from := resolvePath(hc, args[0]) + number := filepath.Base(from) + // The man page may be compressed with gzip. + // If it is, the .gz extension must be removed to properly + // detect the number at the end of the filename. + number = strings.TrimSuffix(number, ".gz") + number = strings.TrimPrefix(filepath.Ext(number), ".") + + // If number is not actually a number, return an error + if _, err := strconv.Atoi(number); err != nil { + return fmt.Errorf("install-manual: %w", ErrNoDetectManNum) + } + + prefix := "/usr/share/man/man" + number + to := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from)) + + return helperInstall(from, to, 0o644) +} + +func installCompletionCmd(hc interp.HandlerContext, cmd string, args []string) error { + // If the command's stdin is the same as the system's, + // that means nothing was piped in. In this case, return an error. + if hc.Stdin == os.Stdin { + return fmt.Errorf("install-completion: %w", ErrNoPipe) + } + + if len(args) < 2 { + return handlers.InsufficientArgsError(cmd, 2, len(args)) + } + + shell := args[0] + name := args[1] + + var prefix string + switch shell { + case "bash": + prefix = "/usr/share/bash-completion/completions" + case "zsh": + prefix = "/usr/share/zsh/site-functions" + name = "_" + name + case "fish": + prefix = "/usr/share/fish/vendor_completions.d" + name += ".fish" + } + + path := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, name) + + err := os.MkdirAll(filepath.Dir(path), 0o755) + if err != nil { + return err + } + + dst, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0o644) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, hc.Stdin) + return err +} + +func installLibraryCmd(hc interp.HandlerContext, cmd string, args []string) error { + prefix := getLibPrefix(hc) + fn := installHelperCmd(prefix, 0o755) + return fn(hc, cmd, args) +} + +// See https://wiki.debian.org/Multiarch/Tuples +var multiarchTupleMap = map[string]string{ + "386": "i386-linux-gnu", + "amd64": "x86_64-linux-gnu", + "arm5": "arm-linux-gnueabi", + "arm6": "arm-linux-gnueabihf", + "arm7": "arm-linux-gnueabihf", + "arm64": "aarch64-linux-gnu", + "mips": "mips-linux-gnu", + "mipsle": "mipsel-linux-gnu", + "mips64": "mips64-linux-gnuabi64", + "mips64le": "mips64el-linux-gnuabi64", + "ppc64": "powerpc64-linux-gnu", + "ppc64le": "powerpc64le-linux-gnu", + "s390x": "s390x-linux-gnu", + "riscv64": "riscv64-linux-gnu", + "loong64": "loongarch64-linux-gnu", +} + +// usrLibDistros is a list of distros that don't support +// /usr/lib64, and must use /usr/lib +var usrLibDistros = []string{ + "arch", + "alpine", + "void", + "chimera", +} + +// Based on CMake's GNUInstallDirs +func getLibPrefix(hc interp.HandlerContext) string { + if dir, ok := os.LookupEnv("LURE_LIB_DIR"); ok { + return dir + } + + out := "/usr/lib" + + distroID := hc.Env.Get("DISTRO_ID").Str + distroLike := strings.Split(hc.Env.Get("DISTRO_ID_LIKE").Str, " ") + + for _, usrLibDistro := range usrLibDistros { + if distroID == usrLibDistro || slices.Contains(distroLike, usrLibDistro) { + return out + } + } + + wordSize := unsafe.Sizeof(uintptr(0)) + if wordSize == 8 { + out = "/usr/lib64" + } + + architecture := hc.Env.Get("ARCH").Str + + if distroID == "debian" || slices.Contains(distroLike, "debian") || + distroID == "ubuntu" || slices.Contains(distroLike, "ubuntu") { + + tuple, ok := multiarchTupleMap[architecture] + if ok { + out = filepath.Join("/usr/lib", tuple) + } + } + + return out +} + +func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error { + path := hc.Dir + if len(args) > 0 { + path = resolvePath(hc, args[0]) + } + + r, err := git.PlainOpen(path) + if err != nil { + return fmt.Errorf("git-version: %w", err) + } + + revNum := 0 + commits, err := r.Log(&git.LogOptions{}) + if err != nil { + return fmt.Errorf("git-version: %w", err) + } + + commits.ForEach(func(*object.Commit) error { + revNum++ + return nil + }) + + HEAD, err := r.Head() + if err != nil { + return fmt.Errorf("git-version: %w", err) + } + + hash := HEAD.Hash().String() + + fmt.Fprintf(hc.Stdout, "%d.%s\n", revNum, hash[:7]) + + return nil +} + +func helperInstall(from, to string, perms os.FileMode) error { + err := os.MkdirAll(filepath.Dir(to), 0o755) + if err != nil { + return err + } + + src, err := os.Open(from) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.OpenFile(to, os.O_TRUNC|os.O_CREATE|os.O_RDWR, perms) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, src) + return err +} + +func resolvePath(hc interp.HandlerContext, path string) string { + if !filepath.IsAbs(path) { + return filepath.Join(hc.Dir, path) + } + return path +} diff --git a/internal/translations/files/lure.en.toml b/internal/translations/files/lure.en.toml new file mode 100644 index 0000000..c462567 --- /dev/null +++ b/internal/translations/files/lure.en.toml @@ -0,0 +1,155 @@ +[[translation]] +id = 1228660974 +value = 'Pulling repository' + +[[translation]] +id = 2779805870 +value = 'Repository up to date' + +[[translation]] +id = 1433222829 +value = 'Would you like to view the build script for' + +[[translation]] +id = 2470847050 +value = 'Failed to prompt user to view build script' + +[[translation]] +id = 855659503 +value = 'Would you still like to continue?' + +[[translation]] +id = 1997041569 +value = 'User chose not to continue after reading script' + +[[translation]] +id = 2347700990 +value = 'Building package' + +[[translation]] +id = 2105058868 +value = 'Downloading sources' + +[[translation]] +id = 1884485082 +value = 'Downloading source' + +[[translation]] +id = 1519177982 +value = 'Error building package' + +[[translation]] +id = 2125220917 +value = 'Choose which package(s) to install' + +[[translation]] +id = 812531604 +value = 'Error prompting for choice of package' + +[[translation]] +id = 1040982801 +value = 'Updating version' + +[[translation]] +id = 1014897988 +value = 'Remove build dependencies?' + +[[translation]] +id = 2205430948 +value = 'Installing build dependencies' + +[[translation]] +id = 2522710805 +value = 'Installing dependencies' + +[[translation]] +id = 3602138206 +value = 'Error installing package' + +[[translation]] +id = 2235794125 +value = 'Would you like to remove build dependencies?' + +[[translation]] +id = 2562049386 +value = "Your system's CPU architecture doesn't match this package. Do you want to build anyway?" + +[[translation]] +id = 4006393493 +value = 'The checksums array must be the same length as sources' + +[[translation]] +id = 3759891273 +value = 'The package() function is required' + +[[translation]] +id = 1057080231 +value = 'Executing package()' + +[[translation]] +id = 2687735200 +value = 'Executing prepare()' + +[[translation]] +id = 535572372 +value = 'Executing version()' + +[[translation]] +id = 436644691 +value = 'Executing build()' + +[[translation]] +id = 1393316459 +value = 'This package is already installed' + +[[translation]] +id = 1267660189 +value = 'Source can be updated, updating if required' + +[[translation]] +id = 21753247 +value = 'Source found in cache, linked to destination' + +[[translation]] +id = 257354570 +value = 'Compressing package' + +[[translation]] +id = 2952487371 +value = 'Building package metadata' + +[[translation]] +id = 3121791194 +value = 'Running LURE as root is forbidden as it may cause catastrophic damage to your system' + +[[translation]] +id = 1256604213 +value = 'Waiting for torrent metadata' + +[[translation]] +id = 432261354 +value = 'Downloading torrent file' + +[[translation]] +id = 1579384326 +value = 'name' + +[[translation]] +id = 3206337475 +value = 'version' + +[[translation]] +id = 1810056261 +value = 'new' + +[[translation]] +id = 1602912115 +value = 'source' + +[[translation]] +id = 2363381545 +value = 'type' + +[[translation]] +id = 3419504365 +value = 'downloader' diff --git a/internal/translations/files/lure.ru.toml b/internal/translations/files/lure.ru.toml new file mode 100644 index 0000000..534fb3c --- /dev/null +++ b/internal/translations/files/lure.ru.toml @@ -0,0 +1,151 @@ +[[translation]] +id = 1228660974 +value = 'Скачивание репозитория' + +[[translation]] +id = 2779805870 +value = 'Репозиторий уже обновлен' + +[[translation]] +id = 1433222829 +value = 'Показать скрипт для пакета' + +[[translation]] +id = 2470847050 +value = 'Не удалось предложить просмотреть скрипт' + +[[translation]] +id = 855659503 +value = 'Продолжить?' + +[[translation]] +id = 1997041569 +value = 'Пользователь решил не продолжать после просмотра скрипта' + +[[translation]] +id = 2347700990 +value = 'Сборка пакета' + +[[translation]] +id = 2105058868 +value = 'Скачивание файлов' + +[[translation]] +id = 1884485082 +value = 'Скачивание источника' + +[[translation]] +id = 1519177982 +value = 'Ошибка при сборке пакета' + +[[translation]] +id = 2125220917 +value = 'Выберите, какие пакеты установить' + +[[translation]] +id = 812531604 +value = 'Ошибка при запросе выбора пакета' + +[[translation]] +id = 1040982801 +value = 'Обновление версии' + +[[translation]] +id = 2235794125 +value = 'Удалить зависимости сборки?' + +[[translation]] +id = 2205430948 +value = 'Установка зависимостей сборки' + +[[translation]] +id = 2522710805 +value = 'Установка зависимостей' + +[[translation]] +id = 3602138206 +value = 'Ошибка при установке пакета' + +[[translation]] +id = 1057080231 +value = 'Вызов функции package()' + +[[translation]] +id = 2687735200 +value = 'Вызов функции prepare()' + +[[translation]] +id = 535572372 +value = 'Вызов функции version()' + +[[translation]] +id = 436644691 +value = 'Вызов функции build()' + +[[translation]] +id = 2562049386 +value = "Архитектура процессора вашей системы не соответствует этому пакету. Продолжать несмотря на это?" + +[[translation]] +id = 3759891273 +value = 'Функция package() необходима' + +[[translation]] +id = 4006393493 +value = 'Массив checksums должен быть той же длины, что и sources' + +[[translation]] +id = 1393316459 +value = 'Этот пакет уже установлен' + +[[translation]] +id = 1267660189 +value = 'Источник может быть обновлен, если требуется, обновляем' + +[[translation]] +id = 21753247 +value = 'Источник найден в кэше' + +[[translation]] +id = 257354570 +value = 'Сжатие пакета' + +[[translation]] +id = 2952487371 +value = 'Создание метаданных пакета' + +[[translation]] +id = 3121791194 +value = 'Запуск LURE от имени root запрещен, так как это может привести к катастрофическому повреждению вашей системы' + +[[translation]] +id = 1256604213 +value = 'Ожидание метаданных торрента' + +[[translation]] +id = 432261354 +value = 'Скачивание торрент-файла' + +[[translation]] +id = 1579384326 +value = 'название' + +[[translation]] +id = 3206337475 +value = 'версия' + +[[translation]] +id = 1810056261 +value = 'новая' + +[[translation]] +id = 1602912115 +value = 'источник' + +[[translation]] +id = 2363381545 +value = 'вид' + +[[translation]] +id = 3419504365 +value = 'протокол-скачивание' \ No newline at end of file diff --git a/internal/translations/translations.go b/internal/translations/translations.go new file mode 100644 index 0000000..3a6c644 --- /dev/null +++ b/internal/translations/translations.go @@ -0,0 +1,56 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package translations + +import ( + "context" + "embed" + "sync" + + "go.elara.ws/logger" + "lure.sh/lure/pkg/loggerctx" + "go.elara.ws/translate" + "golang.org/x/text/language" +) + +//go:embed files +var translationFS embed.FS + +var ( + mu sync.Mutex + translator *translate.Translator +) + +func Translator(ctx context.Context) *translate.Translator { + mu.Lock() + defer mu.Unlock() + log := loggerctx.From(ctx) + if translator == nil { + t, err := translate.NewFromFS(translationFS) + if err != nil { + log.Fatal("Error creating new translator").Err(err).Send() + } + translator = &t + } + return translator +} + +func NewLogger(ctx context.Context, l logger.Logger, lang language.Tag) *translate.TranslatedLogger { + return translate.NewLogger(l, *Translator(ctx), lang) +} diff --git a/internal/types/build.go b/internal/types/build.go new file mode 100644 index 0000000..d658606 --- /dev/null +++ b/internal/types/build.go @@ -0,0 +1,70 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package types + +import "lure.sh/lure/pkg/manager" + +type BuildOpts struct { + Script string + Manager manager.Manager + Clean bool + Interactive bool +} + +// BuildVars represents the script variables required +// to build a package +type BuildVars struct { + Name string `sh:"name,required"` + Version string `sh:"version,required"` + Release int `sh:"release,required"` + Epoch uint `sh:"epoch"` + Description string `sh:"desc"` + Homepage string `sh:"homepage"` + Maintainer string `sh:"maintainer"` + Architectures []string `sh:"architectures"` + Licenses []string `sh:"license"` + Provides []string `sh:"provides"` + Conflicts []string `sh:"conflicts"` + Depends []string `sh:"deps"` + BuildDepends []string `sh:"build_deps"` + OptDepends []string `sh:"opt_deps"` + Replaces []string `sh:"replaces"` + Sources []string `sh:"sources"` + Checksums []string `sh:"checksums"` + Backup []string `sh:"backup"` + Scripts Scripts `sh:"scripts"` +} + +type Scripts struct { + PreInstall string `sh:"preinstall"` + PostInstall string `sh:"postinstall"` + PreRemove string `sh:"preremove"` + PostRemove string `sh:"postremove"` + PreUpgrade string `sh:"preupgrade"` + PostUpgrade string `sh:"postupgrade"` + PreTrans string `sh:"pretrans"` + PostTrans string `sh:"posttrans"` +} + +type Directories struct { + BaseDir string + SrcDir string + PkgDir string + ScriptDir string +} diff --git a/internal/types/config.go b/internal/types/config.go new file mode 100644 index 0000000..b2c2603 --- /dev/null +++ b/internal/types/config.go @@ -0,0 +1,38 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package types + +// Config represents the LURE configuration file +type Config struct { + RootCmd string `toml:"rootCmd"` + PagerStyle string `toml:"pagerStyle"` + IgnorePkgUpdates []string `toml:"ignorePkgUpdates"` + Repos []Repo `toml:"repo"` + Unsafe Unsafe `toml:"unsafe"` +} + +// Repo represents a LURE repo within a configuration file +type Repo struct { + Name string `toml:"name"` + URL string `toml:"url"` +} + +type Unsafe struct { + AllowRunAsRoot bool `toml:"allowRunAsRoot"` +} diff --git a/internal/types/repo.go b/internal/types/repo.go new file mode 100644 index 0000000..c6c9232 --- /dev/null +++ b/internal/types/repo.go @@ -0,0 +1,26 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package types + +// RepoConfig represents a LURE repo's lure-repo.toml file. +type RepoConfig struct { + Repo struct { + MinVersion string `toml:"minVersion"` + } +} diff --git a/list.go b/list.go new file mode 100644 index 0000000..017e416 --- /dev/null +++ b/list.go @@ -0,0 +1,108 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/manager" + "lure.sh/lure/pkg/repos" + "golang.org/x/exp/slices" +) + +var listCmd = &cli.Command{ + Name: "list", + Usage: "List LURE repo packages", + Aliases: []string{"ls"}, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "installed", + Aliases: []string{"I"}, + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + err := repos.Pull(ctx, config.Config(ctx).Repos) + if err != nil { + log.Fatal("Error pulling repositories").Err(err).Send() + } + + where := "true" + args := []any(nil) + if c.NArg() > 0 { + where = "name LIKE ? OR json_array_contains(provides, ?)" + args = []any{c.Args().First(), c.Args().First()} + } + + result, err := db.GetPkgs(ctx, where, args...) + if err != nil { + log.Fatal("Error getting packages").Err(err).Send() + } + defer result.Close() + + var installed map[string]string + if c.Bool("installed") { + mgr := manager.Detect() + if mgr == nil { + log.Fatal("Unable to detect a supported package manager on the system").Send() + } + + installed, err = mgr.ListInstalled(&manager.Opts{AsRoot: false}) + if err != nil { + log.Fatal("Error listing installed packages").Err(err).Send() + } + } + + for result.Next() { + var pkg db.Package + err := result.StructScan(&pkg) + if err != nil { + return err + } + + if slices.Contains(config.Config(ctx).IgnorePkgUpdates, pkg.Name) { + continue + } + + version := pkg.Version + if c.Bool("installed") { + instVersion, ok := installed[pkg.Name] + if !ok { + continue + } else { + version = instVersion + } + } + + fmt.Printf("%s/%s %s\n", pkg.Repository, pkg.Name, version) + } + + if err != nil { + log.Fatal("Error iterating over packages").Err(err).Send() + } + + return nil + }, +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..41811ff --- /dev/null +++ b/main.go @@ -0,0 +1,115 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "context" + "os" + "os/signal" + "strings" + "syscall" + + "github.com/mattn/go-isatty" + "github.com/urfave/cli/v2" + "go.elara.ws/logger" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/translations" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/manager" +) + +var app = &cli.App{ + Name: "lure", + Usage: "Linux User REpository", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "pm-args", + Aliases: []string{"P"}, + Usage: "Arguments to be passed on to the package manager", + }, + &cli.BoolFlag{ + Name: "interactive", + Aliases: []string{"i"}, + Value: isatty.IsTerminal(os.Stdin.Fd()), + Usage: "Enable interactive questions and prompts", + }, + }, + Commands: []*cli.Command{ + installCmd, + removeCmd, + upgradeCmd, + infoCmd, + listCmd, + buildCmd, + addrepoCmd, + removerepoCmd, + refreshCmd, + fixCmd, + genCmd, + helperCmd, + versionCmd, + }, + Before: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + cmd := c.Args().First() + if cmd != "helper" && !config.Config(ctx).Unsafe.AllowRunAsRoot && os.Geteuid() == 0 { + log.Fatal("Running LURE as root is forbidden as it may cause catastrophic damage to your system").Send() + } + + if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" { + args := strings.Split(trimmed, " ") + manager.Args = append(manager.Args, args...) + } + + return nil + }, + After: func(ctx *cli.Context) error { + return db.Close() + }, + EnableBashCompletion: true, +} + +var versionCmd = &cli.Command{ + Name: "version", + Usage: "Print the current LURE version and exit", + Action: func(ctx *cli.Context) error { + println(config.Version) + return nil + }, +} + +func main() { + ctx := context.Background() + log := translations.NewLogger(ctx, logger.NewCLI(os.Stderr), config.Language(ctx)) + ctx = loggerctx.With(ctx, log) + + // Set the root command to the one set in the LURE config + manager.DefaultRootCmd = config.Config(ctx).RootCmd + + ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + err := app.RunContext(ctx, os.Args) + if err != nil { + log.Error("Error while running app").Err(err).Send() + } +} diff --git a/pkg/build/build.go b/pkg/build/build.go new file mode 100644 index 0000000..c42d041 --- /dev/null +++ b/pkg/build/build.go @@ -0,0 +1,838 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package build + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "slices" + "strconv" + "strings" + "time" + + _ "github.com/goreleaser/nfpm/v2/apk" + _ "github.com/goreleaser/nfpm/v2/arch" + _ "github.com/goreleaser/nfpm/v2/deb" + _ "github.com/goreleaser/nfpm/v2/rpm" + + "github.com/goreleaser/nfpm/v2" + "github.com/goreleaser/nfpm/v2/files" + "lure.sh/lure/internal/cliutils" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/cpu" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/dl" + "lure.sh/lure/internal/shutils/decoder" + "lure.sh/lure/internal/shutils/handlers" + "lure.sh/lure/internal/shutils/helpers" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/distro" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/manager" + "lure.sh/lure/pkg/repos" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +// BuildPackage builds the script at the given path. It returns two slices. One contains the paths +// to the built package(s), the other contains the names of the built package(s). +func BuildPackage(ctx context.Context, opts types.BuildOpts) ([]string, []string, error) { + log := loggerctx.From(ctx) + + info, err := distro.ParseOSRelease(ctx) + if err != nil { + return nil, nil, err + } + + fl, err := parseScript(info, opts.Script) + if err != nil { + return nil, nil, err + } + + // The first pass is just used to get variable values and runs before + // the script is displayed, so it's restricted so as to prevent malicious + // code from executing. + vars, err := executeFirstPass(ctx, info, fl, opts.Script) + if err != nil { + return nil, nil, err + } + + dirs := getDirs(ctx, vars, opts.Script) + + // If opts.Clean isn't set and we find the package already built, + // just return it rather than rebuilding + if !opts.Clean { + builtPkgPath, ok, err := checkForBuiltPackage(opts.Manager, vars, getPkgFormat(opts.Manager), dirs.BaseDir) + if err != nil { + return nil, nil, err + } + + if ok { + return []string{builtPkgPath}, nil, err + } + } + + // Ask the user if they'd like to see the build script + err = cliutils.PromptViewScript(ctx, opts.Script, vars.Name, config.Config(ctx).PagerStyle, opts.Interactive) + if err != nil { + log.Fatal("Failed to prompt user to view build script").Err(err).Send() + } + + log.Info("Building package").Str("name", vars.Name).Str("version", vars.Version).Send() + + // The second pass will be used to execute the actual code, + // so it's unrestricted. The script has already been displayed + // to the user by this point, so it should be safe + dec, err := executeSecondPass(ctx, info, fl, dirs) + if err != nil { + return nil, nil, err + } + + // Get the installed packages on the system + installed, err := opts.Manager.ListInstalled(nil) + if err != nil { + return nil, nil, err + } + + cont, err := performChecks(ctx, vars, opts.Interactive, installed) + if err != nil { + return nil, nil, err + } else if !cont { + os.Exit(1) + } + + // Prepare the directories for building + err = prepareDirs(dirs) + if err != nil { + return nil, nil, err + } + + buildDeps, err := installBuildDeps(ctx, vars, opts, installed) + if err != nil { + return nil, nil, err + } + + err = installOptDeps(ctx, vars, opts, installed) + if err != nil { + return nil, nil, err + } + + builtPaths, builtNames, repoDeps, err := buildLUREDeps(ctx, opts, vars) + if err != nil { + return nil, nil, err + } + + log.Info("Downloading sources").Send() + + err = getSources(ctx, dirs, vars) + if err != nil { + return nil, nil, err + } + + err = executeFunctions(ctx, dec, dirs, vars) + if err != nil { + return nil, nil, err + } + + log.Info("Building package metadata").Str("name", vars.Name).Send() + + pkgFormat := getPkgFormat(opts.Manager) + + pkgInfo, err := buildPkgMetadata(vars, dirs, pkgFormat, append(repoDeps, builtNames...)) + if err != nil { + return nil, nil, err + } + + packager, err := nfpm.Get(pkgFormat) + if err != nil { + return nil, nil, err + } + + pkgName := packager.ConventionalFileName(pkgInfo) + pkgPath := filepath.Join(dirs.BaseDir, pkgName) + + pkgFile, err := os.Create(pkgPath) + if err != nil { + return nil, nil, err + } + + log.Info("Compressing package").Str("name", pkgName).Send() + + err = packager.Package(pkgInfo, pkgFile) + if err != nil { + return nil, nil, err + } + + err = removeBuildDeps(ctx, buildDeps, opts) + if err != nil { + return nil, nil, err + } + + // Add the path and name of the package we just built to the + // appropriate slices + pkgPaths := append(builtPaths, pkgPath) + pkgNames := append(builtNames, vars.Name) + + // Remove any duplicates from the pkgPaths and pkgNames. + // Duplicates can be introduced if several of the dependencies + // depend on the same packages. + pkgPaths = removeDuplicates(pkgPaths) + pkgNames = removeDuplicates(pkgNames) + + return pkgPaths, pkgNames, nil +} + +// parseScript parses the build script using the built-in bash implementation +func parseScript(info *distro.OSRelease, script string) (*syntax.File, error) { + fl, err := os.Open(script) + if err != nil { + return nil, err + } + defer fl.Close() + + file, err := syntax.NewParser().Parse(fl, "lure.sh") + if err != nil { + return nil, err + } + + return file, nil +} + +// executeFirstPass executes the parsed script in a restricted environment +// to extract the build variables without executing any actual code. +func executeFirstPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, script string) (*types.BuildVars, error) { + scriptDir := filepath.Dir(script) + env := createBuildEnvVars(info, types.Directories{ScriptDir: scriptDir}) + + runner, err := interp.New( + interp.Env(expand.ListEnviron(env...)), + interp.StdIO(os.Stdin, os.Stdout, os.Stderr), + interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)), + interp.ReadDirHandler(handlers.RestrictedReadDir(scriptDir)), + interp.StatHandler(handlers.RestrictedStat(scriptDir)), + interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), + ) + if err != nil { + return nil, err + } + + err = runner.Run(ctx, fl) + if err != nil { + return nil, err + } + + dec := decoder.New(info, runner) + + var vars types.BuildVars + err = dec.DecodeVars(&vars) + if err != nil { + return nil, err + } + + return &vars, nil +} + +// getDirs returns the appropriate directories for the script +func getDirs(ctx context.Context, vars *types.BuildVars, script string) types.Directories { + baseDir := filepath.Join(config.GetPaths(ctx).PkgsDir, vars.Name) + return types.Directories{ + BaseDir: baseDir, + SrcDir: filepath.Join(baseDir, "src"), + PkgDir: filepath.Join(baseDir, "pkg"), + ScriptDir: filepath.Dir(script), + } +} + +// executeSecondPass executes the build script for the second time, this time without any restrictions. +// It returns a decoder that can be used to retrieve functions and variables from the script. +func executeSecondPass(ctx context.Context, info *distro.OSRelease, fl *syntax.File, dirs types.Directories) (*decoder.Decoder, error) { + env := createBuildEnvVars(info, dirs) + + fakeroot := handlers.FakerootExecHandler(2 * time.Second) + runner, err := interp.New( + interp.Env(expand.ListEnviron(env...)), + interp.StdIO(os.Stdin, os.Stdout, os.Stderr), + interp.ExecHandler(helpers.Helpers.ExecHandler(fakeroot)), + ) + if err != nil { + return nil, err + } + + err = runner.Run(ctx, fl) + if err != nil { + return nil, err + } + + return decoder.New(info, runner), nil +} + +// prepareDirs prepares the directories for building. +func prepareDirs(dirs types.Directories) error { + err := os.RemoveAll(dirs.BaseDir) + if err != nil { + return err + } + err = os.MkdirAll(dirs.SrcDir, 0o755) + if err != nil { + return err + } + return os.MkdirAll(dirs.PkgDir, 0o755) +} + +// performChecks checks various things on the system to ensure that the package can be installed. +func performChecks(ctx context.Context, vars *types.BuildVars, interactive bool, installed map[string]string) (bool, error) { + log := loggerctx.From(ctx) + if !cpu.IsCompatibleWith(cpu.Arch(), vars.Architectures) { + cont, err := cliutils.YesNoPrompt(ctx, "Your system's CPU architecture doesn't match this package. Do you want to build anyway?", interactive, true) + if err != nil { + return false, err + } + + if !cont { + return false, nil + } + } + + if instVer, ok := installed[vars.Name]; ok { + log.Warn("This package is already installed"). + Str("name", vars.Name). + Str("version", instVer). + Send() + } + + return true, nil +} + +// installBuildDeps installs any build dependencies that aren't already installed and returns +// a slice containing the names of all the packages it installed. +func installBuildDeps(ctx context.Context, vars *types.BuildVars, opts types.BuildOpts, installed map[string]string) ([]string, error) { + log := loggerctx.From(ctx) + var buildDeps []string + if len(vars.BuildDepends) > 0 { + found, notFound, err := repos.FindPkgs(ctx, vars.BuildDepends) + if err != nil { + return nil, err + } + + found = removeAlreadyInstalled(found, installed) + + log.Info("Installing build dependencies").Send() + + flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) + buildDeps = packageNames(flattened) + InstallPkgs(ctx, flattened, notFound, opts) + } + return buildDeps, nil +} + +// installOptDeps asks the user which, if any, optional dependencies they want to install. +// If the user chooses to install any optional dependencies, it performs the installation. +func installOptDeps(ctx context.Context, vars *types.BuildVars, opts types.BuildOpts, installed map[string]string) error { + if len(vars.OptDepends) > 0 { + optDeps, err := cliutils.ChooseOptDepends(ctx, vars.OptDepends, "install", opts.Interactive) + if err != nil { + return err + } + + if len(optDeps) == 0 { + return nil + } + + found, notFound, err := repos.FindPkgs(ctx, optDeps) + if err != nil { + return err + } + + found = removeAlreadyInstalled(found, installed) + flattened := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) + InstallPkgs(ctx, flattened, notFound, opts) + } + return nil +} + +// buildLUREDeps builds all the LURE dependencies of the package. It returns the paths and names +// of the packages it built, as well as all the dependencies it didn't find in the LURE repo so +// they can be installed from the system repos. +func buildLUREDeps(ctx context.Context, opts types.BuildOpts, vars *types.BuildVars) (builtPaths, builtNames, repoDeps []string, err error) { + log := loggerctx.From(ctx) + if len(vars.Depends) > 0 { + log.Info("Installing dependencies").Send() + + found, notFound, err := repos.FindPkgs(ctx, vars.Depends) + if err != nil { + return nil, nil, nil, err + } + repoDeps = notFound + + // If there are multiple options for some packages, flatten them all into a single slice + pkgs := cliutils.FlattenPkgs(ctx, found, "install", opts.Interactive) + scripts := GetScriptPaths(ctx, pkgs) + for _, script := range scripts { + newOpts := opts + newOpts.Script = script + + // Build the dependency + pkgPaths, pkgNames, err := BuildPackage(ctx, newOpts) + if err != nil { + return nil, nil, nil, err + } + + // Append the paths of all the built packages to builtPaths + builtPaths = append(builtPaths, pkgPaths...) + // Append the names of all the built packages to builtNames + builtNames = append(builtNames, pkgNames...) + // Append the name of the current package to builtNames + builtNames = append(builtNames, filepath.Base(filepath.Dir(script))) + } + } + + // Remove any potential duplicates, which can be introduced if + // several of the dependencies depend on the same packages. + repoDeps = removeDuplicates(repoDeps) + builtPaths = removeDuplicates(builtPaths) + builtNames = removeDuplicates(builtNames) + return builtPaths, builtNames, repoDeps, nil +} + +// executeFunctions executes the special LURE functions, such as version(), prepare(), etc. +func executeFunctions(ctx context.Context, dec *decoder.Decoder, dirs types.Directories, vars *types.BuildVars) (err error) { + log := loggerctx.From(ctx) + version, ok := dec.GetFunc("version") + if ok { + log.Info("Executing version()").Send() + + buf := &bytes.Buffer{} + + err = version( + ctx, + interp.Dir(dirs.SrcDir), + interp.StdIO(os.Stdin, buf, os.Stderr), + ) + if err != nil { + return err + } + + newVer := strings.TrimSpace(buf.String()) + err = setVersion(ctx, dec.Runner, newVer) + if err != nil { + return err + } + vars.Version = newVer + + log.Info("Updating version").Str("new", newVer).Send() + } + + prepare, ok := dec.GetFunc("prepare") + if ok { + log.Info("Executing prepare()").Send() + + err = prepare(ctx, interp.Dir(dirs.SrcDir)) + if err != nil { + return err + } + } + + build, ok := dec.GetFunc("build") + if ok { + log.Info("Executing build()").Send() + + err = build(ctx, interp.Dir(dirs.SrcDir)) + if err != nil { + return err + } + } + + packageFn, ok := dec.GetFunc("package") + if ok { + log.Info("Executing package()").Send() + + err = packageFn(ctx, interp.Dir(dirs.SrcDir)) + if err != nil { + return err + } + } else { + log.Fatal("The package() function is required").Send() + } + + return nil +} + +// buildPkgMetadata builds the metadata for the package that's going to be built. +func buildPkgMetadata(vars *types.BuildVars, dirs types.Directories, pkgFormat string, deps []string) (*nfpm.Info, error) { + pkgInfo := &nfpm.Info{ + Name: vars.Name, + Description: vars.Description, + Arch: cpu.Arch(), + Platform: "linux", + Version: vars.Version, + Release: strconv.Itoa(vars.Release), + Homepage: vars.Homepage, + License: strings.Join(vars.Licenses, ", "), + Maintainer: vars.Maintainer, + Overridables: nfpm.Overridables{ + Conflicts: vars.Conflicts, + Replaces: vars.Replaces, + Provides: vars.Provides, + Depends: deps, + }, + } + + if pkgFormat == "apk" { + // Alpine refuses to install packages that provide themselves, so remove any such provides + pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool { + return s == pkgInfo.Name + }) + } + + if vars.Epoch != 0 { + pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10) + } + + setScripts(vars, pkgInfo, dirs.ScriptDir) + + if slices.Contains(vars.Architectures, "all") { + pkgInfo.Arch = "all" + } + + contents, err := buildContents(vars, dirs) + if err != nil { + return nil, err + } + pkgInfo.Overridables.Contents = contents + + return pkgInfo, nil +} + +// buildContents builds the contents section of the package, which contains the files +// that will be placed into the final package. +func buildContents(vars *types.BuildVars, dirs types.Directories) ([]*files.Content, error) { + contents := []*files.Content{} + err := filepath.Walk(dirs.PkgDir, func(path string, fi os.FileInfo, err error) error { + trimmed := strings.TrimPrefix(path, dirs.PkgDir) + + if fi.IsDir() { + f, err := os.Open(path) + if err != nil { + return err + } + + // If the directory is empty, skip it + _, err = f.Readdirnames(1) + if err != io.EOF { + return nil + } + + contents = append(contents, &files.Content{ + Source: path, + Destination: trimmed, + Type: "dir", + FileInfo: &files.ContentFileInfo{ + MTime: fi.ModTime(), + }, + }) + + return f.Close() + } + + if fi.Mode()&os.ModeSymlink != 0 { + link, err := os.Readlink(path) + if err != nil { + return err + } + // Remove pkgdir from the symlink's path + link = strings.TrimPrefix(link, dirs.PkgDir) + + contents = append(contents, &files.Content{ + Source: link, + Destination: trimmed, + Type: "symlink", + FileInfo: &files.ContentFileInfo{ + MTime: fi.ModTime(), + Mode: fi.Mode(), + }, + }) + + return nil + } + + fileContent := &files.Content{ + Source: path, + Destination: trimmed, + FileInfo: &files.ContentFileInfo{ + MTime: fi.ModTime(), + Mode: fi.Mode(), + Size: fi.Size(), + }, + } + + // If the file is supposed to be backed up, set its type to config|noreplace + if slices.Contains(vars.Backup, trimmed) { + fileContent.Type = "config|noreplace" + } + + contents = append(contents, fileContent) + + return nil + }) + return contents, err +} + +// removeBuildDeps asks the user if they'd like to remove the build dependencies that were +// installed by installBuildDeps. If so, it uses the package manager to do that. +func removeBuildDeps(ctx context.Context, buildDeps []string, opts types.BuildOpts) error { + if len(buildDeps) > 0 { + remove, err := cliutils.YesNoPrompt(ctx, "Would you like to remove the build dependencies?", opts.Interactive, false) + if err != nil { + return err + } + + if remove { + err = opts.Manager.Remove( + &manager.Opts{ + AsRoot: true, + NoConfirm: true, + }, + buildDeps..., + ) + if err != nil { + return err + } + } + } + return nil +} + +// checkForBuiltPackage tries to detect a previously-built package and returns its path +// and true if it finds one. If it doesn't find it, it returns "", false, nil. +func checkForBuiltPackage(mgr manager.Manager, vars *types.BuildVars, pkgFormat, baseDir string) (string, bool, error) { + filename, err := pkgFileName(vars, pkgFormat) + if err != nil { + return "", false, err + } + + pkgPath := filepath.Join(baseDir, filename) + + _, err = os.Stat(pkgPath) + if err != nil { + return "", false, nil + } + + return pkgPath, true, nil +} + +// pkgFileName returns the filename of the package if it were to be built. +// This is used to check if the package has already been built. +func pkgFileName(vars *types.BuildVars, pkgFormat string) (string, error) { + pkgInfo := &nfpm.Info{ + Name: vars.Name, + Arch: cpu.Arch(), + Version: vars.Version, + Release: strconv.Itoa(vars.Release), + Epoch: strconv.FormatUint(uint64(vars.Epoch), 10), + } + + packager, err := nfpm.Get(pkgFormat) + if err != nil { + return "", err + } + + return packager.ConventionalFileName(pkgInfo), nil +} + +// getPkgFormat returns the package format of the package manager, +// or LURE_PKG_FORMAT if that's set. +func getPkgFormat(mgr manager.Manager) string { + pkgFormat := mgr.Format() + if format, ok := os.LookupEnv("LURE_PKG_FORMAT"); ok { + pkgFormat = format + } + return pkgFormat +} + +// createBuildEnvVars creates the environment variables that will be set in the +// build script when it's executed. +func createBuildEnvVars(info *distro.OSRelease, dirs types.Directories) []string { + env := os.Environ() + + env = append( + env, + "DISTRO_NAME="+info.Name, + "DISTRO_PRETTY_NAME="+info.PrettyName, + "DISTRO_ID="+info.ID, + "DISTRO_VERSION_ID="+info.VersionID, + "DISTRO_ID_LIKE="+strings.Join(info.Like, " "), + "ARCH="+cpu.Arch(), + "NCPU="+strconv.Itoa(runtime.NumCPU()), + ) + + if dirs.ScriptDir != "" { + env = append(env, "scriptdir="+dirs.ScriptDir) + } + + if dirs.PkgDir != "" { + env = append(env, "pkgdir="+dirs.PkgDir) + } + + if dirs.SrcDir != "" { + env = append(env, "srcdir="+dirs.SrcDir) + } + + return env +} + +// getSources downloads the sources from the script. +func getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars) error { + log := loggerctx.From(ctx) + if len(bv.Sources) != len(bv.Checksums) { + log.Fatal("The checksums array must be the same length as sources").Send() + } + + for i, src := range bv.Sources { + opts := dl.Options{ + Name: fmt.Sprintf("%s[%d]", bv.Name, i), + URL: src, + Destination: dirs.SrcDir, + Progress: os.Stderr, + LocalDir: dirs.ScriptDir, + } + + if !strings.EqualFold(bv.Checksums[i], "SKIP") { + // If the checksum contains a colon, use the part before the colon + // as the algorithm and the part after as the actual checksum. + // Otherwise, use the default sha256 with the whole string as the checksum. + algo, hashData, ok := strings.Cut(bv.Checksums[i], ":") + if ok { + checksum, err := hex.DecodeString(hashData) + if err != nil { + return err + } + opts.Hash = checksum + opts.HashAlgorithm = algo + } else { + checksum, err := hex.DecodeString(bv.Checksums[i]) + if err != nil { + return err + } + opts.Hash = checksum + } + } + + err := dl.Download(ctx, opts) + if err != nil { + return err + } + } + + return nil +} + +// setScripts adds any hook scripts to the package metadata. +func setScripts(vars *types.BuildVars, info *nfpm.Info, scriptDir string) { + if vars.Scripts.PreInstall != "" { + info.Scripts.PreInstall = filepath.Join(scriptDir, vars.Scripts.PreInstall) + } + + if vars.Scripts.PostInstall != "" { + info.Scripts.PostInstall = filepath.Join(scriptDir, vars.Scripts.PostInstall) + } + + if vars.Scripts.PreRemove != "" { + info.Scripts.PreRemove = filepath.Join(scriptDir, vars.Scripts.PreRemove) + } + + if vars.Scripts.PostRemove != "" { + info.Scripts.PostRemove = filepath.Join(scriptDir, vars.Scripts.PostRemove) + } + + if vars.Scripts.PreUpgrade != "" { + info.ArchLinux.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade) + info.APK.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade) + } + + if vars.Scripts.PostUpgrade != "" { + info.ArchLinux.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade) + info.APK.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade) + } + + if vars.Scripts.PreTrans != "" { + info.RPM.Scripts.PreTrans = filepath.Join(scriptDir, vars.Scripts.PreTrans) + } + + if vars.Scripts.PostTrans != "" { + info.RPM.Scripts.PostTrans = filepath.Join(scriptDir, vars.Scripts.PostTrans) + } +} + +// setVersion changes the version variable in the script runner. +// It's used to set the version to the output of the version() function. +func setVersion(ctx context.Context, r *interp.Runner, to string) error { + fl, err := syntax.NewParser().Parse(strings.NewReader("version='"+to+"'"), "") + if err != nil { + return err + } + return r.Run(ctx, fl) +} + +// removeAlreadyInstalled returns a map without any dependencies that are already installed +func removeAlreadyInstalled(found map[string][]db.Package, installed map[string]string) map[string][]db.Package { + filteredPackages := make(map[string][]db.Package) + + for name, pkgList := range found { + filteredPkgList := []db.Package{} + for _, pkg := range pkgList { + if _, isInstalled := installed[pkg.Name]; !isInstalled { + filteredPkgList = append(filteredPkgList, pkg) + } + } + filteredPackages[name] = filteredPkgList + } + + return filteredPackages +} + +// packageNames returns the names of all the given packages +func packageNames(pkgs []db.Package) []string { + names := make([]string, len(pkgs)) + for i, p := range pkgs { + names[i] = p.Name + } + return names +} + +// removeDuplicates removes any duplicates from the given slice +func removeDuplicates(slice []string) []string { + seen := map[string]struct{}{} + result := []string{} + + for _, s := range slice { + if _, ok := seen[s]; !ok { + seen[s] = struct{}{} + result = append(result, s) + } + } + + return result +} diff --git a/pkg/build/install.go b/pkg/build/install.go new file mode 100644 index 0000000..e862ca9 --- /dev/null +++ b/pkg/build/install.go @@ -0,0 +1,72 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package build + +import ( + "context" + "path/filepath" + + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/loggerctx" +) + +// InstallPkgs installs native packages via the package manager, +// then builds and installs the LURE packages +func InstallPkgs(ctx context.Context, lurePkgs []db.Package, nativePkgs []string, opts types.BuildOpts) { + log := loggerctx.From(ctx) + + if len(nativePkgs) > 0 { + err := opts.Manager.Install(nil, nativePkgs...) + if err != nil { + log.Fatal("Error installing native packages").Err(err).Send() + } + } + + InstallScripts(ctx, GetScriptPaths(ctx, lurePkgs), opts) +} + +// GetScriptPaths returns a slice of script paths corresponding to the +// given packages +func GetScriptPaths(ctx context.Context, pkgs []db.Package) []string { + var scripts []string + for _, pkg := range pkgs { + scriptPath := filepath.Join(config.GetPaths(ctx).RepoDir, pkg.Repository, pkg.Name, "lure.sh") + scripts = append(scripts, scriptPath) + } + return scripts +} + +// InstallScripts builds and installs the given LURE build scripts +func InstallScripts(ctx context.Context, scripts []string, opts types.BuildOpts) { + log := loggerctx.From(ctx) + for _, script := range scripts { + opts.Script = script + builtPkgs, _, err := BuildPackage(ctx, opts) + if err != nil { + log.Fatal("Error building package").Err(err).Send() + } + + err = opts.Manager.InstallLocal(nil, builtPkgs...) + if err != nil { + log.Fatal("Error installing package").Err(err).Send() + } + } +} diff --git a/pkg/distro/osrelease.go b/pkg/distro/osrelease.go new file mode 100644 index 0000000..3a5f6a7 --- /dev/null +++ b/pkg/distro/osrelease.go @@ -0,0 +1,118 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package distro + +import ( + "context" + "os" + "strings" + + "lure.sh/lure/internal/shutils/handlers" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +// OSRelease contains information from an os-release file +type OSRelease struct { + Name string + PrettyName string + ID string + Like []string + VersionID string + ANSIColor string + HomeURL string + DocumentationURL string + SupportURL string + BugReportURL string + Logo string +} + +var parsed *OSRelease + +// OSReleaseName returns a struct parsed from the system's os-release +// file. It checks /etc/os-release as well as /usr/lib/os-release. +// The first time it's called, it'll parse the os-release file. +// Subsequent calls will return the same value. +func ParseOSRelease(ctx context.Context) (*OSRelease, error) { + if parsed != nil { + return parsed, nil + } + + fl, err := os.Open("/usr/lib/os-release") + if err != nil { + fl, err = os.Open("/etc/os-release") + if err != nil { + return nil, err + } + } + + file, err := syntax.NewParser().Parse(fl, "/usr/lib/os-release") + if err != nil { + return nil, err + } + + fl.Close() + + // Create new shell interpreter with nop open, exec, readdir, and stat handlers + // as well as no environment variables in order to prevent vulnerabilities + // caused by changing the os-release file. + runner, err := interp.New( + interp.OpenHandler(handlers.NopOpen), + interp.ExecHandler(handlers.NopExec), + interp.ReadDirHandler(handlers.NopReadDir), + interp.StatHandler(handlers.NopStat), + interp.Env(expand.ListEnviron()), + ) + if err != nil { + return nil, err + } + + err = runner.Run(ctx, file) + if err != nil { + return nil, err + } + + out := &OSRelease{ + Name: runner.Vars["NAME"].Str, + PrettyName: runner.Vars["PRETTY_NAME"].Str, + ID: runner.Vars["ID"].Str, + VersionID: runner.Vars["VERSION_ID"].Str, + ANSIColor: runner.Vars["ANSI_COLOR"].Str, + HomeURL: runner.Vars["HOME_URL"].Str, + DocumentationURL: runner.Vars["DOCUMENTATION_URL"].Str, + SupportURL: runner.Vars["SUPPORT_URL"].Str, + BugReportURL: runner.Vars["BUG_REPORT_URL"].Str, + Logo: runner.Vars["LOGO"].Str, + } + + distroUpdated := false + if distID, ok := os.LookupEnv("LURE_DISTRO"); ok { + out.ID = distID + } + + if distLike, ok := os.LookupEnv("LURE_DISTRO_LIKE"); ok { + out.Like = strings.Split(distLike, " ") + } else if runner.Vars["ID_LIKE"].IsSet() && !distroUpdated { + out.Like = strings.Split(runner.Vars["ID_LIKE"].Str, " ") + } + + parsed = out + return out, nil +} diff --git a/pkg/gen/funcs.go b/pkg/gen/funcs.go new file mode 100644 index 0000000..c0b6c51 --- /dev/null +++ b/pkg/gen/funcs.go @@ -0,0 +1,13 @@ +package gen + +import ( + "strings" + "text/template" +) + +var funcs = template.FuncMap{ + "tolower": strings.ToLower, + "firstchar": func(s string) string { + return s[:1] + }, +} diff --git a/pkg/gen/pip.go b/pkg/gen/pip.go new file mode 100644 index 0000000..6f8f984 --- /dev/null +++ b/pkg/gen/pip.go @@ -0,0 +1,84 @@ +package gen + +import ( + _ "embed" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "text/template" +) + +//go:embed tmpls/pip.tmpl.sh +var pipTmpl string + +type PipOptions struct { + Name string + Version string + Description string +} + +type pypiAPIResponse struct { + Info pypiInfo `json:"info"` + URLs []pypiURL `json:"urls"` +} + +func (res pypiAPIResponse) SourceURL() (pypiURL, error) { + for _, url := range res.URLs { + if url.PackageType == "sdist" { + return url, nil + } + } + return pypiURL{}, errors.New("package doesn't have a source distribution") +} + +type pypiInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Summary string `json:"summary"` + Homepage string `json:"home_page"` + License string `json:"license"` +} + +type pypiURL struct { + Digests map[string]string `json:"digests"` + Filename string `json:"filename"` + PackageType string `json:"packagetype"` +} + +func Pip(w io.Writer, opts PipOptions) error { + tmpl, err := template.New("pip"). + Funcs(funcs). + Parse(pipTmpl) + if err != nil { + return err + } + + url := fmt.Sprintf( + "https://pypi.org/pypi/%s/%s/json", + opts.Name, + opts.Version, + ) + + res, err := http.Get(url) + if err != nil { + return err + } + defer res.Body.Close() + if res.StatusCode != 200 { + return fmt.Errorf("pypi: %s", res.Status) + } + + var resp pypiAPIResponse + err = json.NewDecoder(res.Body).Decode(&resp) + if err != nil { + return err + } + + if opts.Description != "" { + resp.Info.Summary = opts.Description + } + + return tmpl.Execute(w, resp) +} diff --git a/pkg/gen/tmpls/pip.tmpl.sh b/pkg/gen/tmpls/pip.tmpl.sh new file mode 100644 index 0000000..326ab73 --- /dev/null +++ b/pkg/gen/tmpls/pip.tmpl.sh @@ -0,0 +1,31 @@ +name='{{.Info.Name | tolower}}' +version='{{.Info.Version}}' +release='1' +desc='{{.Info.Summary}}' +homepage='{{.Info.Homepage}}' +maintainer='Example ' +architectures=('all') +license=('{{if .Info.License | ne ""}}{{.Info.License}}{{else}}custom:Unknown{{end}}') +provides=('{{.Info.Name | tolower}}') +conflicts=('{{.Info.Name | tolower}}') + +deps=("python3") +deps_arch=("python") +deps_alpine=("python3") + +build_deps=("python3" "python3-setuptools") +build_deps_arch=("python" "python-setuptools") +build_deps_alpine=("python3" "py3-setuptools") + +sources=("https://files.pythonhosted.org/packages/source/{{.SourceURL.Filename | firstchar}}/{{.Info.Name}}/{{.SourceURL.Filename}}") +checksums=('blake2b-256:{{.SourceURL.Digests.blake2b_256}}') + +build() { + cd "$srcdir/{{.Info.Name}}-${version}" + python3 setup.py build +} + +package() { + cd "$srcdir/{{.Info.Name}}-${version}" + python3 setup.py install --root="${pkgdir}/" --optimize=1 || return 1 +} diff --git a/pkg/loggerctx/log.go b/pkg/loggerctx/log.go new file mode 100644 index 0000000..b948530 --- /dev/null +++ b/pkg/loggerctx/log.go @@ -0,0 +1,29 @@ +package loggerctx + +import ( + "context" + + "go.elara.ws/logger" +) + +// loggerCtxKey is used as the context key for loggers +type loggerCtxKey struct{} + +// With returns a copy of ctx containing log +func With(ctx context.Context, log logger.Logger) context.Context { + return context.WithValue(ctx, loggerCtxKey{}, log) +} + +// From attempts to get a logger from ctx. If ctx doesn't +// contain a logger, it returns a nop logger. +func From(ctx context.Context) logger.Logger { + if val := ctx.Value(loggerCtxKey{}); val != nil { + if log, ok := val.(logger.Logger); ok && log != nil { + return log + } else { + return logger.NewNop() + } + } else { + return logger.NewNop() + } +} diff --git a/pkg/manager/apk.go b/pkg/manager/apk.go new file mode 100644 index 0000000..5b5be40 --- /dev/null +++ b/pkg/manager/apk.go @@ -0,0 +1,166 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package manager + +import ( + "bufio" + "fmt" + "os/exec" + "strings" +) + +// APK represents the APK package manager +type APK struct { + rootCmd string +} + +func (*APK) Exists() bool { + _, err := exec.LookPath("apk") + return err == nil +} + +func (*APK) Name() string { + return "apk" +} + +func (*APK) Format() string { + return "apk" +} + +func (a *APK) SetRootCmd(s string) { + a.rootCmd = s +} + +func (a *APK) Sync(opts *Opts) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apk", "update") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apk: sync: %w", err) + } + return nil +} + +func (a *APK) Install(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apk", "add") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apk: install: %w", err) + } + return nil +} + +func (a *APK) InstallLocal(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apk", "add", "--allow-untrusted") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apk: installlocal: %w", err) + } + return nil +} + +func (a *APK) Remove(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apk", "del") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apk: remove: %w", err) + } + return nil +} + +func (a *APK) Upgrade(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apk", "upgrade") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apk: upgrade: %w", err) + } + return nil +} + +func (a *APK) UpgradeAll(opts *Opts) error { + opts = ensureOpts(opts) + return a.Upgrade(opts) +} + +func (a *APK) ListInstalled(opts *Opts) (map[string]string, error) { + out := map[string]string{} + cmd := exec.Command("apk", "list", "-I") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name, info, ok := strings.Cut(scanner.Text(), "-") + if !ok { + continue + } + + version, _, ok := strings.Cut(info, " ") + if !ok { + continue + } + + out[name] = version + } + + err = scanner.Err() + if err != nil { + return nil, err + } + + return out, nil +} + +func (a *APK) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { + var cmd *exec.Cmd + if opts.AsRoot { + cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd) + cmd.Args = append(cmd.Args, opts.Args...) + cmd.Args = append(cmd.Args, args...) + } else { + cmd = exec.Command(mgrCmd, args...) + } + + if !opts.NoConfirm { + cmd.Args = append(cmd.Args, "-i") + } + + return cmd +} diff --git a/pkg/manager/apt.go b/pkg/manager/apt.go new file mode 100644 index 0000000..36f7569 --- /dev/null +++ b/pkg/manager/apt.go @@ -0,0 +1,152 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package manager + +import ( + "bufio" + "fmt" + "os/exec" + "strings" +) + +// APT represents the APT package manager +type APT struct { + rootCmd string +} + +func (*APT) Exists() bool { + _, err := exec.LookPath("apt") + return err == nil +} + +func (*APT) Name() string { + return "apt" +} + +func (*APT) Format() string { + return "deb" +} + +func (a *APT) SetRootCmd(s string) { + a.rootCmd = s +} + +func (a *APT) Sync(opts *Opts) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apt", "update") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apt: sync: %w", err) + } + return nil +} + +func (a *APT) Install(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apt", "install") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apt: install: %w", err) + } + return nil +} + +func (a *APT) InstallLocal(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + return a.Install(opts, pkgs...) +} + +func (a *APT) Remove(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apt", "remove") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apt: remove: %w", err) + } + return nil +} + +func (a *APT) Upgrade(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + return a.Install(opts, pkgs...) +} + +func (a *APT) UpgradeAll(opts *Opts) error { + opts = ensureOpts(opts) + cmd := a.getCmd(opts, "apt", "upgrade") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("apt: upgradeall: %w", err) + } + return nil +} + +func (a *APT) ListInstalled(opts *Opts) (map[string]string, error) { + out := map[string]string{} + cmd := exec.Command("dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name, version, ok := strings.Cut(scanner.Text(), "\u200b") + if !ok { + continue + } + out[name] = version + } + + err = scanner.Err() + if err != nil { + return nil, err + } + + return out, nil +} + +func (a *APT) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { + var cmd *exec.Cmd + if opts.AsRoot { + cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd) + cmd.Args = append(cmd.Args, opts.Args...) + cmd.Args = append(cmd.Args, args...) + } else { + cmd = exec.Command(mgrCmd, args...) + } + + if opts.NoConfirm { + cmd.Args = append(cmd.Args, "-y") + } + + return cmd +} diff --git a/pkg/manager/dnf.go b/pkg/manager/dnf.go new file mode 100644 index 0000000..0eb11dc --- /dev/null +++ b/pkg/manager/dnf.go @@ -0,0 +1,160 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package manager + +import ( + "bufio" + "fmt" + "os/exec" + "strings" +) + +// DNF represents the DNF package manager +type DNF struct { + rootCmd string +} + +func (*DNF) Exists() bool { + _, err := exec.LookPath("dnf") + return err == nil +} + +func (*DNF) Name() string { + return "dnf" +} + +func (*DNF) Format() string { + return "rpm" +} + +func (d *DNF) SetRootCmd(s string) { + d.rootCmd = s +} + +func (d *DNF) Sync(opts *Opts) error { + opts = ensureOpts(opts) + cmd := d.getCmd(opts, "dnf", "upgrade") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("dnf: sync: %w", err) + } + return nil +} + +func (d *DNF) Install(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := d.getCmd(opts, "dnf", "install", "--allowerasing") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("dnf: install: %w", err) + } + return nil +} + +func (d *DNF) InstallLocal(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + return d.Install(opts, pkgs...) +} + +func (d *DNF) Remove(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := d.getCmd(opts, "dnf", "remove") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("dnf: remove: %w", err) + } + return nil +} + +func (d *DNF) Upgrade(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := d.getCmd(opts, "dnf", "upgrade") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("dnf: upgrade: %w", err) + } + return nil +} + +func (d *DNF) UpgradeAll(opts *Opts) error { + opts = ensureOpts(opts) + cmd := d.getCmd(opts, "dnf", "upgrade") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("dnf: upgradeall: %w", err) + } + return nil +} + +func (d *DNF) ListInstalled(opts *Opts) (map[string]string, error) { + out := map[string]string{} + cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name, version, ok := strings.Cut(scanner.Text(), "\u200b") + if !ok { + continue + } + version = strings.TrimPrefix(version, "0:") + out[name] = version + } + + err = scanner.Err() + if err != nil { + return nil, err + } + + return out, nil +} + +func (d *DNF) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { + var cmd *exec.Cmd + if opts.AsRoot { + cmd = exec.Command(getRootCmd(d.rootCmd), mgrCmd) + cmd.Args = append(cmd.Args, opts.Args...) + cmd.Args = append(cmd.Args, args...) + } else { + cmd = exec.Command(mgrCmd, args...) + } + + if opts.NoConfirm { + cmd.Args = append(cmd.Args, "-y") + } + + return cmd +} diff --git a/pkg/manager/managers.go b/pkg/manager/managers.go new file mode 100644 index 0000000..7aae16f --- /dev/null +++ b/pkg/manager/managers.go @@ -0,0 +1,124 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package manager + +import ( + "os" + "os/exec" +) + +var Args []string + +type Opts struct { + AsRoot bool + NoConfirm bool + Args []string +} + +var DefaultOpts = &Opts{ + AsRoot: true, + NoConfirm: false, +} + +// DefaultRootCmd is the command used for privilege elevation by default +var DefaultRootCmd = "sudo" + +var managers = []Manager{ + &Pacman{}, + &APT{}, + &DNF{}, + &YUM{}, + &APK{}, + &Zypper{}, +} + +// Register registers a new package manager +func Register(m Manager) { + managers = append(managers, m) +} + +// Manager represents a system package manager +type Manager interface { + // Name returns the name of the manager. + Name() string + // Format returns the packaging format of the manager. + // Examples: rpm, deb, apk + Format() string + // Returns true if the package manager exists on the system. + Exists() bool + // Sets the command used to elevate privileges. Defaults to DefaultRootCmd. + SetRootCmd(string) + // Sync fetches repositories without installing anything + Sync(*Opts) error + // Install installs packages + Install(*Opts, ...string) error + // Remove uninstalls packages + Remove(*Opts, ...string) error + // Upgrade upgrades packages + Upgrade(*Opts, ...string) error + // InstallLocal installs packages from local files rather than repos + InstallLocal(*Opts, ...string) error + // UpgradeAll upgrades all packages + UpgradeAll(*Opts) error + // ListInstalled returns all installed packages mapped to their versions + ListInstalled(*Opts) (map[string]string, error) +} + +// Detect returns the package manager detected on the system +func Detect() Manager { + for _, mgr := range managers { + if mgr.Exists() { + return mgr + } + } + return nil +} + +// Get returns the package manager with the given name +func Get(name string) Manager { + for _, mgr := range managers { + if mgr.Name() == name { + return mgr + } + } + return nil +} + +// getRootCmd returns rootCmd if it's not empty, otherwise returns DefaultRootCmd +func getRootCmd(rootCmd string) string { + if rootCmd != "" { + return rootCmd + } + return DefaultRootCmd +} + +func setCmdEnv(cmd *exec.Cmd) { + cmd.Env = os.Environ() + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr +} + +func ensureOpts(opts *Opts) *Opts { + if opts == nil { + opts = DefaultOpts + } + opts.Args = append(opts.Args, Args...) + return opts +} diff --git a/pkg/manager/pacman.go b/pkg/manager/pacman.go new file mode 100644 index 0000000..d58626b --- /dev/null +++ b/pkg/manager/pacman.go @@ -0,0 +1,159 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package manager + +import ( + "bufio" + "fmt" + "os/exec" + "strings" +) + +// Pacman represents the Pacman package manager +type Pacman struct { + rootCmd string +} + +func (*Pacman) Exists() bool { + _, err := exec.LookPath("pacman") + return err == nil +} + +func (*Pacman) Name() string { + return "pacman" +} + +func (*Pacman) Format() string { + return "archlinux" +} + +func (p *Pacman) SetRootCmd(s string) { + p.rootCmd = s +} + +func (p *Pacman) Sync(opts *Opts) error { + opts = ensureOpts(opts) + cmd := p.getCmd(opts, "pacman", "-Sy") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("pacman: sync: %w", err) + } + return nil +} + +func (p *Pacman) Install(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := p.getCmd(opts, "pacman", "-S", "--needed") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("pacman: install: %w", err) + } + return nil +} + +func (p *Pacman) InstallLocal(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := p.getCmd(opts, "pacman", "-U", "--needed") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("pacman: installlocal: %w", err) + } + return nil +} + +func (p *Pacman) Remove(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := p.getCmd(opts, "pacman", "-R") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("pacman: remove: %w", err) + } + return nil +} + +func (p *Pacman) Upgrade(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + return p.Install(opts, pkgs...) +} + +func (p *Pacman) UpgradeAll(opts *Opts) error { + opts = ensureOpts(opts) + cmd := p.getCmd(opts, "pacman", "-Su") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("pacman: upgradeall: %w", err) + } + return nil +} + +func (p *Pacman) ListInstalled(opts *Opts) (map[string]string, error) { + out := map[string]string{} + cmd := exec.Command("pacman", "-Q") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name, version, ok := strings.Cut(scanner.Text(), " ") + if !ok { + continue + } + out[name] = version + } + + err = scanner.Err() + if err != nil { + return nil, err + } + + return out, nil +} + +func (p *Pacman) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { + var cmd *exec.Cmd + if opts.AsRoot { + cmd = exec.Command(getRootCmd(p.rootCmd), mgrCmd) + cmd.Args = append(cmd.Args, opts.Args...) + cmd.Args = append(cmd.Args, args...) + } else { + cmd = exec.Command(mgrCmd, args...) + } + + if opts.NoConfirm { + cmd.Args = append(cmd.Args, "--noconfirm") + } + + return cmd +} diff --git a/pkg/manager/yum.go b/pkg/manager/yum.go new file mode 100644 index 0000000..b5cb5aa --- /dev/null +++ b/pkg/manager/yum.go @@ -0,0 +1,160 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package manager + +import ( + "bufio" + "fmt" + "os/exec" + "strings" +) + +// YUM represents the YUM package manager +type YUM struct { + rootCmd string +} + +func (*YUM) Exists() bool { + _, err := exec.LookPath("yum") + return err == nil +} + +func (*YUM) Name() string { + return "yum" +} + +func (*YUM) Format() string { + return "rpm" +} + +func (y *YUM) SetRootCmd(s string) { + y.rootCmd = s +} + +func (y *YUM) Sync(opts *Opts) error { + opts = ensureOpts(opts) + cmd := y.getCmd(opts, "yum", "upgrade") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("yum: sync: %w", err) + } + return nil +} + +func (y *YUM) Install(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := y.getCmd(opts, "yum", "install", "--allowerasing") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("yum: install: %w", err) + } + return nil +} + +func (y *YUM) InstallLocal(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + return y.Install(opts, pkgs...) +} + +func (y *YUM) Remove(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := y.getCmd(opts, "yum", "remove") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("yum: remove: %w", err) + } + return nil +} + +func (y *YUM) Upgrade(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := y.getCmd(opts, "yum", "upgrade") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("yum: upgrade: %w", err) + } + return nil +} + +func (y *YUM) UpgradeAll(opts *Opts) error { + opts = ensureOpts(opts) + cmd := y.getCmd(opts, "yum", "upgrade") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("yum: upgradeall: %w", err) + } + return nil +} + +func (y *YUM) ListInstalled(opts *Opts) (map[string]string, error) { + out := map[string]string{} + cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name, version, ok := strings.Cut(scanner.Text(), "\u200b") + if !ok { + continue + } + version = strings.TrimPrefix(version, "0:") + out[name] = version + } + + err = scanner.Err() + if err != nil { + return nil, err + } + + return out, nil +} + +func (y *YUM) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { + var cmd *exec.Cmd + if opts.AsRoot { + cmd = exec.Command(getRootCmd(y.rootCmd), mgrCmd) + cmd.Args = append(cmd.Args, opts.Args...) + cmd.Args = append(cmd.Args, args...) + } else { + cmd = exec.Command(mgrCmd, args...) + } + + if opts.NoConfirm { + cmd.Args = append(cmd.Args, "-y") + } + + return cmd +} diff --git a/pkg/manager/zypper.go b/pkg/manager/zypper.go new file mode 100644 index 0000000..55aab95 --- /dev/null +++ b/pkg/manager/zypper.go @@ -0,0 +1,160 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package manager + +import ( + "bufio" + "fmt" + "os/exec" + "strings" +) + +// Zypper represents the Zypper package manager +type Zypper struct { + rootCmd string +} + +func (*Zypper) Exists() bool { + _, err := exec.LookPath("zypper") + return err == nil +} + +func (*Zypper) Name() string { + return "zypper" +} + +func (*Zypper) Format() string { + return "rpm" +} + +func (z *Zypper) SetRootCmd(s string) { + z.rootCmd = s +} + +func (z *Zypper) Sync(opts *Opts) error { + opts = ensureOpts(opts) + cmd := z.getCmd(opts, "zypper", "refresh") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("zypper: sync: %w", err) + } + return nil +} + +func (z *Zypper) Install(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := z.getCmd(opts, "zypper", "install", "-y") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("zypper: install: %w", err) + } + return nil +} + +func (z *Zypper) InstallLocal(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + return z.Install(opts, pkgs...) +} + +func (z *Zypper) Remove(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := z.getCmd(opts, "zypper", "remove", "-y") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("zypper: remove: %w", err) + } + return nil +} + +func (z *Zypper) Upgrade(opts *Opts, pkgs ...string) error { + opts = ensureOpts(opts) + cmd := z.getCmd(opts, "zypper", "update", "-y") + cmd.Args = append(cmd.Args, pkgs...) + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("zypper: upgrade: %w", err) + } + return nil +} + +func (z *Zypper) UpgradeAll(opts *Opts) error { + opts = ensureOpts(opts) + cmd := z.getCmd(opts, "zypper", "update", "-y") + setCmdEnv(cmd) + err := cmd.Run() + if err != nil { + return fmt.Errorf("zypper: upgradeall: %w", err) + } + return nil +} + +func (z *Zypper) ListInstalled(opts *Opts) (map[string]string, error) { + out := map[string]string{} + cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n") + + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + err = cmd.Start() + if err != nil { + return nil, err + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + name, version, ok := strings.Cut(scanner.Text(), "\u200b") + if !ok { + continue + } + version = strings.TrimPrefix(version, "0:") + out[name] = version + } + + err = scanner.Err() + if err != nil { + return nil, err + } + + return out, nil +} + +func (z *Zypper) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { + var cmd *exec.Cmd + if opts.AsRoot { + cmd = exec.Command(getRootCmd(z.rootCmd), mgrCmd) + cmd.Args = append(cmd.Args, opts.Args...) + cmd.Args = append(cmd.Args, args...) + } else { + cmd = exec.Command(mgrCmd, args...) + } + + if opts.NoConfirm { + cmd.Args = append(cmd.Args, "-y") + } + + return cmd +} diff --git a/pkg/repos/find.go b/pkg/repos/find.go new file mode 100644 index 0000000..4179864 --- /dev/null +++ b/pkg/repos/find.go @@ -0,0 +1,83 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package repos + +import ( + "context" + + "lure.sh/lure/internal/db" +) + +// FindPkgs looks for packages matching the inputs inside the database. +// It returns a map that maps the package name input to any packages found for it. +// It also returns a slice that contains the names of all packages that were not found. +func FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) { + found := map[string][]db.Package{} + notFound := []string(nil) + + for _, pkgName := range pkgs { + if pkgName == "" { + continue + } + + result, err := db.GetPkgs(ctx, "json_array_contains(provides, ?)", pkgName) + if err != nil { + return nil, nil, err + } + + added := 0 + for result.Next() { + var pkg db.Package + err = result.StructScan(&pkg) + if err != nil { + return nil, nil, err + } + + added++ + found[pkgName] = append(found[pkgName], pkg) + } + result.Close() + + if added == 0 { + result, err := db.GetPkgs(ctx, "name LIKE ?", pkgName) + if err != nil { + return nil, nil, err + } + + for result.Next() { + var pkg db.Package + err = result.StructScan(&pkg) + if err != nil { + return nil, nil, err + } + + added++ + found[pkgName] = append(found[pkgName], pkg) + } + + result.Close() + } + + if added == 0 { + notFound = append(notFound, pkgName) + } + } + + return found, notFound, nil +} diff --git a/pkg/repos/find_test.go b/pkg/repos/find_test.go new file mode 100644 index 0000000..f182e0d --- /dev/null +++ b/pkg/repos/find_test.go @@ -0,0 +1,148 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package repos_test + +import ( + "context" + "reflect" + "strings" + "testing" + + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/repos" +) + +func TestFindPkgs(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + setCfgDirs(t) + defer removeCacheDir(t) + + ctx := context.Background() + + err = repos.Pull(ctx, []types.Repo{ + { + Name: "default", + URL: "https://github.com/Arsen6331/lure-repo.git", + }, + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + found, notFound, err := repos.FindPkgs([]string{"itd", "nonexistentpackage1", "nonexistentpackage2"}) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if !reflect.DeepEqual(notFound, []string{"nonexistentpackage1", "nonexistentpackage2"}) { + t.Errorf("Expected 'nonexistentpackage{1,2} not to be found") + } + + if len(found) != 1 { + t.Errorf("Expected 1 package found, got %d", len(found)) + } + + itdPkgs, ok := found["itd"] + if !ok { + t.Fatalf("Expected 'itd' packages to be found") + } + + if len(itdPkgs) < 2 { + t.Errorf("Expected two 'itd' packages to be found") + } + + for i, pkg := range itdPkgs { + if !strings.HasPrefix(pkg.Name, "itd") { + t.Errorf("Expected package name of all found packages to start with 'itd', got %s on element %d", pkg.Name, i) + } + } +} + +func TestFindPkgsEmpty(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + setCfgDirs(t) + defer removeCacheDir(t) + + err = db.InsertPackage(db.Package{ + Name: "test1", + Repository: "default", + Version: "0.0.1", + Release: 1, + Description: db.NewJSON(map[string]string{ + "en": "Test package 1", + "ru": "Проверочный пакет 1", + }), + Provides: db.NewJSON([]string{""}), + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + err = db.InsertPackage(db.Package{ + Name: "test2", + Repository: "default", + Version: "0.0.1", + Release: 1, + Description: db.NewJSON(map[string]string{ + "en": "Test package 2", + "ru": "Проверочный пакет 2", + }), + Provides: db.NewJSON([]string{"test"}), + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + found, notFound, err := repos.FindPkgs([]string{"test", ""}) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if len(notFound) != 0 { + t.Errorf("Expected all packages to be found") + } + + if len(found) != 1 { + t.Errorf("Expected 1 package found, got %d", len(found)) + } + + testPkgs, ok := found["test"] + if !ok { + t.Fatalf("Expected 'test' packages to be found") + } + + if len(testPkgs) != 1 { + t.Errorf("Expected one 'test' package to be found, got %d", len(testPkgs)) + } + + if testPkgs[0].Name != "test2" { + t.Errorf("Expected 'test2' package, got '%s'", testPkgs[0].Name) + } +} diff --git a/pkg/repos/pull.go b/pkg/repos/pull.go new file mode 100644 index 0000000..84df688 --- /dev/null +++ b/pkg/repos/pull.go @@ -0,0 +1,441 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package repos + +import ( + "context" + "errors" + "io" + "net/url" + "os" + "path/filepath" + "reflect" + "strings" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-billy/v5/osfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/format/diff" + "github.com/pelletier/go-toml/v2" + "go.elara.ws/vercmp" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/shutils/decoder" + "lure.sh/lure/internal/shutils/handlers" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/distro" + "lure.sh/lure/pkg/loggerctx" + "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/interp" + "mvdan.cc/sh/v3/syntax" +) + +// Pull pulls the provided repositories. If a repo doesn't exist, it will be cloned +// and its packages will be written to the DB. If it does exist, it will be pulled. +// In this case, only changed packages will be processed if possible. +// If repos is set to nil, the repos in the LURE config will be used. +func Pull(ctx context.Context, repos []types.Repo) error { + log := loggerctx.From(ctx) + + if repos == nil { + repos = config.Config(ctx).Repos + } + + for _, repo := range repos { + repoURL, err := url.Parse(repo.URL) + if err != nil { + return err + } + + log.Info("Pulling repository").Str("name", repo.Name).Send() + repoDir := filepath.Join(config.GetPaths(ctx).RepoDir, repo.Name) + + var repoFS billy.Filesystem + gitDir := filepath.Join(repoDir, ".git") + // Only pull repos that contain valid git repos + if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() { + r, err := git.PlainOpen(repoDir) + if err != nil { + return err + } + + w, err := r.Worktree() + if err != nil { + return err + } + + old, err := r.Head() + if err != nil { + return err + } + + err = w.PullContext(ctx, &git.PullOptions{Progress: os.Stderr}) + if errors.Is(err, git.NoErrAlreadyUpToDate) { + log.Info("Repository up to date").Str("name", repo.Name).Send() + } else if err != nil { + return err + } + repoFS = w.Filesystem + + // Make sure the DB is created even if the repo is up to date + if !errors.Is(err, git.NoErrAlreadyUpToDate) || db.IsEmpty(ctx) { + new, err := r.Head() + if err != nil { + return err + } + + // If the DB was not present at startup, that means it's + // empty. In this case, we need to update the DB fully + // rather than just incrementally. + if db.IsEmpty(ctx) { + err = processRepoFull(ctx, repo, repoDir) + if err != nil { + return err + } + } else { + err = processRepoChanges(ctx, repo, r, w, old, new) + if err != nil { + return err + } + } + } + } else { + err = os.RemoveAll(repoDir) + if err != nil { + return err + } + + err = os.MkdirAll(repoDir, 0o755) + if err != nil { + return err + } + + _, err = git.PlainCloneContext(ctx, repoDir, false, &git.CloneOptions{ + URL: repoURL.String(), + Progress: os.Stderr, + }) + if err != nil { + return err + } + + err = processRepoFull(ctx, repo, repoDir) + if err != nil { + return err + } + + repoFS = osfs.New(repoDir) + } + + fl, err := repoFS.Open("lure-repo.toml") + if err != nil { + log.Warn("Git repository does not appear to be a valid LURE repo").Str("repo", repo.Name).Send() + continue + } + + var repoCfg types.RepoConfig + err = toml.NewDecoder(fl).Decode(&repoCfg) + if err != nil { + return err + } + fl.Close() + + // If the version doesn't have a "v" prefix, it's not a standard version. + // It may be "unknown" or a git version, but either way, there's no way + // to compare it to the repo version, so only compare versions with the "v". + if strings.HasPrefix(config.Version, "v") { + if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 { + log.Warn("LURE repo's minumum LURE version is greater than the current version. Try updating LURE if something doesn't work.").Str("repo", repo.Name).Send() + } + } + } + + return nil +} + +type actionType uint8 + +const ( + actionDelete actionType = iota + actionUpdate +) + +type action struct { + Type actionType + File string +} + +func processRepoChanges(ctx context.Context, repo types.Repo, r *git.Repository, w *git.Worktree, old, new *plumbing.Reference) error { + oldCommit, err := r.CommitObject(old.Hash()) + if err != nil { + return err + } + + newCommit, err := r.CommitObject(new.Hash()) + if err != nil { + return err + } + + patch, err := oldCommit.Patch(newCommit) + if err != nil { + return err + } + + var actions []action + for _, fp := range patch.FilePatches() { + from, to := fp.Files() + + if !isValid(from, to) { + continue + } + + if to == nil { + actions = append(actions, action{ + Type: actionDelete, + File: from.Path(), + }) + } else if from == nil { + actions = append(actions, action{ + Type: actionUpdate, + File: to.Path(), + }) + } else { + if from.Path() != to.Path() { + actions = append(actions, + action{ + Type: actionDelete, + File: from.Path(), + }, + action{ + Type: actionUpdate, + File: to.Path(), + }, + ) + } else { + actions = append(actions, action{ + Type: actionUpdate, + File: to.Path(), + }) + } + } + } + + repoDir := w.Filesystem.Root() + parser := syntax.NewParser() + + for _, action := range actions { + env := append(os.Environ(), "scriptdir="+filepath.Dir(filepath.Join(repoDir, action.File))) + runner, err := interp.New( + interp.Env(expand.ListEnviron(env...)), + interp.ExecHandler(handlers.NopExec), + interp.ReadDirHandler(handlers.RestrictedReadDir(repoDir)), + interp.StatHandler(handlers.RestrictedStat(repoDir)), + interp.OpenHandler(handlers.RestrictedOpen(repoDir)), + interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}), + ) + if err != nil { + return err + } + + switch action.Type { + case actionDelete: + if filepath.Base(action.File) != "lure.sh" { + continue + } + + scriptFl, err := oldCommit.File(action.File) + if err != nil { + return nil + } + + r, err := scriptFl.Reader() + if err != nil { + return nil + } + + var pkg db.Package + err = parseScript(ctx, parser, runner, r, &pkg) + if err != nil { + return err + } + + err = db.DeletePkgs(ctx, "name = ? AND repository = ?", pkg.Name, repo.Name) + if err != nil { + return err + } + case actionUpdate: + if filepath.Base(action.File) != "lure.sh" { + action.File = filepath.Join(filepath.Dir(action.File), "lure.sh") + } + + scriptFl, err := newCommit.File(action.File) + if err != nil { + return nil + } + + r, err := scriptFl.Reader() + if err != nil { + return nil + } + + pkg := db.Package{ + Description: db.NewJSON(map[string]string{}), + Homepage: db.NewJSON(map[string]string{}), + Maintainer: db.NewJSON(map[string]string{}), + Depends: db.NewJSON(map[string][]string{}), + BuildDepends: db.NewJSON(map[string][]string{}), + Repository: repo.Name, + } + + err = parseScript(ctx, parser, runner, r, &pkg) + if err != nil { + return err + } + + resolveOverrides(runner, &pkg) + + err = db.InsertPackage(ctx, pkg) + if err != nil { + return err + } + } + } + + return nil +} + +// isValid makes sure the path of the file being updated is valid. +// It checks to make sure the file is not within a nested directory +// and that it is called lure.sh. +func isValid(from, to diff.File) bool { + var path string + if from != nil { + path = from.Path() + } + if to != nil { + path = to.Path() + } + + match, _ := filepath.Match("*/*.sh", path) + return match +} + +func processRepoFull(ctx context.Context, repo types.Repo, repoDir string) error { + glob := filepath.Join(repoDir, "/*/lure.sh") + matches, err := filepath.Glob(glob) + if err != nil { + return err + } + + parser := syntax.NewParser() + + for _, match := range matches { + env := append(os.Environ(), "scriptdir="+filepath.Dir(match)) + runner, err := interp.New( + interp.Env(expand.ListEnviron(env...)), + interp.ExecHandler(handlers.NopExec), + interp.ReadDirHandler(handlers.RestrictedReadDir(repoDir)), + interp.StatHandler(handlers.RestrictedStat(repoDir)), + interp.OpenHandler(handlers.RestrictedOpen(repoDir)), + interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}), + ) + if err != nil { + return err + } + + scriptFl, err := os.Open(match) + if err != nil { + return err + } + + pkg := db.Package{ + Description: db.NewJSON(map[string]string{}), + Homepage: db.NewJSON(map[string]string{}), + Maintainer: db.NewJSON(map[string]string{}), + Depends: db.NewJSON(map[string][]string{}), + BuildDepends: db.NewJSON(map[string][]string{}), + Repository: repo.Name, + } + + err = parseScript(ctx, parser, runner, scriptFl, &pkg) + if err != nil { + return err + } + + resolveOverrides(runner, &pkg) + + err = db.InsertPackage(ctx, pkg) + if err != nil { + return err + } + } + + return nil +} + +func parseScript(ctx context.Context, parser *syntax.Parser, runner *interp.Runner, r io.ReadCloser, pkg *db.Package) error { + defer r.Close() + fl, err := parser.Parse(r, "lure.sh") + if err != nil { + return err + } + + runner.Reset() + err = runner.Run(ctx, fl) + if err != nil { + return err + } + + d := decoder.New(&distro.OSRelease{}, runner) + d.Overrides = false + d.LikeDistros = false + return d.DecodeVars(pkg) +} + +var overridable = map[string]string{ + "deps": "Depends", + "build_deps": "BuildDepends", + "desc": "Description", + "homepage": "Homepage", + "maintainer": "Maintainer", +} + +func resolveOverrides(runner *interp.Runner, pkg *db.Package) { + pkgVal := reflect.ValueOf(pkg).Elem() + for name, val := range runner.Vars { + for prefix, field := range overridable { + if strings.HasPrefix(name, prefix) { + override := strings.TrimPrefix(name, prefix) + override = strings.TrimPrefix(override, "_") + + field := pkgVal.FieldByName(field) + varVal := field.FieldByName("Val") + varType := varVal.Type() + + switch varType.Elem().String() { + case "[]string": + varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.List)) + case "string": + varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.Str)) + } + break + } + } + } +} diff --git a/pkg/repos/pull_test.go b/pkg/repos/pull_test.go new file mode 100644 index 0000000..6520720 --- /dev/null +++ b/pkg/repos/pull_test.go @@ -0,0 +1,109 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package repos_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/repos" +) + +func setCfgDirs(t *testing.T) { + t.Helper() + + paths := config.GetPaths() + + var err error + paths.CacheDir, err = os.MkdirTemp("/tmp", "lure-pull-test.*") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + paths.RepoDir = filepath.Join(paths.CacheDir, "repo") + paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs") + + err = os.MkdirAll(paths.RepoDir, 0o755) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + err = os.MkdirAll(paths.PkgsDir, 0o755) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + paths.DBPath = filepath.Join(paths.CacheDir, "db") +} + +func removeCacheDir(t *testing.T) { + t.Helper() + + err := os.RemoveAll(config.GetPaths().CacheDir) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } +} + +func TestPull(t *testing.T) { + _, err := db.Open(":memory:") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + defer db.Close() + + setCfgDirs(t) + defer removeCacheDir(t) + + ctx := context.Background() + + err = repos.Pull(ctx, []types.Repo{ + { + Name: "default", + URL: "https://github.com/Arsen6331/lure-repo.git", + }, + }) + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + result, err := db.GetPkgs("name LIKE 'itd%'") + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + var pkgAmt int + for result.Next() { + var dbPkg db.Package + err = result.StructScan(&dbPkg) + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + pkgAmt++ + } + + if pkgAmt < 2 { + t.Errorf("Expected 2 packages to match, got %d", pkgAmt) + } +} diff --git a/pkg/search/search.go b/pkg/search/search.go new file mode 100644 index 0000000..b56300f --- /dev/null +++ b/pkg/search/search.go @@ -0,0 +1,167 @@ +package search + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path/filepath" + "strconv" + "strings" + + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" +) + +// Filter represents search filters. +type Filter int + +// Filters +const ( + FilterNone Filter = iota + FilterInRepo + FilterSupportsArch +) + +// SoryBy represents a value that packages can be sorted by. +type SortBy int + +// Sort values +const ( + SortByNone = iota + SortByName + SortByRepo + SortByVersion +) + +// Package represents a package from LURE's database +type Package struct { + Name string + Version string + Release int + Epoch uint + Description map[string]string + Homepage map[string]string + Maintainer map[string]string + Architectures []string + Licenses []string + Provides []string + Conflicts []string + Replaces []string + Depends map[string][]string + BuildDepends map[string][]string + OptDepends map[string][]string + Repository string +} + +func convertPkg(p db.Package) Package { + return Package{ + Name: p.Name, + Version: p.Version, + Release: p.Release, + Epoch: p.Epoch, + Description: p.Description.Val, + Homepage: p.Homepage.Val, + Maintainer: p.Maintainer.Val, + Architectures: p.Architectures.Val, + Licenses: p.Licenses.Val, + Provides: p.Provides.Val, + Conflicts: p.Conflicts.Val, + Replaces: p.Replaces.Val, + Depends: p.Depends.Val, + BuildDepends: p.BuildDepends.Val, + OptDepends: p.OptDepends.Val, + Repository: p.Repository, + } +} + +// Options contains the options for a search. +type Options struct { + Filter Filter + FilterValue string + SortBy SortBy + Limit int64 + Query string +} + +// Search searches for packages in the database based on the given options. +func Search(ctx context.Context, opts Options) ([]Package, error) { + query := "(name LIKE ? OR description LIKE ? OR json_array_contains(provides, ?))" + args := []any{"%" + opts.Query + "%", "%" + opts.Query + "%", opts.Query} + + if opts.Filter != FilterNone { + switch opts.Filter { + case FilterInRepo: + query += " AND repository = ?" + case FilterSupportsArch: + query += " AND json_array_contains(architectures, ?)" + } + args = append(args, opts.FilterValue) + } + + if opts.SortBy != SortByNone { + switch opts.SortBy { + case SortByName: + query += " ORDER BY name" + case SortByRepo: + query += " ORDER BY repository" + case SortByVersion: + query += " ORDER BY version" + } + } + + if opts.Limit != 0 { + query += " LIMIT " + strconv.FormatInt(opts.Limit, 10) + } + + result, err := db.GetPkgs(ctx, query, args...) + if err != nil { + return nil, err + } + + var out []Package + for result.Next() { + pkg := db.Package{} + err = result.StructScan(&pkg) + if err != nil { + return nil, err + } + out = append(out, convertPkg(pkg)) + } + + return out, err +} + +// GetPkg gets a single package from the database and returns it. +func GetPkg(ctx context.Context, repo, name string) (Package, error) { + pkg, err := db.GetPkg(ctx, "name = ? AND repository = ?", name, repo) + return convertPkg(*pkg), err +} + +var ( + // ErrInvalidArgument is an error returned by GetScript when one of its arguments + // contain invalid characters + ErrInvalidArgument = errors.New("name and repository must not contain . or /") + + // ErrScriptNotFound is returned by GetScript if it can't find the script requested + // by the user. + ErrScriptNotFound = errors.New("requested script not found") +) + +// GetScript returns a reader containing the build script for a given package. +func GetScript(ctx context.Context, repo, name string) (io.ReadCloser, error) { + if strings.Contains(name, "./") || strings.ContainsAny(repo, "./") { + return nil, ErrInvalidArgument + } + + scriptPath := filepath.Join(config.GetPaths(ctx).RepoDir, repo, name, "lure.sh") + fl, err := os.Open(scriptPath) + if errors.Is(err, fs.ErrNotExist) { + return nil, ErrScriptNotFound + } else if err != nil { + return nil, err + } + + return fl, nil +} diff --git a/repo.go b/repo.go new file mode 100644 index 0000000..fc15142 --- /dev/null +++ b/repo.go @@ -0,0 +1,162 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "os" + "path/filepath" + + "github.com/pelletier/go-toml/v2" + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/repos" + "golang.org/x/exp/slices" +) + +var addrepoCmd = &cli.Command{ + Name: "addrepo", + Usage: "Add a new repository", + Aliases: []string{"ar"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Required: true, + Usage: "Name of the new repo", + }, + &cli.StringFlag{ + Name: "url", + Aliases: []string{"u"}, + Required: true, + Usage: "URL of the new repo", + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + name := c.String("name") + repoURL := c.String("url") + + cfg := config.Config(ctx) + + for _, repo := range cfg.Repos { + if repo.URL == repoURL { + log.Fatal("Repo already exists").Str("name", repo.Name).Send() + } + } + + cfg.Repos = append(cfg.Repos, types.Repo{ + Name: name, + URL: repoURL, + }) + + cfgFl, err := os.Create(config.GetPaths(ctx).ConfigPath) + if err != nil { + log.Fatal("Error opening config file").Err(err).Send() + } + + err = toml.NewEncoder(cfgFl).Encode(cfg) + if err != nil { + log.Fatal("Error encoding config").Err(err).Send() + } + + err = repos.Pull(ctx, cfg.Repos) + if err != nil { + log.Fatal("Error pulling repos").Err(err).Send() + } + + return nil + }, +} + +var removerepoCmd = &cli.Command{ + Name: "removerepo", + Usage: "Remove an existing repository", + Aliases: []string{"rr"}, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Required: true, + Usage: "Name of the repo to be deleted", + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + name := c.String("name") + cfg := config.Config(ctx) + + found := false + index := 0 + for i, repo := range cfg.Repos { + if repo.Name == name { + index = i + found = true + } + } + if !found { + log.Fatal("Repo does not exist").Str("name", name).Send() + } + + cfg.Repos = slices.Delete(cfg.Repos, index, index+1) + + cfgFl, err := os.Create(config.GetPaths(ctx).ConfigPath) + if err != nil { + log.Fatal("Error opening config file").Err(err).Send() + } + + err = toml.NewEncoder(cfgFl).Encode(&cfg) + if err != nil { + log.Fatal("Error encoding config").Err(err).Send() + } + + err = os.RemoveAll(filepath.Join(config.GetPaths(ctx).RepoDir, name)) + if err != nil { + log.Fatal("Error removing repo directory").Err(err).Send() + } + + err = db.DeletePkgs(ctx, "repository = ?", name) + if err != nil { + log.Fatal("Error removing packages from database").Err(err).Send() + } + + return nil + }, +} + +var refreshCmd = &cli.Command{ + Name: "refresh", + Usage: "Pull all repositories that have changed", + Aliases: []string{"ref"}, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + err := repos.Pull(ctx, config.Config(ctx).Repos) + if err != nil { + log.Fatal("Error pulling repos").Err(err).Send() + } + return nil + }, +} diff --git a/scripts/completion/bash b/scripts/completion/bash new file mode 100644 index 0000000..ac5df1f --- /dev/null +++ b/scripts/completion/bash @@ -0,0 +1,21 @@ +#! /bin/bash + +: ${PROG:=$(basename ${BASH_SOURCE})} + +_cli_bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + local cur opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + if [[ "$cur" == "-"* ]]; then + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) + else + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + fi + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi +} + +complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG +unset PROG \ No newline at end of file diff --git a/scripts/completion/zsh b/scripts/completion/zsh new file mode 100644 index 0000000..3f2b2a4 --- /dev/null +++ b/scripts/completion/zsh @@ -0,0 +1,20 @@ +#compdef lure + +_cli_zsh_autocomplete() { + local -a opts + local cur + cur=${words[-1]} + if [[ "$cur" == "-"* ]]; then + opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}") + else + opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}") + fi + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi +} + +compdef _cli_zsh_autocomplete lure diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..acc1829 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# LURE - Linux User REpository +# Copyright (C) 2023 Elara Musayelyan +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +info() { + echo $'\x1b[32m[INFO]\x1b[0m' $@ +} + +warn() { + echo $'\x1b[31m[WARN]\x1b[0m' $@ +} + +error() { + echo $'\x1b[31;1m[ERR]\x1b[0m' $@ + exit 1 +} + +installPkg() { + rootCmd="" + if command -v doas &>/dev/null; then + rootCmd="doas" + elif command -v sudo &>/dev/null; then + rootCmd="sudo" + else + warn "No privilege elevation command (e.g. sudo, doas) detected" + fi + + case $1 in + pacman) $rootCmd pacman --noconfirm -U ${@:2} ;; + apk) $rootCmd apk add --allow-untrusted ${@:2} ;; + zypper) $rootCmd zypper --no-gpg-checks install ${@:2} ;; + *) $rootCmd $1 install -y ${@:2} ;; + esac +} + +if ! command -v curl &>/dev/null; then + error "This script requires the curl command. Please install it and run again." +fi + +pkgFormat="" +pkgMgr="" +if command -v pacman &>/dev/null; then + info "Detected pacman" + pkgFormat="pkg.tar.zst" + pkgMgr="pacman" +elif command -v apt &>/dev/null; then + info "Detected apt" + pkgFormat="deb" + pkgMgr="apt" +elif command -v dnf &>/dev/null; then + info "Detected dnf" + pkgFormat="rpm" + pkgMgr="dnf" +elif command -v yum &>/dev/null; then + info "Detected yum" + pkgFormat="rpm" + pkgMgr="yum" +elif command -v zypper &>/dev/null; then + info "Detected zypper" + pkgFormat="rpm" + pkgMgr="zypper" +elif command -v apk &>/dev/null; then + info "Detected apk" + pkgFormat="apk" + pkgMgr="apk" +else + error "No supported package manager detected!" +fi + +latestVersion=$(curl -sI 'https://gitea.elara.ws/lure/lure/releases/latest' | grep -io 'location: .*' | rev | cut -d '/' -f1 | rev | tr -d '[:space:]') +info "Found latest LURE version:" $latestVersion + +fname="$(mktemp -u -p /tmp "lure.XXXXXXXXXX").${pkgFormat}" +url="https://gitea.elara.ws/lure/lure/releases/download/${latestVersion}/linux-user-repository-${latestVersion#v}-linux-$(uname -m).${pkgFormat}" + +info "Downloading LURE package" +curl -L $url -o $fname + +info "Installing LURE package" +installPkg $pkgMgr $fname + +info "Cleaning up" +rm $fname + +info "Done!" diff --git a/upgrade.go b/upgrade.go new file mode 100644 index 0000000..320f3f2 --- /dev/null +++ b/upgrade.go @@ -0,0 +1,131 @@ +/* + * LURE - Linux User REpository + * Copyright (C) 2023 Elara Musayelyan + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package main + +import ( + "context" + "fmt" + + "github.com/urfave/cli/v2" + "lure.sh/lure/internal/config" + "lure.sh/lure/internal/db" + "lure.sh/lure/internal/types" + "lure.sh/lure/pkg/build" + "lure.sh/lure/pkg/distro" + "lure.sh/lure/pkg/loggerctx" + "lure.sh/lure/pkg/manager" + "lure.sh/lure/pkg/repos" + "go.elara.ws/vercmp" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +var upgradeCmd = &cli.Command{ + Name: "upgrade", + Usage: "Upgrade all installed packages", + Aliases: []string{"up"}, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "clean", + Aliases: []string{"c"}, + Usage: "Build package from scratch even if there's an already built package available", + }, + }, + Action: func(c *cli.Context) error { + ctx := c.Context + log := loggerctx.From(ctx) + + info, err := distro.ParseOSRelease(ctx) + if err != nil { + log.Fatal("Error parsing os-release file").Err(err).Send() + } + + mgr := manager.Detect() + if mgr == nil { + log.Fatal("Unable to detect a supported package manager on the system").Send() + } + + err = repos.Pull(ctx, config.Config(ctx).Repos) + if err != nil { + log.Fatal("Error pulling repos").Err(err).Send() + } + + updates, err := checkForUpdates(ctx, mgr, info) + if err != nil { + log.Fatal("Error checking for updates").Err(err).Send() + } + + if len(updates) > 0 { + build.InstallPkgs(ctx, updates, nil, types.BuildOpts{ + Manager: mgr, + Clean: c.Bool("clean"), + Interactive: c.Bool("interactive"), + }) + } else { + log.Info("There is nothing to do.").Send() + } + + return nil + }, +} + +func checkForUpdates(ctx context.Context, mgr manager.Manager, info *distro.OSRelease) ([]db.Package, error) { + installed, err := mgr.ListInstalled(nil) + if err != nil { + return nil, err + } + + pkgNames := maps.Keys(installed) + found, _, err := repos.FindPkgs(ctx, pkgNames) + if err != nil { + return nil, err + } + + var out []db.Package + for pkgName, pkgs := range found { + if slices.Contains(config.Config(ctx).IgnorePkgUpdates, pkgName) { + continue + } + + if len(pkgs) > 1 { + // Puts the element with the highest version first + slices.SortFunc(pkgs, func(a, b db.Package) int { + return vercmp.Compare(a.Version, b.Version) + }) + } + + // First element is the package we want to install + pkg := pkgs[0] + + repoVer := pkg.Version + if pkg.Release != 0 && pkg.Epoch == 0 { + repoVer = fmt.Sprintf("%s-%d", pkg.Version, pkg.Release) + } else if pkg.Release != 0 && pkg.Epoch != 0 { + repoVer = fmt.Sprintf("%d:%s-%d", pkg.Epoch, pkg.Version, pkg.Release) + } + + c := vercmp.Compare(repoVer, installed[pkgName]) + if c == 0 || c == -1 { + continue + } else if c == 1 { + out = append(out, pkg) + } + } + return out, nil +}