aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkitsunyan2018-03-10 21:10:43 +0000
committerkitsunyan2018-03-10 21:10:43 +0000
commitfd397b356e9be1d3572ace965ead57120803a0ac (patch)
treed9e6fe9f903ab935fa6df2e6862a97515caf926c
Initial commitv0.1
-rw-r--r--COPYING674
-rw-r--r--Makefile166
-rw-r--r--README.md32
-rw-r--r--completion/bash.in61
-rwxr-xr-xcompletion/make.sh12
-rw-r--r--doc/asciidoc.conf8
-rw-r--r--doc/pakku.8.txt46
-rw-r--r--doc/pakku.conf.5.txt41
-rw-r--r--lib/bisect.nim77
-rw-r--r--pakku.conf9
-rw-r--r--src/args.nim175
-rw-r--r--src/aur.nim192
-rw-r--r--src/common.nim392
-rw-r--r--src/config.nim125
-rw-r--r--src/feature/localquery.nim81
-rw-r--r--src/feature/syncinfo.nim130
-rw-r--r--src/feature/syncinstall.nim827
-rw-r--r--src/feature/syncsearch.nim53
-rw-r--r--src/format.nim308
-rw-r--r--src/main.nim260
-rw-r--r--src/package.nim249
-rw-r--r--src/pacman.nim367
-rw-r--r--src/utils.nim203
-rw-r--r--src/wrapper/alpm.nim146
-rw-r--r--src/wrapper/curl.nim123
25 files changed, 4757 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <https://www.gnu.org/licenses/>.
+
+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:
+
+ <program> Copyright (C) <year> <name of author>
+ 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
+<https://www.gnu.org/licenses/>.
+
+ 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
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8fee482
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,166 @@
+VERSION = 0.1
+COPYRIGHT = 2018 kitsunyan
+DIST_MODE = false
+
+MAN_PAGES = \
+ doc/pakku.8 \
+ doc/pakku.conf.5
+
+TARGETS = \
+ completion/bash \
+ lib/bisect \
+ src/pakku \
+ ${MAN_PAGES}
+
+TARGETS_NODIST = \
+ ${MAN_PAGES:=.in}
+
+DIST = \
+ COPYING \
+ Makefile \
+ pakku.conf \
+ completion/bash.in \
+ completion/make.sh \
+ doc/asciidoc.conf \
+ ${MAN_PAGES:=.txt} \
+ lib/*.nim \
+ src/*.nim \
+ src/feature/*.nim \
+ src/wrapper/*.nim
+
+EXTRA_DIST = \
+ ${MAN_PAGES:=.in}
+
+DESTDIR =
+PREFIX = /usr/local
+
+BINDIR = ${PREFIX}/bin
+PKGLIBDIR = ${PREFIX}/lib/pakku
+COMPLETIONSDIR = ${PREFIX}/share/bash-completion/completions
+MANDIR = ${PREFIX}/share/man
+LOCALSTATEDIR = /var
+SYSCONFDIR = /etc
+
+ifneq ($(wildcard .git),)
+RVERSION = $(shell (git describe --tags 2> /dev/null || echo v${VERSION}) | \
+tail -c +2 | head -1)
+else
+RVERSION = ${VERSION}
+endif
+
+NIM_TARGET = release
+NIM_OPTIMIZE = size
+NIM_CACHE_DIR = nimcache
+
+NIM_OPTIONS = \
+ --putenv:'PROG_VERSION'="${RVERSION}" \
+ --putenv:'PROG_COPYRIGHT'="${COPYRIGHT}" \
+ --putenv:'PROG_PKGLIBDIR'="${PKGLIBDIR}" \
+ --putenv:'PROG_LOCALSTATEDIR'="${LOCALSTATEDIR}" \
+ --putenv:'PROG_SYSCONFDIR'="${SYSCONFDIR}" \
+ -d:'${NIM_TARGET}' \
+ --opt:'${NIM_OPTIMIZE}' \
+ --hint'[Conf]':off \
+ --hint'[Processing]':off \
+ --hint'[Link]':off \
+ --hint'[SuccessX]':off
+
+ASCIIDOC_OPTIONS = \
+ -f doc/asciidoc.conf \
+ -a manmanual='Pakku Manual' \
+ -a mansource='Pakku' \
+ -a manversion="${RVERSION}"
+
+.PHONY: all clean install distcheck
+
+all: \
+ ${TARGETS} \
+ ${TARGETS_NODIST}
+
+completion/bash: completion/bash.in
+ @echo "GEN: $@"
+ @(cd completion && ./make.sh)
+
+${MAN_PAGES:=.in}: ${MAN_PAGES:=.txt}
+ @echo "GEN: $@"
+ @a2x \
+ --doctype manpage \
+ --format manpage \
+ --asciidoc-opts="${ASCIIDOC_OPTIONS}" \
+ "${@:.in=}.txt"
+ @mv "${@:.in=}" "$@"
+
+${MAN_PAGES}: ${MAN_PAGES:=.in}
+ @echo "GEN: $@"
+ @sed \
+ -e 's,{sysconfdir},${SYSCONFDIR},' \
+ < "${@:=.in}" > "$@"
+
+lib/bisect: lib/bisect.nim
+ @echo "NIM: $@"
+ @nim c ${NIM_OPTIONS} \
+ --nimcache:"${NIM_CACHE_DIR}/bisect" \
+ -o:"$@" "$<"
+
+src/pakku: src/main.nim
+ @echo "NIM: $@"
+ @nim c ${NIM_OPTIONS} \
+ --nimcache:"${NIM_CACHE_DIR}/main" \
+ -o:"$@" "$<"
+
+clean:
+ @rm -rfv nimcache
+ @rm -rfv ${TARGETS}
+ifneq (${DIST_MODE},true)
+ @rm -rfv ${TARGETS_NODIST}
+endif
+
+define install
+ @echo 'INSTALL: $3'
+ @install -Dm$1 $2 '${DESTDIR}$3'
+endef
+
+define uninstall
+ @echo 'UNINSTALL: $1/$2'
+ @rm '${DESTDIR}$1/$2'
+ @rmdir -p '${DESTDIR}$1' 2> /dev/null || true
+endef
+
+install:
+ $(call install,644,'completion/bash','${COMPLETIONSDIR}/pakku')
+ $(call install,644,'doc/pakku.8','${MANDIR}/man8/pakku.8')
+ $(call install,644,'doc/pakku.conf.5','${MANDIR}/man5/pakku.conf.5')
+ $(call install,755,'lib/bisect','${PKGLIBDIR}/bisect')
+ $(call install,755,'src/pakku','${BINDIR}/pakku')
+ $(call install,644,'pakku.conf','${SYSCONFDIR}/pakku.conf')
+
+uninstall:
+ $(call uninstall,'${COMPLETIONSDIR}','pakku')
+ $(call uninstall,'${MANDIR}/man8','pakku.8')
+ $(call uninstall,'${MANDIR}/man5','pakku.conf.5')
+ $(call uninstall,'${PKGLIBDIR}','bisect')
+ $(call uninstall,'${BINDIR}','pakku')
+ $(call uninstall,'${SYSCONFDIR}','pakku.conf')
+
+distcheck:
+ @rm -rf 'pakku-${RVERSION}'
+ @mkdir 'pakku-${RVERSION}'
+ @for f in ${DIST}; do cp --parents $$f 'pakku-${RVERSION}'; done
+
+ @sed -i 'pakku-${RVERSION}/Makefile' \
+ -e 's/^VERSION =.*/VERSION = ${RVERSION}/' \
+ -e 's/^DIST_MODE =.*/DIST_MODE = true/'
+
+ @(cd 'pakku-${RVERSION}' && \
+ make && \
+ mkdir 'destdir' && \
+ make DESTDIR="`pwd`/destdir" install && \
+ make DESTDIR="`pwd`/destdir" uninstall && \
+ [ ! -d 'destdir' ] && \
+ make clean)
+
+ @tar -cJvf 'pakku-${RVERSION}.tar.xz' \
+ ${DIST:%='pakku-${RVERSION}'/%} \
+ ${EXTRA_DIST:%='pakku-${RVERSION}'/%}
+
+ @rm -rf 'pakku-${RVERSION}'
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..297eafb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,32 @@
+# pakku
+
+Pakku is a pacman wrapper with additional features, such as AUR support.
+
+## Description
+
+There are dozens of AUR helpers, but all of them have a fatal flaw:
+I didn't write them! So I made another useless AUR helper.
+Say hello to stillborn AUR helper written in stillborn programming language!
+
+Basically, pakku supports the following features:
+
+- Installing packages from AUR
+- Building packages from official repositories
+- Searching and querying AUR packages
+- Reading comments for AUR packages
+- Pacman integration
+
+In other words, it does the same things any AUR helper capable of.
+
+The following principles were the basis of the program:
+
+- Pacman-like user interface
+- Pacman options support (`--asdeps`, `--needed`, etc)
+- Pacman configuration support (output settings, ignored packages, etc)
+- Download, ask all questions, and only after that start building
+- No PKGBUILD sourcing
+
+## Examples
+
+- Build packages from sources: `pakku -S --build linux linux-headers`
+- Query all "dependency islands": `pakku -Qdttt`
diff --git a/completion/bash.in b/completion/bash.in
new file mode 100644
index 0000000..86abb69
--- /dev/null
+++ b/completion/bash.in
@@ -0,0 +1,61 @@
+make_import _arch_compgen
+
+make_import _arch_ptr2comp
+
+make_import _arch_incomp
+
+make_import _pacman_pkg
+
+make_import _pacman_file
+
+_pakku() {
+ local common core cur database files prev query remove sync upgrade o
+ COMPREPLY=()
+ _get_comp_words_by_ref cur prev
+ database=('asdeps asexplicit')
+ files=('list machinereadable owns search refresh regex' 'l o s x y')
+ query=('changelog check deps explicit file foreign groups info list owns
+ search unrequired upgrades' 'c e g i k l m o p s t u')
+ remove=('cascade dbonly nodeps assume-installed nosave print recursive unneeded' 'c n p s u')
+ sync=('asdeps asexplicit clean dbonly downloadonly force groups ignore ignoregroup
+ info list needed nodeps assume-installed print refresh recursive search sysupgrade
+ noaur build'
+ 'c g i l p s u w y')
+ upgrade=('asdeps asexplicit force needed nodeps assume-installed print recursive' 'p')
+ common=('arch cachedir color config confirm dbpath debug gpgdir help hookdir logfile
+ noconfirm noprogressbar noscriptlet quiet root verbose' 'b d h q r v')
+ core=('database files help query remove sync upgrade version' 'D F Q R S U V h')
+
+ for o in 'D database' 'F files' 'Q query' 'R remove' 'S sync' 'U upgrade'; do
+ _arch_incomp "$o" && break
+ done
+
+ if [[ $? != 0 ]]; then
+ _arch_ptr2comp core
+ elif [[ ! $prev =~ ^-\w*[Vbhr] &&
+ ! $prev = --@(cachedir|color|config|dbpath|help|hookdir|gpgdir|logfile|root|version) ]]
+ then
+ [[ $cur = -* ]] && _arch_ptr2comp ${o#* } common ||
+ case ${o% *} in
+ D|R)
+ _pacman_pkg Qq;;
+ F)
+ _arch_incomp 'l list' && _pacman_pkg Slq;
+ ;;
+ Q)
+ { _arch_incomp 'g groups' && _pacman_pkg Qg sort; } ||
+ { _arch_incomp 'p file' && _pacman_file; } ||
+ _arch_incomp 'o owns' || _arch_incomp 'u upgrades' ||
+ _pacman_pkg Qq;;
+ S)
+ { _arch_incomp 'g groups' && _pacman_pkg Sg; } ||
+ { _arch_incomp 'l list' && _pacman_pkg Sl sort; } ||
+ _arch_compgen "`pakku -Ssq "$cur" 2> /dev/null`";;
+ U)
+ _pacman_file;;
+ esac
+ fi
+ true
+}
+
+complete -F _pakku -o default pakku
diff --git a/completion/make.sh b/completion/make.sh
new file mode 100755
index 0000000..dd00d05
--- /dev/null
+++ b/completion/make.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+pacman_bash_completion='/usr/share/bash-completion/completions/pacman'
+
+(IFS=; while read -r line; do
+ [ "${line:0:12}" = 'make_import ' ] && {
+ grep -Poz '(?<=\n)'"${line:12}"'\(\) \{\n(.*\n)*?\}' "$pacman_bash_completion" |
+ xargs -0
+ } || {
+ echo "$line"
+ }
+done) < 'bash.in' > 'bash'
diff --git a/doc/asciidoc.conf b/doc/asciidoc.conf
new file mode 100644
index 0000000..1872c0d
--- /dev/null
+++ b/doc/asciidoc.conf
@@ -0,0 +1,8 @@
+[macros]
+(?su)[\\]?(?P<name>linkman):(?P<target>\S*?)\[(?P<attrlist>.*?)\]=
+
+[linkman-inlinemacro]
+{0%{target}}
+{0#<citerefentry>}
+{0#<refentrytitle>{target}</refentrytitle><manvolnum>{0}</manvolnum>}
+{0#</citerefentry>}
diff --git a/doc/pakku.8.txt b/doc/pakku.8.txt
new file mode 100644
index 0000000..5f183db
--- /dev/null
+++ b/doc/pakku.8.txt
@@ -0,0 +1,46 @@
+pakku(8)
+========
+
+Name
+----
+pakku - package manager utility wrapper
+
+Synopsis
+--------
+'pakku' <operation> [options] [targets]
+
+Description
+-----------
+Pakku is a pacman wrapper with additional features, such as AUR support.
+
+Operations and Options
+----------------------
+Pakku provides all operations and options from pacman. This page will focus
+on pakku-specific options only. See linkman:pacman[8] for additional
+information.
+
+Query Options (apply to '-Q')[[QO]]
+-----------------------------------
+*-t, \--unrequired*::
+ Specify this option thrice combining with '-d' to list dependencies
+ which are not required by any explicitly installed package.
+
+Sync Options (apply to '-S')[[SO]]
+----------------------------------
+*\--build*::
+ Build packages from source. Building is supported for directly specified
+ packages only, so it will not work for package groups or virtual packages.
+
+*\--noaur*::
+ Disable AUR support. This option is assumed when you use non-standard
+ root path, allow downgrading using twice specified '-u', skip dependency
+ checking using '-d', or run the program from root.
+
+Configuration
+-------------
+See linkman:pacman.conf[5] and linkman:pakku.conf[5] for details on configuring
+the program.
+
+See Also
+--------
+linkman:pacman[8], linkman:pacman.conf[5], linkman:pakku.conf[5]
diff --git a/doc/pakku.conf.5.txt b/doc/pakku.conf.5.txt
new file mode 100644
index 0000000..5ea2460
--- /dev/null
+++ b/doc/pakku.conf.5.txt
@@ -0,0 +1,41 @@
+pakku.conf(5)
+=============
+
+Name
+----
+pakku.conf - pakku configuration file
+
+Synopsis
+--------
+\{sysconfdir\}/pakku.conf
+
+Description
+-----------
+This configuration file uses a similar format to pacman.conf. The file is
+divided into sections. Only options section is used.
+
+Options
+-------
+*TmpDir =* path/to/tmp/dir::
+ Set the temporary directory in which pakku will perform all
+ building operations. The default value is +/tmp/pakku-$\{USER\}+.
+
+*AurComments*::
+ Download and display comments for AUR packages.
+
+*CheckIgnored*::
+ Check ignored packages for updates in AUR. Pakku will be able to warn
+ about ignored package upgrade.
+
+*PrintAurNotFound*::
+ Print warnings during upgrade operation when foreign packages were not
+ found in AUR.
+
+*ViewNoDefault*::
+ When building from AUR, pakku will ask whether you want to view the
+ content of PKGBUILD and other files. Pressing enter key will give the
+ positive answer unless this option is specified.
+
+See Also
+--------
+linkman:pacman.conf[5], linkman:pakku[8]
diff --git a/lib/bisect.nim b/lib/bisect.nim
new file mode 100644
index 0000000..53d73f1
--- /dev/null
+++ b/lib/bisect.nim
@@ -0,0 +1,77 @@
+import future, os, osproc, re, strutils
+
+{.passL: "-lalpm".}
+
+proc vercmp(a: cstring, b: cstring): cint
+ {.cdecl, importc: "alpm_pkg_vercmp".}
+
+proc getSourceVersion(relativePath: string): seq[string] =
+ let lines = execProcess("/bin/bash", ["-c",
+ """source "$1/PKGBUILD" && echo "$epoch" && echo "$pkgver" && echo "$pkgrel"""",
+ "bash", relativePath], options = {}).split("\n")
+ if lines.len == 4:
+ lines[0 .. 2]
+ else:
+ @[]
+
+proc getSrcInfoVersion(relativePath: string): seq[string] =
+ var file: File
+ var epoch = ""
+ var pkgver = ""
+ var pkgrel = ""
+
+ if file.open(relativePath & "/.SRCINFO"):
+ try:
+ var matches: array[2, string]
+ while true:
+ let line = file.readLine()
+ if line.match(re"[\t\ ]*(\w+)\ =\ (.*)", matches):
+ case matches[0]:
+ of "epoch":
+ epoch = matches[1]
+ of "pkgver":
+ pkgver = matches[1]
+ of "pkgrel":
+ pkgrel = matches[1]
+ else:
+ discard
+ except:
+ discard
+ finally:
+ file.close()
+
+ @[epoch, pkgver, pkgrel]
+
+let compareMethod = commandLineParams()[0]
+let relativePath = commandLineParams()[1]
+let compareVersion = commandLineParams()[2]
+
+let (currentVersion, supported) = if compareMethod == "source":
+ (getSourceVersion(relativePath), true)
+ elif compareMethod == "srcinfo":
+ (getSrcInfoVersion(relativePath), true)
+ else:
+ (@[], false)
+
+if not supported:
+ programResult = 255
+elif currentVersion.len != 3:
+ programResult = 125
+else:
+ let epoch = currentVersion[0].strip
+ let pkgver = currentVersion[1].strip
+ let pkgrel = currentVersion[2].strip
+
+ if pkgver.len == 0 or pkgrel.len == 0:
+ programResult = 125
+ else:
+ let version = if epoch.len > 0:
+ epoch & ":" & pkgver & "-" & pkgrel
+ else:
+ pkgver & "-" & pkgrel
+
+ echo(version)
+ if vercmp(compareVersion, version) > 0:
+ programResult = 0
+ else:
+ programResult = 1
diff --git a/pakku.conf b/pakku.conf
new file mode 100644
index 0000000..0b7e200
--- /dev/null
+++ b/pakku.conf
@@ -0,0 +1,9 @@
+# See the pakku.conf(5) manpage
+
+[options]
+#TmpDir = /tmp/pakku-${USER}
+
+AurComments
+CheckIgnored
+PrintAurNotFound
+#ViewNoDefault
diff --git a/src/args.nim b/src/args.nim
new file mode 100644
index 0000000..12b31b3
--- /dev/null
+++ b/src/args.nim
@@ -0,0 +1,175 @@
+import
+ future, options, os, posix, sequtils, sets, strutils,
+ utils
+
+type
+ ArgumentType* {.pure.} = enum
+ short, long, target
+
+ Argument* = tuple[
+ key: string,
+ value: Option[string],
+ atype: ArgumentType
+ ]
+
+ OptionPair* = tuple[
+ short: Option[string],
+ long: string
+ ]
+
+ OptionKey* = tuple[
+ key: string,
+ long: bool
+ ]
+
+iterator readLines(): string =
+ try:
+ while true:
+ yield readLine(stdin)
+ except:
+ discard
+
+iterator splitSingle(valueFull: string, optionsWithParameter: HashSet[OptionKey],
+ next: Option[string]): tuple[key: string, value: Option[string], consumedNext: bool] =
+ var i = 0
+ while i < valueFull.len:
+ let key = $valueFull[i]
+ if (key, false) in optionsWithParameter:
+ if i == valueFull.high:
+ if next.isNone:
+ raise commandError(trc("%s: option requires an argument -- '%c'\n").strip
+ .replace("%s", "$#").replace("%c", "$#") % [getAppFilename(), key],
+ showError = false)
+ else:
+ yield (key, next, true)
+ else:
+ yield (key, some(valueFull[i + 1 .. ^1]), false)
+ i = valueFull.len
+ else:
+ yield (key, none(string), false)
+ i += 1
+
+proc splitArgs*(params: seq[string],
+ optionsWithParameter: HashSet[OptionKey]): seq[Argument] =
+ proc handleCurrentNext(current: string, next: Option[string],
+ stdinConsumed: bool, endOfOpts: bool): (seq[Argument], Option[string], bool, bool) =
+ if current == "-":
+ if stdinConsumed or isatty(0) == 1:
+ raise commandError(trp("argument '-' specified without input on stdin\n").strip)
+ else:
+ let args = lc[x | (y <- readLines(), x <- y.splitWhitespace), string]
+ .map(s => (s, none(string), ArgumentType.target))
+
+ return (args, next, true, endOfOpts)
+ elif endOfOpts:
+ return (@[(current, none(string), ArgumentType.target)], next, stdinConsumed, true)
+ elif current == "--":
+ return (@[], next, stdinConsumed, true)
+ elif current[0 .. 1] == "--":
+ let valueFull = current[2 .. ^1]
+ let index = valueFull.find("=")
+ let key = if index >= 0: valueFull[0 .. index - 1] else: valueFull
+ let valueOption = if index >= 0: some(valueFull[index + 1 .. ^1]) else: none(string)
+
+ if (key, true) in optionsWithParameter:
+ if valueOption.isSome:
+ return (@[(key, valueOption, ArgumentType.long)], next, stdinConsumed, false)
+ elif next.isSome:
+ return (@[(key, next, ArgumentType.long)], none(string), stdinConsumed, false)
+ else:
+ raise commandError(trc("%s: option '%s%s' requires an argument\n").strip
+ .replace("%s", "$#") % [getAppFilename(), "--", key], showError = false)
+ elif valueOption.isSome:
+ raise commandError(trc("%s: option '%s%s' doesn't allow an argument\n").strip
+ .replace("%s", "$#") % [getAppFilename(), "--", key], showError = false)
+ else:
+ return (@[(key, none(string), ArgumentType.long)], next, stdinConsumed, false)
+ elif current[0] == '-' and current.len >= 2:
+ let argsResult = toSeq(splitSingle(current[1 .. ^1], optionsWithParameter, next))
+ let consumedNext = argsResult.map(a => a.consumedNext).foldl(a or b)
+ let newNext = next.filter(n => not consumedNext)
+
+ return (lc[(x.key, x.value, ArgumentType.short) | (x <- argsResult), Argument],
+ newNext, stdinConsumed, false)
+ else:
+ return (@[(current, none(string), ArgumentType.target)], next, stdinConsumed, false)
+
+ type
+ ParseArgument = tuple[arg: Option[Argument],current: Option[string]]
+ ParseCycle = tuple[args: seq[ParseArgument], stdinConsumed: bool, endOfOpts: bool]
+
+ proc buildArgs(input: ParseCycle, next: Option[string]): ParseCycle =
+ if input.args.len == 0:
+ (@[(none(Argument), next)], input.stdinConsumed, input.endOfOpts)
+ else:
+ let last = input.args[^1]
+ if last.current.isSome:
+ let handleResult: tuple[args: seq[Argument], next: Option[string],
+ stdinConsumed: bool, endOfOpts: bool] = handleCurrentNext(last.current.unsafeGet,
+ next, input.stdinConsumed, input.endOfOpts)
+
+ let append = handleResult.args.map(a => (some(a), none(string)))
+ (input.args[0 .. ^2] & append & (none(Argument), handleResult.next),
+ handleResult.stdinConsumed, handleResult.endOfOpts)
+ elif next.isSome:
+ (input.args & (none(Argument), next), input.stdinConsumed, input.endOfOpts)
+ else:
+ input
+
+ let cycle: ParseCycle = (params.map(some) & none(string))
+ .foldl(buildArgs(a, b), (newSeq[ParseArgument](), false, false))
+
+ if cycle.stdinConsumed:
+ discard close(0)
+ discard open("/dev/tty", O_RDONLY)
+
+ lc[x | (y <- cycle.args, x <- y.arg), Argument]
+
+proc isShort*(arg: Argument): bool = arg.atype == ArgumentType.short
+proc isLong*(arg: Argument): bool = arg.atype == ArgumentType.long
+proc isTarget*(arg: Argument): bool = arg.atype == ArgumentType.target
+
+proc collectArg*(arg: Argument): seq[string] =
+ if arg.isShort:
+ let key = "-" & arg.key
+ arg.value.map(v => @[key, v]).get(@[key])
+ elif arg.isLong:
+ let key = "--" & arg.key
+ arg.value.map(v => @[key, v]).get(@[key])
+ elif arg.isTarget:
+ @[arg.key]
+ else:
+ @[]
+
+proc len*(op: OptionPair): int =
+ if op.short.isSome: 2 else: 1
+
+iterator items*(op: OptionPair): OptionKey =
+ if op.short.isSome:
+ yield (op.short.unsafeGet, false)
+ yield (op.long, true)
+
+proc filter*(args: seq[Argument], removeMatches: bool, keepTargets: bool,
+ pairs: varargs[OptionPair]): seq[Argument] =
+ let pairsSeq = @pairs
+ let argsSet = lc[x | (y <- pairsSeq, x <- y), OptionKey].toSet
+
+ args.filter(arg => (arg.isShort and (removeMatches xor (arg.key, false) in argsSet)) or
+ (arg.isLong and (removeMatches xor (arg.key, true) in argsSet)) or
+ (arg.isTarget and keepTargets))
+
+template count*(args: seq[Argument], pairs: varargs[OptionPair]): int =
+ args.filter(false, false, pairs).len
+
+template check*(args: seq[Argument], pairs: varargs[OptionPair]): bool =
+ args.count(pairs) > 0
+
+template whitelisted*(args: seq[Argument], pairs: varargs[OptionPair]): bool =
+ args.filter(true, false, pairs).len == 0
+
+proc matchOption*(arg: Argument, pair: OptionPair): bool =
+ (arg.isShort and pair.short.map(o => o == arg.key).get(false)) or
+ (arg.isLong and arg.key == pair.long)
+
+proc targets*(args: seq[Argument]): seq[string] =
+ args.filter(isTarget).map(a => a.key)
diff --git a/src/aur.nim b/src/aur.nim
new file mode 100644
index 0000000..acf9f96
--- /dev/null
+++ b/src/aur.nim
@@ -0,0 +1,192 @@
+import
+ future, json, options, re, sequtils, sets, strutils, tables,
+ package, utils,
+ "wrapper/curl"
+
+type
+ AurComment* = tuple[
+ author: string,
+ date: string,
+ text: string
+ ]
+
+const
+ aurUrl* = "https://aur.archlinux.org/"
+
+proc parseRpcPackageInfo(obj: JsonNode): Option[RpcPackageInfo] =
+ template optInt64(i: int64): Option[int64] =
+ if i > 0: some(i) else: none(int64)
+
+ let base = obj["PackageBase"].getStr
+ let name = obj["Name"].getStr
+ let version = obj["Version"].getStr
+ let descriptionEmpty = obj["Description"].getStr
+ let description = if descriptionEmpty.len > 0: some(descriptionEmpty) else: none(string)
+ let maintainerEmpty = obj["Maintainer"].getStr
+ let maintainer = if maintainerEmpty.len > 0: some(maintainerEmpty) else: none(string)
+ let firstSubmitted = obj["FirstSubmitted"].getBiggestInt(0).optInt64
+ let lastModified = obj["LastModified"].getBiggestInt(0).optInt64
+ let votes = (int) obj["NumVotes"].getBiggestInt(0)
+ let popularity = obj["Popularity"].getFloat(0)
+
+ if base.len > 0 and name.len > 0:
+ some(RpcPackageInfo(repo: "aur", base: base, name: name, version: version,
+ description: description, maintainer: maintainer,
+ firstSubmitted: firstSubmitted, lastModified: lastModified,
+ votes: votes, popularity: popularity))
+ else:
+ none(RpcPackageInfo)
+
+template withAur*(body: untyped): untyped =
+ withCurlGlobal():
+ body
+
+proc obtainPkgBaseSrcInfo(base: string): (string, Option[string]) =
+ try:
+ withAur():
+ withCurl(instance):
+ let url = aurUrl & "cgit/aur.git/plain/.SRCINFO?h=" &
+ instance.escape(base)
+ (performString(url), none(string))
+ except CurlError:
+ ("", some(getCurrentException().msg))
+
+proc getRpcPackageInfo*(pkgs: seq[string]): (seq[RpcPackageInfo], Option[string]) =
+ if pkgs.len == 0:
+ (@[], none(string))
+ else:
+ withAur():
+ try:
+ withCurl(instance):
+ let url = aurUrl & "rpc/?v=5&type=info&arg[]=" & @pkgs
+ .deduplicate
+ .map(u => instance.escape(u))
+ .foldl(a & "&arg[]=" & b)
+
+ let response = performString(url)
+ let results = parseJson(response)["results"]
+ let table = lc[(x.name, x) | (y <- results, x <- parseRpcPackageInfo(y)),
+ (string, RpcPackageInfo)].toTable
+ (lc[x | (p <- pkgs, x <- table.opt(p)), RpcPackageInfo], none(string))
+ except CurlError:
+ (@[], some(getCurrentException().msg))
+ except JsonParsingError:
+ (@[], some(tr"failed to parse server response"))
+
+proc getAurPackageInfo*(pkgs: seq[string], rpcInfosOption: Option[seq[RpcPackageInfo]],
+ progressCallback: (int, int) -> void): (seq[PackageInfo], seq[string]) =
+ if pkgs.len == 0:
+ (@[], @[])
+ else:
+ withAur():
+ progressCallback(0, pkgs.len)
+
+ let (rpcInfos, error) = if rpcInfosOption.isSome:
+ (rpcInfosOption.unsafeGet, none(string))
+ else:
+ getRpcPackageInfo(pkgs)
+
+ if error.isSome:
+ (@[], @[error.unsafeGet])
+ else:
+ type
+ ParseResult = tuple[
+ infos: seq[PackageInfo],
+ error: Option[string]
+ ]
+
+ let deduplicated = lc[x.base | (x <- rpcInfos), string].deduplicate
+ progressCallback(0, deduplicated.len)
+
+ proc obtainAndParse(base: string, index: int): ParseResult =
+ let (srcInfo, operror) = obtainPkgBaseSrcInfo(base)
+ progressCallback(index + 1, deduplicated.len)
+
+ if operror.isSome:
+ (@[], operror)
+ else:
+ let pkgInfos = parseSrcInfo("aur", srcInfo,
+ aurUrl & base & ".git", none(string), none(string), none(string), rpcInfos)
+ (pkgInfos, none(string))
+
+ let parsed = deduplicated.foldl(a & obtainAndParse(b, a.len), newSeq[ParseResult]())
+ let infos = lc[x | (y <- parsed, x <- y.infos), PackageInfo]
+ let errors = lc[x | (y <- parsed, x <- y.error), string]
+
+ let table = infos.map(i => (i.name, i)).toTable
+ (lc[x | (p <- pkgs, x <- table.opt(p)), PackageInfo], errors)
+
+proc findAurPackages*(query: seq[string]): (seq[RpcPackageInfo], Option[string]) =
+ if query.len == 0 or query[0].len <= 2:
+ (@[], none(string))
+ else:
+ withAur():
+ try:
+ withCurl(instance):
+ let url = aurUrl & "rpc/?v=5&type=search&by=name&arg=" &
+ instance.escape(query[0])
+
+ let response = performString(url)
+ let results = parseJson(response)["results"]
+ let rpcInfos = lc[x | (y <- results, x <- parseRpcPackageInfo(y)), RpcPackageInfo]
+
+ let filteredRpcInfos = if query.len > 1: (block:
+ let queryLow = query[1 .. ^1].map(q => q.toLowerAscii)
+ rpcInfos.filter(i => queryLow.map(q => i.name.toLowerAscii.contains(q) or
+ i.description.map(d => d.toLowerAscii.contains(q)).get(false)).foldl(a and b)))
+ else:
+ rpcInfos
+
+ (filteredRpcInfos, none(string))
+ except CurlError:
+ (@[], some(getCurrentException().msg))
+
+proc downloadAurComments*(base: string): (seq[AurComment], Option[string]) =
+ let (content, error) = withAur():
+ try:
+ withCurl(instance):
+ let url = aurUrl & "pkgbase/" & base & "/?comments=all"
+ (performString(url), none(string))
+ except CurlError:
+ ("", some(getCurrentException().msg))
+
+ if error.isSome:
+ (@[], error)
+ else:
+ let commentRe = re("<h4\\ id=\"comment-\\d+\">\\n\\s+(.*)?\\ commented\\ on\\ " &
+ "(.*)\\n(?:.*\\n)*?\\s+</h4>\\n\\t\\t<div\\ id=\"comment-\\d+-content\"\\ " &
+ "class=\"article-content\">((?:\\n.*)*?)\\n\\t\\t</div>")
+
+ proc transformComment(comment: string): string =
+ comment
+ # line breaks can leave a space
+ .replace("\n", " ")
+ # force line break
+ .replace("<br />", "\n")
+ # paragraphs look like 2 line breaks
+ .replace("<p>", "\n\n")
+ .replace("</p>", "\n\n")
+ # remove tags
+ .replace(re"<.*?>", "")
+ # multiple spaces become 1 spage
+ .replace(re"\ {2,}", " ")
+ # strip lines
+ .strip.split("\n").map(s => s.strip).foldl(a & "\n" & b).strip
+ # don't allow more than 2 line breaks
+ .replace(re"\n{2,}", "\n\n")
+ # replace mnemonics
+ .replace("&lt;", "<")
+ .replace("&gt;", ">")
+ .replace("&quot;", "\"")
+ .replace("&amp;", "&")
+
+ proc findAllMatches(start: int, found: seq[AurComment]): seq[AurComment] =
+ var matches: array[3, string]
+ let index = content.find(commentRe, matches, start)
+ if index >= 0:
+ findAllMatches(index + 1, found & (matches[0].strip, matches[1].strip,
+ transformComment(matches[2])))
+ else:
+ found
+
+ (findAllMatches(0, @[]), none(string))
diff --git a/src/common.nim b/src/common.nim
new file mode 100644
index 0000000..e17c802
--- /dev/null
+++ b/src/common.nim
@@ -0,0 +1,392 @@
+import
+ future, options, os, osproc, posix, sequtils, sets, strutils, tables,
+ args, config, package, pacman, utils,
+ "wrapper/alpm"
+
+type
+ SyncFoundPackageInfo* = tuple[
+ base: string,
+ version: string,
+ arch: Option[string]
+ ]
+
+ SyncFoundInfo* = tuple[
+ repo: string,
+ pkg: Option[SyncFoundPackageInfo]
+ ]
+
+ PackageTarget* = object of RootObj
+ name*: string
+ repo*: Option[string]
+
+ SyncPackageTarget* = object of PackageTarget
+ foundInfo*: Option[SyncFoundInfo]
+
+ FullPackageTarget*[T] = object of SyncPackageTarget
+ pkgInfo*: Option[T]
+
+proc toPackageReference*(dependency: ptr AlpmDependency): PackageReference =
+ let op = case dependency.depmod:
+ of AlpmDepMod.eq: some(ConstraintOperation.eq)
+ of AlpmDepMod.ge: some(ConstraintOperation.ge)
+ of AlpmDepMod.le: some(ConstraintOperation.le)
+ of AlpmDepMod.gt: some(ConstraintOperation.gt)
+ of AlpmDepMod.lt: some(ConstraintOperation.lt)
+ else: none(ConstraintOperation)
+
+ let description = if dependency.desc != nil: some($dependency.desc) else: none(string)
+ ($dependency.name, description, op.map(o => (o, $dependency.version)))
+
+proc checkConstraints(lop: ConstraintOperation, rop: ConstraintOperation, cmp: int): bool =
+ let (x1, x2) = if cmp > 0:
+ (1, -1)
+ elif cmp < 0:
+ (-1, 1)
+ else:
+ (0, 0)
+
+ proc c(op: ConstraintOperation, x1: int, x2: int): bool =
+ case op:
+ of ConstraintOperation.eq: x1 == x2
+ of ConstraintOperation.ge: x1 >= x2
+ of ConstraintOperation.le: x1 <= x2
+ of ConstraintOperation.gt: x1 > x2
+ of ConstraintOperation.lt: x1 < x2
+
+ template a(x: int): bool = lop.c(x, x1) and rop.c(x, x2)
+
+ a(2) or a(1) or a(0) or a(-1) or a(-2)
+
+proc isProvidedBy*(package: PackageReference, by: PackageReference): bool =
+ if package.name == by.name:
+ if package.constraint.isNone or by.constraint.isNone:
+ true
+ else:
+ let lcon = package.constraint.unsafeGet
+ let rcon = package.constraint.unsafeGet
+ let cmp = vercmp(lcon.version, rcon.version)
+ checkConstraints(lcon.operation, rcon.operation, cmp)
+ else:
+ false
+
+proc checkAndRefresh*(color: bool, args: seq[Argument]): tuple[code: int, args: seq[Argument]] =
+ let refreshCount = args.count((some("y"), "refresh"))
+ if refreshCount > 0:
+ let code = pacmanRun(true, color, args
+ .keepOnlyOptions(commonOptions, upgradeCommonOptions) &
+ ("S", none(string), ArgumentType.short) &
+ ("y", none(string), ArgumentType.short).repeat(refreshCount))
+
+ let callArgs = args
+ .filter(arg => not arg.matchOption((some("y"), "refresh")))
+ (code, callArgs)
+ else:
+ (0, args)
+
+proc packageTargets*(args: seq[Argument]): seq[PackageTarget] =
+ args.targets.map(proc (target: string): PackageTarget =
+ let splitTarget = target.split('/', 2)
+ if splitTarget.len == 2:
+ PackageTarget(name: splitTarget[1], repo: some(splitTarget[0]))
+ else:
+ PackageTarget(name: target, repo: none(string)))
+
+proc isAurTargetSync*(target: SyncPackageTarget): bool =
+ target.foundInfo.isNone and (target.repo.isNone or target.repo == some("aur"))
+
+proc isAurTargetFull*[T: RpcPackageInfo](target: FullPackageTarget[T]): bool =
+ target.foundInfo.isSome and target.foundInfo.unsafeGet.repo == "aur"
+
+proc findSyncTargets*(handle: ptr AlpmHandle, dbs: seq[ptr AlpmDatabase],
+ targets: seq[PackageTarget], allowGroups: bool, checkProvides: bool):
+ (seq[SyncPackageTarget], seq[string]) =
+ let dbTable = dbs.map(d => ($d.name, d)).toTable
+
+ proc checkProvided(name: string, db: ptr AlpmDatabase): bool =
+ for pkg in db.packages:
+ for provides in pkg.provides:
+ if $provides.name == name:
+ return true
+ return false
+
+ proc findSync(target: PackageTarget): Option[SyncFoundInfo] =
+ if target.repo.isSome:
+ let repo = target.repo.unsafeGet
+
+ if dbTable.hasKey(repo):
+ let db = dbTable[repo]
+ let pkg = db[target.name]
+
+ if pkg != nil:
+ let base = if pkg.base == nil: target.name else: $pkg.base
+ return some((repo, some((base, $pkg.version, some($pkg.arch)))))
+ elif checkProvides and target.name.checkProvided(db):
+ return some((repo, none(SyncFoundPackageInfo)))
+ else:
+ return none(SyncFoundInfo)
+ else:
+ return none(SyncFoundInfo)
+ else:
+ let directResult = dbs
+ .map(db => (block:
+ let pkg = db[target.name]
+ if pkg != nil:
+ let base = if pkg.base == nil: target.name else: $pkg.base
+ some(($db.name, some((base, $pkg.version, some($pkg.arch)))))
+ else:
+ none(SyncFoundInfo)))
+ .filter(i => i.isSome)
+ .optFirst
+ .flatten
+
+ if directResult.isSome:
+ return directResult
+ else:
+ if allowGroups:
+ let groupRepo = lc[d | (d <- dbs, g <- d.groups, $g.name == target.name),
+ ptr AlpmDatabase].optFirst
+ if groupRepo.isSome:
+ return groupRepo.map(d => ($d.name, none(SyncFoundPackageInfo)))
+
+ if checkProvides:
+ for db in dbs:
+ if target.name.checkProvided(db):
+ return some(($db.name, none(SyncFoundPackageInfo)))
+ return none(SyncFoundInfo)
+ else:
+ return none(SyncFoundInfo)
+
+ let syncTargets = targets.map(t => SyncPackageTarget(name: t.name,
+ repo: t.repo, foundInfo: findSync(t)))
+ let checkAur = syncTargets.filter(isAurTargetSync).map(t => t.name)
+ (syncTargets, checkAur)
+
+proc mapAurTargets*[T: RpcPackageInfo](targets: seq[SyncPackageTarget],
+ pkgInfos: seq[T]): seq[FullPackageTarget[T]] =
+ let aurTable = pkgInfos.map(i => (i.name, i)).toTable
+
+ targets.map(proc (target: SyncPackageTarget): FullPackageTarget[T] =
+ if target.foundInfo.isNone and aurTable.hasKey(target.name):
+ let pkgInfo = aurTable[target.name]
+ let syncInfo = ("aur", some((pkgInfo.base, pkgInfo.version, none(string))))
+ FullPackageTarget[T](name: target.name, repo: target.repo,
+ foundInfo: some(syncInfo), pkgInfo: some(pkgInfo))
+ else:
+ FullPackageTarget[T](name: target.name, repo: target.repo,
+ foundInfo: target.foundInfo, pkgInfo: none(T)))
+
+proc formatArgument*(target: PackageTarget): string =
+ target.repo.map(r => r & "/" & target.name).get(target.name)
+
+proc ensureTmpOrError*(config: Config): Option[string] =
+ let tmpRootExists = try:
+ discard config.tmpRoot.existsOrCreateDir()
+ true
+ except:
+ false
+
+ if not tmpRootExists:
+ some(tr"failed to create tmp directory '$#'" % [config.tmpRoot])
+ else:
+ none(string)
+
+proc bisectVersion(repoPath: string, debug: bool, firstCommit: Option[string],
+ compareMethod: string, relativePath: string, version: string): Option[string] =
+ template forkExecWithoutOutput(args: varargs[string]): int =
+ forkWait(() => (block:
+ discard close(0)
+ if not debug:
+ discard close(1)
+ discard close(2)
+
+ execResult(args)))
+
+ let (workFirstCommit, checkFirst) = if firstCommit.isSome:
+ (firstCommit, false)
+ else:
+ (runProgram(gitCmd, "-C", repoPath,
+ "rev-list", "--max-parents=0", "@").optLast, true)
+ let realLastThreeCommits = runProgram(gitCmd, "-C", repoPath,
+ "rev-list", "--max-count=3", "@")
+ let index = workFirstCommit.map(c => realLastThreeCommits.find(c)).get(-1)
+ let lastThreeCommits = if index >= 0:
+ realLastThreeCommits[0 .. index]
+ else:
+ realLastThreeCommits
+
+ proc checkCommit(commit: string): Option[string] =
+ let checkout1Code = forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "checkout", commit)
+
+ if checkout1Code != 0:
+ none(string)
+ else:
+ let foundVersion = runProgram(pkgLibDir & "/bisect",
+ compareMethod, repoPath & "/" & relativePath, version).optFirst
+ let checkout2Code = forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "checkout", lastThreeCommits[0])
+
+ if checkout2Code != 0:
+ none(string)
+ elif foundVersion == some(version):
+ some(commit)
+ else:
+ none(string)
+
+ if lastThreeCommits.len == 0:
+ none(string)
+ elif lastThreeCommits.len == 1:
+ if checkFirst:
+ checkCommit(lastThreeCommits[0])
+ else:
+ none(string)
+ elif lastThreeCommits.len == 2:
+ let checkedCommit = checkCommit(lastThreeCommits[0])
+ if checkedCommit.isSome:
+ checkedCommit
+ elif checkFirst:
+ checkCommit(lastThreeCommits[1])
+ else:
+ none(string)
+ else:
+ # find the commit with specific package version using git bisect
+ let bisectStartCode = forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "bisect", "start", "@", workFirstCommit.get(""))
+
+ if bisectStartCode != 0:
+ none(string)
+ else:
+ discard forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "bisect", "run", pkgLibDir & "/bisect", compareMethod, relativePath, version)
+
+ let commit = runProgram(gitCmd, "-C", repoPath,
+ "rev-list", "--max-count=1", "refs/bisect/bad").optFirst
+
+ discard forkExecWithoutOutput(gitCmd, "-C", repoPath,
+ "bisect", "reset")
+
+ if commit.isSome:
+ let checkedCommit = commit.map(checkCommit).flatten
+ if checkedCommit.isSome:
+ checkedCommit
+ else:
+ # non-incremental git history (e.g. downgrade without epoch change), bisect again
+ bisectVersion(repoPath, debug, commit, compareMethod, relativePath, version)
+ elif checkFirst and workFirstCommit.isSome:
+ checkCommit(workFirstCommit.unsafeGet)
+ else:
+ none(string)
+
+proc obtainBuildPkgInfos*(config: Config,
+ pacmanTargets: seq[FullPackageTarget[RpcPackageInfo]]): (seq[PackageInfo], seq[string]) =
+ type
+ LookupBaseGroup = tuple[
+ base: string,
+ version: string,
+ arch: string,
+ repo: string
+ ]
+
+ LookupGitResult = tuple[
+ group: LookupBaseGroup,
+ git: Option[GitRepo]
+ ]
+
+ let bases: seq[LookupBaseGroup] = pacmanTargets
+ .map(target => (block:
+ let info = target.foundInfo.get
+ let pkg = info.pkg.get
+ (pkg.base, pkg.version, pkg.arch.get, info.repo)))
+ .deduplicate
+
+ let lookupResults: seq[LookupGitResult] = bases
+ .map(b => (b, lookupGitRepo(b.repo, b.base, b.arch)))
+ let notFoundRepos = lookupResults.filter(r => r.git.isNone)
+
+ if notFoundRepos.len > 0:
+ let messages = notFoundRepos.map(r => tr"$#: repository not found" % [r.group.base])
+ (newSeq[PackageInfo](), messages)
+ else:
+ let message = ensureTmpOrError(config)
+ if message.isSome:
+ (@[], @[message.unsafeGet])
+ else:
+ proc findCommitAndGetSrcInfo(base: string, version: string,
+ repo: string, git: GitRepo): seq[PackageInfo] =
+ let repoPath = repoPath(config.tmpRoot, base)
+ removeDirQuiet(repoPath)
+
+ try:
+ if forkWait(() => execResult(gitCmd, "-C", config.tmpRoot,
+ "clone", "-q", git.url, "-b", git.branch,
+ "--single-branch", base)) == 0:
+ let commit = bisectVersion(repoPath, config.debug, none(string),
+ "source", git.path, version)
+
+ if commit.isNone:
+ @[]
+ else:
+ discard forkWait(() => execResult(gitCmd, "-C", repoPath,
+ "checkout", "-q", commit.unsafeGet))
+ let output = execProcess(bashCmd, ["-c",
+ """cd "$2/$3" && "$1" --printsrcinfo""",
+ "bash", makePkgCmd, repoPath, git.path], options = {})
+ parseSrcInfo(repo, output, git.url, some(git.branch), commit, some(git.path))
+ .filter(i => i.version == version)
+ else:
+ @[]
+ finally:
+ removeDirQuiet(repoPath)
+
+ let pkgInfos = lc[x | (r <- lookupResults, x <- findCommitAndGetSrcInfo(r.group.base,
+ r.group.version, r.group.repo, r.git.get)), PackageInfo]
+
+ let pkgInfosTable = pkgInfos.map(i => (i.name, i)).toTable
+
+ let foundPkgInfos = lc[x | (y <- pacmanTargets,
+ x <- pkgInfosTable.opt(y.name)), PackageInfo]
+ let messages = pacmanTargets
+ .filter(t => not pkgInfosTable.hasKey(t.name))
+ .map(t => tr"$#: failed to get package info" % [t.name])
+
+ discard rmdir(config.tmpRoot)
+ (foundPkgInfos, messages)
+
+proc cloneRepo*(config: Config, basePackages: seq[PackageInfo]): (int, Option[string]) =
+ let base = basePackages[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+
+ let message = ensureTmpOrError(config)
+ if message.isSome:
+ (1, message)
+ elif repoPath.existsDir():
+ (0, none(string))
+ else:
+ let gitUrl = basePackages[0].gitUrl
+ let gitBranch = basePackages[0].gitBranch
+ let gitCommit = basePackages[0].gitCommit
+ let aur = basePackages[0].repo == "aur"
+ let branch = gitBranch.get("master")
+
+ let cloneCode = forkWait(() => execResult(gitCmd, "-C", config.tmpRoot,
+ "clone", "-q", gitUrl, "-b", branch, "--single-branch", base))
+
+ if cloneCode == 0:
+ if gitCommit.isSome:
+ let code = forkWait(() => execResult(gitCmd, "-C", repoPath,
+ "reset", "-q", "--hard", gitCommit.unsafeGet))
+ (code, none(string))
+ elif aur: (block:
+ let commit = bisectVersion(repoPath, config.debug, none(string),
+ "srcinfo", basePackages[0].gitPath.get("."), basePackages[0].version)
+
+ if commit.isSome:
+ let code = forkWait(() => execResult(gitCmd, "-C", repoPath,
+ "reset", "-q", "--hard", commit.unsafeGet))
+ (code, none(string))
+ else:
+ (1, none(string)))
+ else:
+ (1, none(string))
+ else:
+ (cloneCode, none(string))
diff --git a/src/config.nim b/src/config.nim
new file mode 100644
index 0000000..abcf376
--- /dev/null
+++ b/src/config.nim
@@ -0,0 +1,125 @@
+import
+ future, options, posix, re, sets, strutils, tables,
+ utils
+
+type
+ ColorMode* {.pure.} = enum
+ colorNever = "never",
+ colorAuto = "auto",
+ colorAlways = "always"
+
+ CommonConfig* = object of RootObj
+ dbs*: seq[string]
+ arch*: string
+ debug*: bool
+ progressBar*: bool
+ verbosePkgList*: bool
+ ignorePkgs*: HashSet[string]
+ ignoreGroups*: HashSet[string]
+
+ PacmanConfig* = object of CommonConfig
+ rootOption*: Option[string]
+ dbOption*: Option[string]
+ colorMode*: ColorMode
+
+ Config* = object of CommonConfig
+ root*: string
+ db*: string
+ tmpRoot*: string
+ color*: bool
+ aurComments*: bool
+ checkIgnored*: bool
+ printAurNotFound*: bool
+ viewNoDefault*: bool
+
+proc readConfigFile*(configFile: string):
+ (OrderedTable[string, ref Table[string, string]], bool) =
+ var file: File
+ var table = initOrderedTable[string, ref Table[string, string]]()
+ var category: ref Table[string, string]
+ var currentCategory = ""
+
+ let wasError = if file.open(configFile):
+ try:
+ var matches: array[2, string]
+
+ while true:
+ let rawLine = readLine(file).strip(leading = false, trailing = true)
+ let commentIndex = rawLine.find('#')
+ let line = if commentIndex >= 0:
+ rawLine[0 .. commentIndex - 1].strip(leading = false, trailing = true)
+ else:
+ rawLine
+
+ if line.len > 0:
+ if line.match(re"\[(.*)\]", matches):
+ currentCategory = matches[0]
+ if table.hasKey(currentCategory):
+ category = table[currentCategory]
+ else:
+ category = newTable[string, string]()
+ table[currentCategory] = category
+ elif currentCategory.len > 0:
+ if line.match(re"\ *(\w+)\ *=\ *(.*)", matches):
+ category[].add(matches[0], matches[1])
+ else:
+ category[].add(line.strip(leading = true, trailing = false), "")
+
+ false
+ except EOFError:
+ false
+ except IOError:
+ true
+ finally:
+ file.close()
+ else:
+ true
+
+ (table, wasError)
+
+proc ignored*(config: Config, name: string, groups: openArray[string]): bool =
+ name in config.ignorePkgs or (config.ignoreGroups * groups.toSet).len > 0
+
+proc isRootDefault*(config: Config): bool =
+ config.root == "/"
+
+proc get*(colorMode: ColorMode): bool =
+ case colorMode:
+ of ColorMode.colorNever: false
+ of ColorMode.colorAlways: true
+ of ColorMode.colorAuto: isatty(1) == 1
+
+proc root*(config: PacmanConfig): string =
+ config.rootOption.get("/")
+
+proc db*(config: PacmanConfig): string =
+ if config.dbOption.isSome:
+ config.dbOption.unsafeGet
+ else:
+ let root = config.root
+ let workRoot = if root == "/": "" else: root
+ workRoot & localStateDir & "/lib/pacman/"
+
+proc obtainConfig*(config: PacmanConfig): Config =
+ let (configTable, _) = readConfigFile(sysConfDir & "/pakku.conf")
+ let options = configTable.opt("options").map(t => t[]).get(initTable[string, string]())
+
+ let root = config.root
+ let db = config.db
+ let color = config.colorMode.get
+
+ let (userId, userName) = getUser()
+ let tmpRoot = options.opt("TmpDir").get("/tmp/pakku-${USER}")
+ .replace("${UID}", $userId)
+ .replace("${USER}", userName)
+ let aurComments = options.hasKey("AurComments")
+ let checkIgnored = options.hasKey("CheckIgnored")
+ let printAurNotFound = options.hasKey("PrintAurNotFound")
+ let viewNoDefault = options.hasKey("ViewNoDefault")
+
+ Config(root: root, db: db, tmpRoot: tmpRoot, color: color,
+ dbs: config.dbs, arch: config.arch, debug: config.debug,
+ progressBar: config.progressBar, verbosePkgList: config.verbosePkgList,
+ ignorePkgs: config.ignoreGroups, ignoreGroups: config.ignoreGroups,
+ aurComments: aurComments, checkIgnored: checkIgnored,
+ printAurNotFound: printAurNotFound, viewNoDefault: viewNoDefault)
diff --git a/src/feature/localquery.nim b/src/feature/localquery.nim
new file mode 100644
index 0000000..b07ec98
--- /dev/null
+++ b/src/feature/localquery.nim
@@ -0,0 +1,81 @@
+import
+ algorithm, future, options, sequtils, sets, strutils, tables,
+ "../args", "../common", "../config", "../format", "../package", "../pacman", "../utils",
+ "../wrapper/alpm"
+
+proc handleQueryOrphans*(args: seq[Argument], config: Config): int =
+ type Package = tuple[name: string, explicit: bool]
+
+ let (installed, alternatives) = withAlpm(config.root, config.db,
+ newSeq[string](), config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ var installed = initTable[Package, HashSet[PackageReference]]()
+ var alternatives = initTable[string, HashSet[PackageReference]]()
+
+ for pkg in handle.local.packages:
+ proc fixProvides(reference: PackageReference): PackageReference =
+ if reference.constraint.isNone:
+ (reference.name, reference.description,
+ some((ConstraintOperation.eq, $pkg.version)))
+ else:
+ reference
+
+ let depends = toSeq(pkg.depends.items)
+ .map(toPackageReference).toSet
+ let optional = toSeq(pkg.optional.items)
+ .map(toPackageReference).toSet
+ let provides = toSeq(pkg.provides.items)
+ .map(toPackageReference).map(fixProvides).toSet
+
+ installed.add(($pkg.name, pkg.reason == AlpmReason.explicit),
+ depends + optional)
+ if provides.len > 0:
+ alternatives.add($pkg.name, provides)
+
+ (installed, alternatives)
+
+ let providedBy = lc[(y, x.key) | (x <- alternatives.namedPairs, y <- x.value),
+ tuple[reference: PackageReference, name: string]]
+
+ let installedSeq = lc[x | (x <- installed.pairs),
+ tuple[package: Package, dependencies: HashSet[PackageReference]]]
+ let explicit = installedSeq
+ .filter(t => t.package.explicit)
+ .map(t => t.package.name)
+ .toSet
+
+ proc findRequired(results: HashSet[string], check: HashSet[string]): HashSet[string] =
+ let full = results + check
+
+ let direct = lc[x | (y <- installedSeq, y.package.name in check,
+ x <- y.dependencies), PackageReference]
+
+ let indirect = lc[x.name | (y <- direct, x <- providedBy,
+ y.isProvidedBy(x.reference)), string].toSet
+
+ let checkNext = (direct.map(p => p.name).toSet + indirect) - full
+ if checkNext.len > 0: findRequired(full, checkNext) else: full
+
+ let required = findRequired(initSet[string](), explicit)
+ let orphans = installedSeq.map(t => t.package.name).toSet - required
+
+ let targets = args.targets.map(t => (if t[0 .. 5] == "local/": t[6 .. ^1] else: t))
+
+ # Provide similar output for not installed packages
+ let unknownTargets = targets.toSet - toSeq(installed.keys).map(p => p.name).toSet
+ let results = if targets.len > 0:
+ targets.filter(t => t in orphans or t in unknownTargets)
+ else:
+ toSeq(orphans.items).sorted(cmp)
+
+ if results.len > 0:
+ let newArgs = args.filter(arg => not arg.isTarget and
+ not arg.matchOption((some("t"), "unrequired")) and
+ not arg.matchOption((some("d"), "deps"))) &
+ results.map(r => (r, none(string), ArgumentType.target))
+ pacmanExec(false, config.color, newArgs)
+ elif targets.len == 0:
+ 0
+ else:
+ 1
diff --git a/src/feature/syncinfo.nim b/src/feature/syncinfo.nim
new file mode 100644
index 0000000..3c64282
--- /dev/null
+++ b/src/feature/syncinfo.nim
@@ -0,0 +1,130 @@
+import
+ future, options, posix, sequtils, strutils, times,
+ "../args", "../aur", "../common", "../config", "../format", "../package",
+ "../pacman", "../utils",
+ "../wrapper/alpm"
+
+const
+ pacmanInfoStrings = [
+ "Architecture",
+ "Backup Files",
+ "Build Date",
+ "Compressed Size",
+ "Conflicts With",
+ "Depends On",
+ "Description",
+ "Download Size",
+ "Groups",
+ "Install Date",
+ "Install Reason",
+ "Install Script",
+ "Installed Size",
+ "Licenses",
+ "MD5 Sum",
+ "Name",
+ "Optional Deps",
+ "Optional For",
+ "Packager",
+ "Provides",
+ "Replaces",
+ "Repository",
+ "Required By",
+ "SHA-256 Sum",
+ "Signatures",
+ "URL",
+ "Validated By",
+ "Version"
+ ]
+
+proc formatDeps(title: string, config: Config,
+ refs: seq[ArchPackageReference]): PackageLineFormat =
+ proc formatDep(reference: ArchPackageReference): (string, bool) =
+ reference.reference.description
+ .map(d => ($reference.reference & ": " & d, true))
+ .get(($reference.reference, false))
+
+ let values: seq[tuple[title: string, hasDesc: bool]] = refs
+ .filter(r => r.arch.isNone or r.arch == some(config.arch))
+ .map(formatDep)
+
+ if values.len > 0:
+ (title, values.map(v => v.title), values.map(v => v.hasDesc).foldl(a or b))
+ else:
+ (title, @[], false)
+
+proc formatDate(date: Option[int64]): seq[string] =
+ if date.isSome:
+ var time = (posix.Time) date.unsafeGet
+ var ltime: Tm
+ discard localtime_r(time, ltime)
+ var buffer: array[100, char]
+ let res = strftime(addr(buffer), buffer.len, "%c", ltime)
+ if res > 0: @[buffer.toString(none(int))] else: @[]
+ else:
+ @[]
+
+proc handleTarget(config: Config, padding: int, args: seq[Argument],
+ target: FullPackageTarget[PackageInfo]): int =
+ if target.foundInfo.isSome:
+ if isAurTargetFull[PackageInfo](target):
+ let pkgInfo = target.pkgInfo.unsafeGet
+
+ printPackageInfo(padding, config.color,
+ (trp"Repository", @["aur"], false),
+ (trp"Name", @[pkgInfo.name], false),
+ (trp"Version", @[pkgInfo.version], false),
+ (trp"Description", toSeq(pkgInfo.description.items), false),
+ (trp"Architecture", pkgInfo.archs, false),
+ (trp"URL", toSeq(pkgInfo.url.items), false),
+ (trp"Licenses", pkgInfo.licenses, false),
+ (trp"Groups", pkgInfo.groups, false),
+ formatDeps(trp"Provides", config, pkgInfo.provides),
+ formatDeps(trp"Depends On", config, pkgInfo.depends),
+ formatDeps(trp"Optional Deps", config, pkgInfo.optional),
+ formatDeps(trp"Conflicts With", config, pkgInfo.conflicts),
+ formatDeps(trp"Replaces", config, pkgInfo.replaces),
+ (tr"Maintainer", toSeq(pkgInfo.maintainer.items()), false),
+ (tr"First Submitted", pkgInfo.firstSubmitted.formatDate, false),
+ (tr"Last Modified", pkgInfo.lastModified.formatDate, false),
+ (tr"Rating", @[formatPkgRating(pkgInfo.votes, pkgInfo.popularity)], false))
+
+ 0
+ else:
+ pacmanRun(false, config.color, args &
+ (target.formatArgument, none(string), ArgumentType.target))
+ else:
+ if target.repo == some("aur"):
+ printError(config.color, trp("package '%s' was not found\n") % [target.formatArgument])
+ 1
+ else:
+ pacmanRun(false, config.color, args &
+ (target.formatArgument, none(string), ArgumentType.target))
+
+proc handleSyncInfo*(args: seq[Argument], config: Config): int =
+ let (_, callArgs) = checkAndRefresh(config.color, args)
+ let targets = args.packageTargets
+
+ let (syncTargets, checkAur) = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+ findSyncTargets(handle, dbs, targets, false, false)
+
+ let (pkgInfos, aerrors) = getAurPackageInfo(checkAur, none(seq[RpcPackageInfo]),
+ proc (a: int, b: int) = discard)
+ for e in aerrors: printError(config.color, e)
+
+ let fullTargets = mapAurTargets[PackageInfo](syncTargets, pkgInfos)
+
+ let code = min(aerrors.len, 1)
+ if fullTargets.filter(isAurTargetFull[PackageInfo]).len == 0:
+ if code == 0:
+ pacmanExec(false, config.color, callArgs)
+ else:
+ discard pacmanRun(false, config.color, callArgs)
+ code
+ else:
+ let finalArgs = callArgs.filter(arg => not arg.isTarget)
+ let padding = pacmanInfoStrings.map(s => s.trp).computeMaxLength
+
+ let codes = code & lc[handleTarget(config, padding, finalArgs, x) | (x <- fullTargets), int]
+ codes.filter(c => c != 0).optFirst.get(0)
diff --git a/src/feature/syncinstall.nim b/src/feature/syncinstall.nim
new file mode 100644
index 0000000..5f6e082
--- /dev/null
+++ b/src/feature/syncinstall.nim
@@ -0,0 +1,827 @@
+import
+ algorithm, future, options, os, posix, sequtils, sets, strutils, tables,
+ "../args", "../aur", "../config", "../common", "../format", "../package",
+ "../pacman", "../utils",
+ "../wrapper/alpm"
+
+type
+ Installed = tuple[
+ name: string,
+ version: string,
+ groups: seq[string],
+ foreign: bool
+ ]
+
+ SatisfyResult = tuple[
+ installed: bool,
+ name: string,
+ buildPkgInfo: Option[PackageInfo]
+ ]
+
+ BuildResult = tuple[
+ version: string,
+ arch: string,
+ ext: string,
+ names: seq[string]
+ ]
+
+proc groupsSeq(pkg: ptr AlpmPackage): seq[string] =
+ toSeq(pkg.groups.items).map(s => $s)
+
+proc orderInstallation(ordered: seq[seq[seq[PackageInfo]]], grouped: seq[seq[PackageInfo]],
+ dependencies: Table[PackageReference, SatisfyResult]): seq[seq[seq[PackageInfo]]] =
+ let orderedNamesSet = lc[c.name | (a <- ordered, b <- a, c <- b), string].toSet
+
+ proc hasBuildDependency(pkgInfos: seq[PackageInfo]): bool =
+ for pkgInfo in pkgInfos:
+ for reference in pkgInfo.allDepends:
+ let satres = dependencies[reference.reference]
+ if satres.buildPkgInfo.isSome and
+ not (satres.buildPkgInfo.unsafeGet in pkgInfos) and
+ not (satres.buildPkgInfo.unsafeGet.name in orderedNamesSet):
+ return true
+ return false
+
+ let split: seq[tuple[pkgInfos: seq[PackageInfo], dependent: bool]] =
+ grouped.map(i => (i, i.hasBuildDependency))
+
+ let newOrdered = ordered & split.filter(s => not s.dependent).map(s => s.pkgInfos)
+ let unordered = split.filter(s => s.dependent).map(s => s.pkgInfos)
+
+ if unordered.len > 0:
+ if unordered.len == grouped.len:
+ newOrdered & unordered
+ else:
+ orderInstallation(newOrdered, unordered, dependencies)
+ else:
+ newOrdered
+
+proc orderInstallation(pkgInfos: seq[PackageInfo],
+ dependencies: Table[PackageReference, SatisfyResult]): seq[seq[seq[PackageInfo]]] =
+ let grouped = pkgInfos.groupBy(i => i.base).map(p => p.values)
+
+ orderInstallation(@[], grouped, dependencies)
+ .map(x => x.filter(s => s.len > 0))
+ .filter(x => x.len > 0)
+
+proc findDependencies(config: Config, handle: ptr AlpmHandle, dbs: seq[ptr AlpmDatabase],
+ satisfied: Table[PackageReference, SatisfyResult], unsatisfied: seq[PackageReference],
+ printMode: bool, noaur: bool): (Table[PackageReference, SatisfyResult], seq[PackageReference]) =
+ proc findInSatisfied(reference: PackageReference): Option[PackageInfo] =
+ for satref, res in satisfied.pairs:
+ if res.buildPkgInfo.isSome:
+ let pkgInfo = res.buildPkgInfo.unsafeGet
+ if satref == reference or reference.isProvidedBy((pkgInfo.name, none(string),
+ some((ConstraintOperation.eq, pkgInfo.version)))):
+ return some(pkgInfo)
+ for provides in pkgInfo.provides:
+ if provides.arch.isNone or provides.arch == some(config.arch):
+ if reference.isProvidedBy(provides.reference):
+ return some(pkgInfo)
+ return none(PackageInfo)
+
+ proc findInDatabaseWithGroups(db: ptr AlpmDatabase, reference: PackageReference,
+ directName: bool): Option[tuple[name: string, groups: seq[string]]] =
+ for pkg in db.packages:
+ if reference.isProvidedBy(($pkg.name, none(string),
+ some((ConstraintOperation.eq, $pkg.version)))):
+ return some(($pkg.name, pkg.groupsSeq))
+ for provides in pkg.provides:
+ if reference.isProvidedBy(provides.toPackageReference):
+ if directName:
+ return some(($pkg.name, pkg.groupsSeq))
+ else:
+ return some(($provides.name, pkg.groupsSeq))
+ return none((string, seq[string]))
+
+ proc findInDatabase(db: ptr AlpmDatabase, reference: PackageReference,
+ directName: bool, checkIgnored: bool): Option[string] =
+ let res = findInDatabaseWithGroups(db, reference, directName)
+ if res.isSome:
+ let r = res.unsafeGet
+ if checkIgnored and config.ignored(r.name, r.groups):
+ none(string)
+ else:
+ some(r.name)
+ else:
+ none(string)
+
+ proc findInDatabases(reference: PackageReference,
+ directName: bool, checkIgnored: bool): Option[string] =
+ for db in dbs:
+ let name = findInDatabase(db, reference, directName, checkIgnored)
+ if name.isSome:
+ return name
+ return none(string)
+
+ proc find(reference: PackageReference): Option[SatisfyResult] =
+ let localName = findInDatabase(handle.local, reference, true, false)
+ if localName.isSome:
+ some((true, localName.unsafeGet, none(PackageInfo)))
+ else:
+ let pkgInfo = findInSatisfied(reference)
+ if pkgInfo.isSome:
+ some((false, pkgInfo.unsafeGet.name, pkgInfo))
+ else:
+ let syncName = findInDatabases(reference, false, true)
+ if syncName.isSome:
+ some((false, syncName.unsafeGet, none(PackageInfo)))
+ else:
+ none(SatisfyResult)
+
+ type ReferenceResult = tuple[reference: PackageReference, result: Option[SatisfyResult]]
+
+ let findResult: seq[ReferenceResult] = unsatisfied.map(r => (r, r.find))
+ let success = findResult.filter(r => r.result.isSome)
+ let aurCheck = findResult.filter(r => r.result.isNone).map(r => r.reference)
+
+ let (aurSuccess, aurFail) = if not noaur and aurCheck.len > 0: (block:
+ let (update, terminate) = if aurCheck.len >= 4:
+ printProgressShare(config.progressBar, tr"checking build dependencies")
+ else:
+ (proc (a: int, b: int) {.closure.} = discard, proc {.closure.} = discard)
+ try:
+ withAur():
+ let (pkgInfos, aerrors) = getAurPackageInfo(aurCheck.map(r => r.name),
+ none(seq[RpcPackageInfo]), update)
+ for e in aerrors: printError(config.color, e)
+
+ let acceptedPkgInfos = pkgInfos.filter(i => not config.ignored(i.name, i.groups))
+ let aurTable = acceptedPkgInfos.map(i => (i.name, i)).toTable
+ let aurResult = aurCheck.map(proc (reference: PackageReference): ReferenceResult =
+ if aurTable.hasKey(reference.name):
+ (reference, some((false, reference.name, some(aurTable[reference.name]))))
+ else:
+ (reference, none(SatisfyResult)))
+
+ let aurSuccess = aurResult.filter(r => r.result.isSome)
+ let aurFail = aurResult.filter(r => r.result.isNone).map(r => r.reference)
+ (aurSuccess, aurFail)
+ finally:
+ terminate())
+ else:
+ (@[], aurCheck)
+
+ let newSatisfied = (toSeq(satisfied.pairs) &
+ success.map(r => (r.reference, r.result.unsafeGet)) &
+ aurSuccess.map(r => (r.reference, r.result.unsafeGet))).toTable
+
+ let newUnsatisfied = lc[x.reference | (y <- aurSuccess,
+ r <- y.result, i <- r.buildPkgInfo, x <- i.allDepends,
+ x.arch.isNone or x.arch == some(config.arch)), PackageReference].deduplicate
+
+ if aurFail.len > 0:
+ (newSatisfied, aurFail)
+ elif newUnsatisfied.len > 0:
+ findDependencies(config, handle, dbs, newSatisfied, newUnsatisfied, printMode, noaur)
+ else:
+ (newSatisfied, @[])
+
+proc findDependencies(config: Config, handle: ptr AlpmHandle,
+ dbs: seq[ptr AlpmDatabase], pkgInfos: seq[PackageInfo], printMode: bool, noaur: bool):
+ (Table[PackageReference, SatisfyResult], seq[PackageReference]) =
+ let satisfied = pkgInfos.map(p => ((p.name, none(string), none(VersionConstraint)),
+ (false, p.name, some(p)))).toTable
+ let unsatisfied = lc[x.reference | (i <- pkgInfos, x <- i.allDepends,
+ x.arch.isNone or x.arch == some(config.arch)), PackageReference].deduplicate
+ findDependencies(config, handle, dbs, satisfied, unsatisfied, printMode, noaur)
+
+proc filterNotFoundSyncTargets[T: RpcPackageInfo](syncTargets: seq[SyncPackageTarget],
+ pkgInfos: seq[T]): (Table[string, T], seq[SyncPackageTarget]) =
+ let rpcInfoTable = pkgInfos.map(d => (d.name, d)).toTable
+
+ proc notFoundOrFoundInAur(target: SyncPackageTarget): bool =
+ target.foundInfo.isNone and
+ not (target.isAurTargetSync and rpcInfoTable.hasKey(target.name))
+
+ # collect packages which were found neither in sync DB nor in AUR
+ let notFoundTargets = syncTargets.filter(notFoundOrFoundInAur)
+
+ (rpcInfoTable, notFoundTargets)
+
+proc printSyncNotFound(config: Config, notFoundTargets: seq[SyncPackageTarget]) =
+ let dbs = config.dbs.toSet
+
+ for target in notFoundTargets:
+ if target.repo.isNone or target.repo == some("aur") or target.repo.unsafeGet in dbs:
+ printError(config.color, trp("target not found: %s\n") % [target.name])
+ else:
+ printError(config.color, trp("database not found: %s\n") % [target.repo.unsafeGet])
+
+proc printUnsatisfied(config: Config,
+ satisfied: Table[PackageReference, SatisfyResult], unsatisfied: seq[PackageReference]) =
+ if unsatisfied.len > 0:
+ for _, satres in satisfied.pairs:
+ for pkgInfo in satres.buildPkgInfo:
+ for reference in pkgInfo.allDepends:
+ let pref = reference.reference
+ if pref in unsatisfied:
+ printError(config.color,
+ trp("unable to satisfy dependency '%s' required by %s\n") %
+ [$pref, pkgInfo.name])
+
+proc editLoop(config: Config, base: string, repoPath: string, gitPath: Option[string],
+ defaultYes: bool, noconfirm: bool): char =
+ proc editFileLoop(file: string): char =
+ let default = if defaultYes: 'y' else: 'n'
+ let res = printColonUserChoice(config.color,
+ tr"View and edit $#?" % [base & "/" & file], ['y', 'n', 's', 'a', '?'],
+ default, '?', noconfirm, 'n')
+
+ if res == '?':
+ printUserInputHelp(('s', tr"skip all files"),
+ ('a', tr"abort operation"))
+ editFileLoop(file)
+ elif res == 'y':
+ let visualEnv = getenv("VISUAL")
+ let editorEnv = getenv("EDITOR")
+ let editor = if visualEnv != nil and visualEnv.len > 0:
+ $visualEnv
+ elif editorEnv != nil and editorEnv.len > 0:
+ $editorEnv
+ else:
+ printColonUserInput(config.color, tr"Enter editor executable name" & ":",
+ noconfirm, "", "")
+
+ if editor.strip.len == 0:
+ 'n'
+ else:
+ discard forkWait(proc: int =
+ discard chdir(buildPath(repoPath, gitPath))
+ execResult(bashCmd, "-c", """$1 "$2"""", "bash", editor, file))
+ editFileLoop(file)
+ else:
+ res
+
+ let rawFiles = if gitPath.isSome:
+ runProgram(gitCmd, "-C", repoPath, "ls-tree", "-r", "--name-only", "@",
+ gitPath.unsafeGet & "/").map(s => s[gitPath.unsafeGet.len + 1 .. ^1])
+ else:
+ runProgram(gitCmd, "-C", repoPath, "ls-tree", "-r", "--name-only", "@")
+
+ let files = ("PKGBUILD" & rawFiles.filter(x => x != ".SRCINFO")).deduplicate
+
+ proc editFileLoopAll(index: int): char =
+ if index < files.len:
+ let res = editFileLoop(files[index])
+ if res == 'n': editFileLoopAll(index + 1) else: res
+ else:
+ 'n'
+
+ editFileLoopAll(0)
+
+proc buildLoop(config: Config, pkgInfos: seq[PackageInfo], noconfirm: bool,
+ noextract: bool): (Option[BuildResult], int) =
+ let base = pkgInfos[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+ let gitPath = pkgInfos[0].gitPath
+ let buildPath = buildPath(repoPath, gitPath)
+
+ let buildCode = forkWait(proc: int =
+ if chdir(buildPath) == 0:
+ discard setenv("PKGDEST", config.tmpRoot, 1)
+ discard setenv("CARCH", config.arch, 1)
+
+ if not noextract:
+ removeDirQuiet(buildPath & "src")
+
+ let optional: seq[tuple[arg: string, cond: bool]] = @[
+ ("-e", noextract),
+ ("-m", not config.color)
+ ]
+
+ execResult(@[makepkgCmd, "-f"] &
+ optional.filter(o => o.cond).map(o => o.arg))
+ else:
+ quit(1))
+
+ if buildCode != 0:
+ printError(config.color, tr"failed to build '$#'" % [base])
+ (none(BuildResult), buildCode)
+ else:
+ let confFileEnv = getenv("MAKEPKG_CONF")
+ let confFile = if confFileEnv == nil or confFileEnv.len == 0:
+ sysConfDir & "/makepkg.conf"
+ else:
+ $confFileEnv
+
+ let envExt = getenv("PKGEXT")
+ let confExt = if envExt == nil or envExt.len == 0:
+ runProgram(bashCmd, "-c",
+ "source \"$@\" && echo \"$PKGEXT\"",
+ "bash", confFile).optFirst.get("")
+ else:
+ $envExt
+
+ let extracted = runProgram(bashCmd, "-c",
+ """source "$@" && echo "$epoch" && echo "$pkgver" && """ &
+ """echo "$pkgrel" && echo "${arch[@]}" && echo "${pkgname[@]}"""",
+ "bash", buildPath & "/PKGBUILD")
+ if extracted.len != 5:
+ printError(config.color, tr"failed to extract package info '$#'" % [base])
+ (none(BuildResult), 1)
+ else:
+ let epoch = extracted[0]
+ let versionShort = extracted[1] & "-" & extracted[2]
+ let version = if epoch.len > 0: epoch & ":" & versionShort else: versionShort
+ let archs = extracted[3].split(" ").toSet
+ let arch = if config.arch in archs: config.arch else: "any"
+ let names = extracted[4].split(" ")
+
+ (some((version, arch, $confExt, names)), 0)
+
+proc buildFromSources(config: Config, commonArgs: seq[Argument],
+ pkgInfos: seq[PackageInfo], noconfirm: bool): (Option[BuildResult], int) =
+ let base = pkgInfos[0].base
+ let (cloneCode, cloneErrorMessage) = cloneRepo(config, pkgInfos)
+
+ if cloneCode != 0:
+ for e in cloneErrorMessage: printError(config.color, e)
+ printError(config.color, tr"$#: failed to clone git repository" % [base])
+ (none(BuildResult), cloneCode)
+ else:
+ proc loop(noextract: bool, showEditLoop: bool): (Option[BuildResult], int) =
+ let res = if showEditLoop:
+ editLoop(config, base, repoPath(config.tmpRoot, base), pkgInfos[0].gitPath,
+ false, noconfirm)
+ else:
+ 'n'
+
+ if res == 'a':
+ (none(BuildResult), 1)
+ else:
+ let (buildResult, code) = buildLoop(config, pkgInfos,
+ noconfirm, noextract)
+
+ if code != 0:
+ proc ask(): char =
+ let res = printColonUserChoice(config.color,
+ tr"Build failed, retry?", ['y', 'e', 'n', '?'], 'n', '?',
+ noconfirm, 'n')
+ if res == '?':
+ printUserInputHelp(('e', tr"retry with --noextract option"))
+ ask()
+ else:
+ res
+
+ let res = ask()
+ if res == 'e':
+ loop(true, true)
+ elif res == 'y':
+ loop(false, true)
+ else:
+ (buildResult, code)
+ else:
+ (buildResult, code)
+
+ loop(false, false)
+
+proc installGroupFromSources(config: Config, commonArgs: seq[Argument],
+ basePackages: seq[seq[PackageInfo]], explicits: HashSet[string], noconfirm: bool): int =
+ proc buildNext(index: int, buildResults: seq[BuildResult]): (seq[BuildResult], int) =
+ if index < basePackages.len:
+ let (buildResult, code) = buildFromSources(config, commonArgs,
+ basePackages[index], noconfirm)
+
+ if code != 0:
+ (buildResults, code)
+ else:
+ buildNext(index + 1, buildResults & buildResult.unsafeGet)
+ else:
+ (buildResults, 0)
+
+ let (buildResults, buildCode) = buildNext(0, @[])
+
+ proc formatArchiveFile(br: BuildResult, name: string): string =
+ config.tmpRoot & "/" & name & "-" & br.version & "-" & br.arch & br.ext
+
+ let files = lc[(name, formatArchiveFile(br, name)) |
+ (br <- buildResults, name <- br.names), (string, string)].toTable
+ let install = lc[x | (g <- basePackages, i <- g, x <- files.opt(i.name)), string]
+
+ proc handleTmpRoot(clear: bool) =
+ for _, file in files:
+ if clear or not (file in install):
+ try:
+ removeFile(file)
+ except:
+ discard
+
+ if not clear:
+ printWarning(config.color, tr"packages are saved to '$#'" % [config.tmpRoot])
+
+ if buildCode != 0:
+ handleTmpRoot(true)
+ buildCode
+ else:
+ let res = printColonUserChoice(config.color,
+ tr"Continue installing?", ['y', 'n'], 'y', 'n',
+ noconfirm, 'y')
+
+ if res != 'y':
+ handleTmpRoot(false)
+ 1
+ else:
+ let explicit = basePackages.filter(p => p.filter(i => i.name in explicits).len > 0).len > 0
+ let asdepsSeq = if not explicit: @[("asdeps", none(string), ArgumentType.long)] else: @[]
+
+ let installCode = pacmanRun(true, config.color, commonArgs &
+ ("U", none(string), ArgumentType.short) & asdepsSeq &
+ install.map(i => (i, none(string), ArgumentType.target)))
+
+ if installCode != 0:
+ handleTmpRoot(false)
+ installCode
+ else:
+ handleTmpRoot(true)
+ 0
+
+proc handleInstall(args: seq[Argument], config: Config, upgradeCount: int,
+ noconfirm: bool, explicits: HashSet[string], installed: seq[Installed],
+ dependencies: Table[PackageReference, SatisfyResult],
+ directPacmanTargets: seq[string], additionalPacmanTargets: seq[string],
+ basePackages: seq[seq[seq[PackageInfo]]]): int =
+ let (directCode, directSome) = if directPacmanTargets.len > 0 or upgradeCount > 0:
+ (pacmanRun(true, config.color, args.filter(arg => not arg.isTarget) &
+ directPacmanTargets.map(t => (t, none(string), ArgumentType.target))), true)
+ else:
+ (0, false)
+
+ if directCode != 0:
+ directCode
+ else:
+ let commonArgs = args.keepOnlyOptions(commonOptions, upgradeCommonOptions)
+
+ let (paths, confirmAndCloneCode) = if basePackages.len > 0: (block:
+ let installedVersions = installed.map(i => (i.name, i.version)).toTable
+
+ printPackages(config.color, config.verbosePkgList,
+ lc[(i.name, i.repo, installedVersions.opt(i.name), i.version) |
+ (g <- basePackages, b <- g, i <- b), PackageInstallFormat]
+ .sorted((a, b) => cmp(a.name, b.name)))
+ let input = printColonUserChoice(config.color,
+ tr"Proceed with building?", ['y', 'n'], 'y', 'n', noconfirm, 'y')
+
+ if input == 'y':
+ let (update, terminate) = if config.debug:
+ (proc (a: int, b: int) {.closure.} = discard, proc {.closure.} = discard)
+ else:
+ printProgressShare(config.progressBar, tr"cloning repositories")
+
+ let flatBasePackages = lc[x | (a <- basePackages, x <- a), seq[PackageInfo]]
+ update(0, flatBasePackages.len)
+
+ proc cloneNext(index: int, paths: seq[string]): (seq[string], int) =
+ if index < flatBasePackages.len:
+ let pkgInfos = flatBasePackages[index]
+ let base = pkgInfos[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+ let (cloneCode, cloneErrorMessage) = cloneRepo(config, flatBasePackages[index])
+
+ if cloneCode == 0:
+ update(index + 1, flatBasePackages.len)
+ cloneNext(index + 1, paths & repoPath)
+ else:
+ terminate()
+ for e in cloneErrorMessage: printError(config.color, e)
+ printError(config.color, tr"$#: failed to clone git repository" %
+ [pkgInfos[0].base])
+ (paths & repoPath, cloneCode)
+ else:
+ terminate()
+ (paths, 0)
+
+ let (paths, cloneCode) = cloneNext(0, @[])
+ if cloneCode != 0:
+ (paths, cloneCode)
+ else:
+ proc checkNext(index: int, skipEdit: bool): int =
+ if index < flatBasePackages.len:
+ let pkgInfos = flatBasePackages[index]
+ let base = pkgInfos[0].base
+ let repoPath = repoPath(config.tmpRoot, base)
+
+ let aur = pkgInfos[0].repo == "aur"
+
+ if not skipEdit and aur and config.aurComments:
+ echo(tr"downloading comments from AUR...")
+ let (comments, error) = downloadAurComments(base)
+ for e in error: printError(config.color, e)
+ if comments.len > 0:
+ let commentsReversed = toSeq(comments.reversed)
+ printComments(config.color, pkgInfos[0].maintainer, commentsReversed)
+
+ let res = if skipEdit:
+ 'n'
+ else: (block:
+ let defaultYes = aur and not config.viewNoDefault
+ editLoop(config, base, repoPath, pkgInfos[0].gitPath, defaultYes, noconfirm))
+
+ if res == 'a':
+ 1
+ else:
+ checkNext(index + 1, skipEdit or res == 's')
+ else:
+ 0
+
+ (paths, checkNext(0, false))
+ else:
+ (@[], 1))
+ else:
+ (@[], 0)
+
+ proc removeTmp() =
+ for path in paths:
+ removeDirQuiet(path)
+ discard rmdir(config.tmpRoot)
+
+ if confirmAndCloneCode != 0:
+ removeTmp()
+ confirmAndCloneCode
+ else:
+ let (additionalCode, additionalSome) = if additionalPacmanTargets.len > 0: (block:
+ printColon(config.color, tr"Installing build dependencies...")
+
+ (pacmanRun(true, config.color, commonArgs &
+ ("S", none(string), ArgumentType.short) &
+ ("needed", none(string), ArgumentType.long) &
+ ("asdeps", none(string), ArgumentType.long) &
+ additionalPacmanTargets.map(t => (t, none(string), ArgumentType.target))), true))
+ else:
+ (0, false)
+
+ if additionalCode != 0:
+ removeTmp()
+ additionalCode
+ else:
+ if basePackages.len > 0:
+ # check all pacman dependencies were installed
+ let unsatisfied = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ proc checkSatisfied(reference: PackageReference): bool =
+ for pkg in handle.local.packages:
+ if reference.isProvidedBy(($pkg.name, none(string),
+ some((ConstraintOperation.eq, $pkg.version)))):
+ return true
+ for provides in pkg.provides:
+ if reference.isProvidedBy(provides.toPackageReference):
+ return true
+ return false
+
+ lc[x.key | (x <- dependencies.namedPairs, not x.value.installed and
+ x.value.buildPkgInfo.isNone and not x.key.checkSatisfied), PackageReference]
+
+ if unsatisfied.len > 0:
+ removeTmp()
+ printUnsatisfied(config, dependencies, unsatisfied)
+ 1
+ else:
+ proc installNext(index: int, lastCode: int): (int, int) =
+ if index < basePackages.len and lastCode == 0:
+ let code = installGroupFromSources(config, commonArgs,
+ basePackages[index], explicits, noconfirm)
+ installNext(index + 1, code)
+ else:
+ (lastCode, index - 1)
+
+ let (code, index) = installNext(0, 0)
+ if code != 0 and index < basePackages.len - 1:
+ printWarning(config.color, tr"installation aborted")
+ removeTmp()
+ code
+ elif not directSome and not additionalSome:
+ echo(trp(" there is nothing to do\n"))
+ 0
+ else:
+ 0
+
+proc handlePrint(args: seq[Argument], config: Config, printFormat: string,
+ upgradeCount: int, directPacmanTargets: seq[string], additionalPacmanTargets: seq[string],
+ basePackages: seq[seq[seq[PackageInfo]]]): int =
+
+ let code = if directPacmanTargets.len > 0 or
+ additionalPacmanTargets.len > 0 or upgradeCount > 0:
+ pacmanRun(false, config.color, args.filter(arg => not arg.isTarget) &
+ (directPacmanTargets & additionalPacmanTargets)
+ .map(t => (t, none(string), ArgumentType.target)))
+ else:
+ 0
+
+ if code == 0:
+ proc printWithFormat(pkgInfo: PackageInfo) =
+ echo(printFormat
+ .replace("%n", pkgInfo.name)
+ .replace("%v", pkgInfo.version)
+ .replace("%r", "aur")
+ .replace("%s", "0")
+ .replace("%l", pkgInfo.gitUrl))
+
+ for installGroup in basePackages:
+ for pkgInfos in installGroup:
+ for pkgInfo in pkgInfos:
+ printWithFormat(pkgInfo)
+ 0
+ else:
+ code
+
+proc handleSyncInstall*(args: seq[Argument], config: Config): int =
+ let (_, callArgs) = checkAndRefresh(config.color, args)
+
+ let upgradeCount = args.count((some("u"), "sysupgrade"))
+ let needed = args.check((none(string), "needed"))
+ let noaur = args.check((none(string), "noaur"))
+ let build = args.check((none(string), "build"))
+
+ let printModeArg = args.check((some("p"), "print"))
+ let printModeFormat = args.filter(arg => arg
+ .matchOption((none(string), "print-format"))).optLast
+ let printFormat = if printModeArg or printModeFormat.isSome:
+ some(printModeFormat.map(arg => arg.value.get).get("%l"))
+ else:
+ none(string)
+
+ let noconfirm = args
+ .filter(arg => arg.matchOption((none(string), "confirm")) or
+ arg.matchOption((none(string), "noconfirm"))).optLast
+ .map(arg => arg.key == "noconfirm").get(false)
+
+ let targets = args.packageTargets
+
+ let (syncTargets, checkAur, installed) = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ let (syncTargets, checkAur) = findSyncTargets(handle, dbs, targets,
+ not build, not build)
+
+ let installed = lc[($p.name, $p.version, p.groupsSeq,
+ dbs.filter(d => d[p.name] != nil).len == 0) |
+ (p <- handle.local.packages), Installed]
+
+ (syncTargets, checkAur, installed)
+
+ let realCheckAur = if noaur:
+ @[]
+ elif upgradeCount > 0:
+ installed
+ .filter(i => i.foreign and
+ (config.checkIgnored or not config.ignored(i.name, i.groups)))
+ .map(i => i.name) & checkAur
+ else:
+ checkAur
+
+ withAur():
+ if realCheckAur.len > 0 and printFormat.isNone:
+ printColon(config.color, tr"Checking AUR database...")
+ let (rpcInfos, aerrors) = getRpcPackageInfo(realCheckAur)
+ for e in aerrors: printError(config.color, e)
+
+ let (rpcInfoTable, notFoundTargets) = filterNotFoundSyncTargets(syncTargets, rpcInfos)
+
+ if notFoundTargets.len > 0:
+ printSyncNotFound(config, notFoundTargets)
+ 1
+ else:
+ let fullTargets = mapAurTargets(syncTargets, rpcInfos)
+ let pacmanTargets = fullTargets.filter(t => not isAurTargetFull(t))
+ let aurTargets = fullTargets.filter(isAurTargetFull)
+
+ if upgradeCount > 0 and not noaur and printFormat.isNone and config.printAurNotFound:
+ for inst in installed:
+ if inst.foreign and not config.ignored(inst.name, inst.groups) and
+ not rpcInfoTable.hasKey(inst.name):
+ printWarning(config.color, tr"$# was not found in AUR" % [inst.name])
+
+ let installedTable = installed.map(i => (i.name, i)).toTable
+
+ proc checkNeeded(name: string, version: string): bool =
+ if installedTable.hasKey(name):
+ let i = installedTable[name]
+ vercmp(version, i.version) > 0
+ else:
+ true
+
+ let targetRpcInfos: seq[tuple[rpcInfo: RpcPackageInfo, upgradeable: bool]] =
+ aurTargets.map(t => t.pkgInfo.get).map(i => (i, checkNeeded(i.name, i.version)))
+
+ if printFormat.isNone and needed:
+ for rpcInfo in targetRpcInfos:
+ if not rpcInfo.upgradeable:
+ # not upgradeable assumes that package is installed
+ let inst = installedTable[rpcInfo.rpcInfo.name]
+ printWarning(config.color, tra("%s-%s is up to date -- skipping\n") %
+ [rpcInfo.rpcInfo.name, inst.version])
+
+ let aurTargetsSet = aurTargets.map(t => t.name).toSet
+ let fullRpcInfos = (targetRpcInfos
+ .filter(i => not needed or i.upgradeable).map(i => i.rpcInfo) &
+ rpcInfos.filter(i => upgradeCount > 0 and not (i.name in aurTargetsSet) and
+ checkNeeded(i.name, i.version))).deduplicate
+
+ if fullRpcInfos.len > 0 and printFormat.isNone:
+ echo(tr"downloading full package descriptions...")
+ let (aurPkgInfos, faerrors) = getAurPackageInfo(fullRpcInfos
+ .map(i => i.name), some(fullRpcInfos), proc (a: int, b: int) = discard)
+
+ if faerrors.len > 0:
+ for e in faerrors: printError(config.color, e)
+ 1
+ else:
+ let neededPacmanTargets = if printFormat.isNone and build and needed:
+ pacmanTargets.filter(target => (block:
+ let version = target.foundInfo.get.pkg.get.version
+ if checkNeeded(target.name, version):
+ true
+ else:
+ printWarning(config.color, tra("%s-%s is up to date -- skipping\n") %
+ [target.name, version])
+ false))
+ else:
+ pacmanTargets
+
+ let checkPacmanPkgInfos = printFormat.isNone and build and
+ neededPacmanTargets.len > 0
+
+ let (buildPkgInfos, obtainErrorMessages) = if checkPacmanPkgInfos: (block:
+ printColon(config.color, tr"Checking repositories...")
+ obtainBuildPkgInfos(config, pacmanTargets))
+ else:
+ (@[], @[])
+
+ if checkPacmanPkgInfos and buildPkgInfos.len < pacmanTargets.len:
+ for e in obtainErrorMessages: printError(config.color, e)
+ 1
+ else:
+ let pkgInfos = buildPkgInfos & aurPkgInfos
+ let targetsSet = fullTargets.map(t => t.name).toSet
+
+ let acceptedPkgInfos = pkgInfos.filter(pkgInfo => (block:
+ let instGroups = lc[x | (i <- installedTable.opt(pkgInfo.name),
+ x <- i.groups), string]
+
+ if config.ignored(pkgInfo.name, (instGroups & pkgInfo.groups).deduplicate):
+ if pkgInfo.name in targetsSet:
+ if printFormat.isNone:
+ let input = printColonUserChoice(config.color,
+ trp"%s is in IgnorePkg/IgnoreGroup. Install anyway?" % [pkgInfo.name],
+ ['y', 'n'], 'y', 'n', noconfirm, 'y')
+ input != 'n'
+ else:
+ true
+ else:
+ false
+ else:
+ true))
+
+ if acceptedPkgInfos.len > 0 and printFormat.isNone:
+ echo(trp("resolving dependencies...\n"))
+ let (satisfied, unsatisfied) = withAlpm(config.root, config.db,
+ config.dbs, config.arch, handle, dbs, errors):
+ findDependencies(config, handle, dbs, acceptedPkgInfos,
+ printFormat.isSome, noaur)
+
+ if unsatisfied.len > 0:
+ printUnsatisfied(config, satisfied, unsatisfied)
+ 1
+ else:
+ if printFormat.isNone:
+ let acceptedSet = acceptedPkgInfos.map(i => i.name).toSet
+
+ for pkgInfo in pkgInfos:
+ if not (pkgInfo.name in acceptedSet):
+ if not (pkgInfo.name in targetsSet) and upgradeCount > 0 and
+ installedTable.hasKey(pkgInfo.name):
+ printWarning(config.color, tra("%s: ignoring package upgrade (%s => %s)\n") %
+ [pkgInfo.name, installedTable[pkgInfo.name].version, pkgInfo.version])
+ else:
+ printWarning(config.color, trp("skipping target: %s\n") % [pkgInfo.name])
+ elif pkgInfo.repo == "aur" and pkgInfo.maintainer.isNone:
+ printWarning(config.color, tr"$# is orphaned" % [pkgInfo.name])
+
+ let aurPrintSet = acceptedPkgInfos.map(i => i.name).toSet
+ let fullPkgInfos = acceptedPkgInfos & lc[i | (s <- satisfied.values,
+ i <- s.buildPkgInfo, not (i.name in aurPrintSet)), PackageInfo].deduplicate
+
+ let directPacmanTargets = pacmanTargets.map(t => t.formatArgument)
+ let additionalPacmanTargets = lc[x.name | (x <- satisfied.values,
+ not x.installed and x.buildPkgInfo.isNone), string]
+ let orderedPkgInfos = orderInstallation(fullPkgInfos, satisfied)
+
+ let pacmanArgs = callArgs.filterExtensions(true, true)
+
+ if printFormat.isSome:
+ handlePrint(pacmanArgs, config, printFormat.unsafeGet, upgradeCount,
+ directPacmanTargets, additionalPacmanTargets, orderedPkgInfos)
+ else:
+ let explicits = if not args.check((none(string), "asdeps")):
+ targets.map(t => t.name)
+ else:
+ @[]
+
+ let passDirectPacmanTargets = if build: @[] else: directPacmanTargets
+
+ handleInstall(pacmanArgs, config, upgradeCount, noconfirm,
+ explicits.toSet, installed, satisfied, passDirectPacmanTargets,
+ additionalPacmanTargets, orderedPkgInfos)
diff --git a/src/feature/syncsearch.nim b/src/feature/syncsearch.nim
new file mode 100644
index 0000000..7b2e77c
--- /dev/null
+++ b/src/feature/syncsearch.nim
@@ -0,0 +1,53 @@
+import
+ algorithm, future, options, sequtils, strutils,
+ "../args", "../aur", "../config", "../common", "../format", "../package",
+ "../pacman", "../utils",
+ "../wrapper/alpm"
+
+proc handleSyncSearch*(args: seq[Argument], config: Config): int =
+ let (_, callArgs) = checkAndRefresh(config.color, args)
+
+ let quiet = args.check((some("q"), "quiet"))
+
+ let (aurPackages, aerrors) = findAurPackages(args.targets)
+ for e in aerrors: printError(config.color, e)
+
+ type Package = tuple[rpcInfo: RpcPackageInfo, installedVersion: Option[string]]
+
+ proc checkLocalPackages: seq[Package] =
+ if quiet:
+ aurPackages.map(pkg => (pkg, none(string)))
+ elif aurPackages.len > 0:
+ withAlpm(config.root, config.db, newSeq[string](), config.arch, handle, dbs, errors):
+ for e in errors: printError(config.color, e)
+
+ aurPackages.map(proc (rpcInfo: RpcPackageInfo): Package =
+ let pkg = handle.local[rpcInfo.name]
+ if pkg != nil:
+ (rpcInfo, some($pkg.version))
+ else:
+ (rpcInfo, none(string)))
+ else:
+ @[]
+
+ let pkgs = checkLocalPackages()
+ .sorted((a, b) => cmp(a.rpcInfo.name, b.rpcInfo.name))
+
+ var code = min(aerrors.len, 1)
+ if pkgs.len == 0:
+ if code == 0:
+ pacmanExec(false, config.color, callArgs)
+ else:
+ discard pacmanRun(false, config.color, callArgs)
+ code
+ else:
+ discard pacmanRun(false, config.color, callArgs)
+
+ for pkg in pkgs:
+ if quiet:
+ echo(pkg.rpcInfo.name)
+ else:
+ printPackageSearch(config.color, "aur", pkg.rpcInfo.name,
+ pkg.rpcInfo.version, pkg.installedVersion, pkg.rpcInfo.description,
+ some(formatPkgRating(pkg.rpcInfo.votes, pkg.rpcInfo.popularity)))
+ 0
diff --git a/src/format.nim b/src/format.nim
new file mode 100644
index 0000000..49dc5ba
--- /dev/null
+++ b/src/format.nim
@@ -0,0 +1,308 @@
+import
+ future, options, sequtils, strutils, times, unicode,
+ utils
+
+type
+ PackageLineFormat* = tuple[
+ title: string,
+ values: seq[string],
+ forceBreak: bool
+ ]
+
+ PackageInstallFormat* = tuple[
+ name: string,
+ repo: string,
+ oldVersion: Option[string],
+ newVersion: string
+ ]
+
+ CommentFormat* = tuple[
+ author: string,
+ date: string,
+ text: string
+ ]
+
+ Color {.pure.} = enum
+ normal = "\x1b[0m"
+ red = "\x1b[1;31m"
+ green = "\x1b[1;32m"
+ yellow = "\x1b[1;33m"
+ blue = "\x1b[1;34m"
+ magenta = "\x1b[1;35m"
+ cyan = "\x1b[1;36m"
+ bold = "\x1b[1;39m"
+
+template `^`(c: Color): string =
+ if color: $c else: ""
+
+type
+ WinSize = object
+ row: cushort
+ col: cushort
+ xpixel: cushort
+ ypixel: cushort
+
+proc ioctl[T](fd: cint, request: culong, argp: var T): cint
+ {.importc, header: "<sys/ioctl.h>".}
+
+proc getWindowSize(): tuple[width: int, height: int] =
+ var winSize: WinSize
+ if ioctl(1, 0x5413, winSize) != -1:
+ ((int) winSize.col, (int) winSize.row)
+ else:
+ (0, 0)
+
+proc formatPkgRating*(votes: int, popularity: float): string =
+ $votes & " / " & formatFloat(popularity, format = ffDecimal, precision = 6)
+
+proc computeMaxLength*(texts: openArray[string]): int =
+ texts.map(runeLen).max
+
+proc splitLines(text: string, lineSize: int, lines: seq[string] = @[]): seq[string] =
+ let addBreaks = lineSize >= 10
+
+ if not addBreaks:
+ lines & text
+ else:
+ let offset = text.runeOffset(lineSize)
+ if offset < 0:
+ lines & text
+ else:
+ let leftIndex = text.rfind(' ', offset - 1)
+ let rightIndex = text.find(' ', offset - 1)
+ let index = if leftIndex >= 0: leftIndex else: rightIndex
+
+ if index < 0:
+ lines & text
+ else:
+ text[index .. ^1].strip.splitLines(lineSize,
+ lines & text[0 .. index - 1].strip)
+
+proc printPackageInfo*(minPadding: int, color: bool, lines: varargs[PackageLineFormat]) =
+ let width = getWindowSize().width
+ let divider = " : "
+ let padding = max(lines.map(line => line.title.runeLen).max, minPadding)
+
+ let lineSize = width - (padding + divider.len)
+
+ proc formatTextLines(values: seq[string], forceBreak: bool): seq[string] =
+ if values.len == 0:
+ @[]
+ elif forceBreak:
+ lc[x | (y <- values.map(s => s.strip.splitLines(lineSize)), x <- y), string]
+ else:
+ values.map(v => v.strip).foldl(a & " " & b).splitLines(lineSize)
+
+ proc formatText(values: seq[string], forceBreak: bool): string =
+ let textSeq = formatTextLines(values, forceBreak)
+ if textSeq.len > 0:
+ textSeq.foldl(a & "\n" & ' '.repeat(padding + divider.len) & b)
+ else:
+ "None"
+
+ for line in lines:
+ let title = line.title & ' '.repeat(padding - line.title.runeLen) & divider
+ let text = formatText(line.values, line.forceBreak)
+ echo(^Color.bold, title, ^Color.normal, text)
+
+ # pacman leaves empty line in the end of info
+ echo()
+
+proc printPackageSearch*(color: bool, repo: string, name: string,
+ version: string, installedVersion: Option[string],
+ description: Option[string], extra: Option[string]) =
+ let commonText = ^Color.magenta & repo & "/" &
+ ^Color.bold & name & " " & ^Color.green & version & ^Color.normal
+
+ let installedText = if installedVersion == some(version):
+ " " & ^Color.cyan &
+ "[" & trp"installed" & "]" & ^Color.normal
+ elif installedVersion.isSome:
+ " " & ^Color.cyan &
+ "[" & trp"installed" & " " & installedVersion.unsafeGet &
+ "]" & ^Color.normal
+ else:
+ ""
+
+ let extraText = extra.map(e => " " & ^Color.yellow &
+ "[" & e & "]" & ^Color.normal).get("")
+
+ echo(commonText & installedText & extraText)
+
+ let padding = 4
+ let lines = description.get("").splitLines(getWindowSize().width - padding)
+ for line in lines:
+ echo(' '.repeat(padding), line)
+
+proc printPackages*(color: bool, verbose: bool, packages: seq[PackageInstallFormat]) =
+ if verbose:
+ let packageTitle = trp"Package" & " (" & $packages.len & ")"
+ let oldVersionTitle = trp"Old Version"
+ let newVersionTitle = trp"New Version"
+
+ let packageLen = max(packageTitle.runeLen,
+ packages.map(p => p.name.len + 1 + p.repo.len).max)
+
+ let oldVersionLenEmpty = packages.map(p => p.oldVersion.map(v => v.len).get(0)).max
+ let oldVersionLen = if oldVersionLenEmpty > 0:
+ max(oldVersionTitle.runeLen, oldVersionLenEmpty)
+ else:
+ 0
+
+ echo()
+ echo(^Color.bold & packageTitle &
+ ' '.repeat(packageLen - packageTitle.runeLen) &
+ (if oldVersionLen > 0: " " & oldVersionTitle &
+ ' '.repeat(oldVersionLen - oldVersionTitle.runeLen) else: "") &
+ " " & newVersionTitle & ^Color.normal)
+ echo()
+ for package in packages:
+ let name = package.repo & "/" & package.name
+ let oldVersion = package.oldVersion.get("")
+ echo(name & ' '.repeat(packageLen - name.runeLen) &
+ (if oldVersionLen > 0: " " & oldVersion &
+ ' '.repeat(oldVersionLen - oldVersion.len) else: "") &
+ " " & package.newVersion)
+ echo()
+ else:
+ let title = trp"Packages" & " (" & $packages.len & ") "
+ let padding = title.runeLen
+ let lines = packages.map(p => p.name & "-" & p.newVersion).foldl(a & " " & b)
+ .splitLines(getWindowSize().width - padding)
+
+ echo()
+ echo(^Color.bold, title, ^Color.normal, lines[0])
+ for line in lines[1 .. ^1]:
+ echo(' '.repeat(padding), line)
+ echo()
+
+proc printComments*(color: bool, maintainer: Option[string],
+ comments: seq[CommentFormat]) =
+ echo()
+ for comment in comments:
+ let badge = if maintainer == some(comment.author):
+ ^Color.cyan & "[maintainer]" & ^Color.normal & " "
+ else:
+ ""
+ echo(^Color.blue & comment.author & ^Color.normal & " " & badge &
+ ^Color.bold & comment.date & ^Color.normal)
+ echo(comment.text.replace("\n\n", "\n"))
+ echo()
+
+proc printError*(color: bool, s: string) =
+ stderr.writeLine(^Color.red, trp"error: ", ^Color.normal, s)
+
+proc printWarning*(color: bool, s: string) =
+ stderr.writeLine(^Color.yellow, trp"warning: ", ^Color.normal, s)
+
+proc printColon*(color: bool, s: string) =
+ echo(^Color.blue, ":: ", ^Color.bold, s, ^Color.normal)
+
+proc printColonUserInput*(color: bool, s: string,
+ noconfirm: bool, default: string, cancel: string): string =
+ stdout.write(^Color.blue, ":: ", ^Color.bold, s, ^Color.normal, " ")
+ stdout.flushFile()
+ if noconfirm:
+ echo()
+ default
+ else:
+ try:
+ stdin.readLine()
+ except EOFError:
+ cancel
+
+proc printColonUserChoice*(color: bool, s: string, answers: openArray[char],
+ positive: char, negative: char, noconfirm: bool, default: char): char =
+ let answersStr = answers
+ .map(c => (if c == positive: c.toUpperAscii else: c))
+ .foldl(a & "/" & $b, "")
+
+ let input = printColonUserInput(color, s & " [" & answersStr[1 .. ^1] & "]",
+ noconfirm, $default, $negative)
+ if input.len == 0:
+ positive
+ elif input.len == 1:
+ let c = input[0].toLowerAscii
+ if c in answers: c else: negative
+ else:
+ negative
+
+proc printUserInputHelp*(operations: varargs[tuple[answer: char, description: string]]) =
+ for operation in (@operations & ('?', tr"view this help")):
+ echo(" ", operation.answer, " - ", operation.description)
+
+proc printProgressFull*(bar: bool, title: string): ((string, float) -> void, () -> void) =
+ let width = getWindowSize().width
+
+ if not bar or width <= 0:
+ echo(title, "...")
+ (proc (a: string, c: float) {.closure.} = discard, proc {.closure.} = discard)
+ else:
+ let infoLen = max(width * 6 / 10, 50).int
+ let progressLen = width - infoLen
+ let startTime = getTime().toUnix
+
+ var lastTime = startTime
+ var lastProgress = 0f
+ var averageSpeed = -1f
+
+ proc update(prefix: string, progress: float) {.closure.} =
+ let progressTrim = max(min(1, progress + 0.005), 0)
+ let progressStr = $(progressTrim * 100).int & "%"
+ let paddedProgressStr = ' '.repeat(5 - progressStr.len) & progressStr
+
+ let indicator = if progressLen > 8: (block:
+ let fullLen = progressLen - 8
+ let barLen = (fullLen.float * progressTrim).int
+ " [" & '#'.repeat(barLen) & '-'.repeat(fullLen - barLen) & "]")
+ else:
+ ""
+
+ let time = getTime().toUnix
+ if progress > lastProgress and time > lastTime:
+ let speed = (progress - lastProgress) / (time - lastTime).float
+ lastTime = time
+ lastProgress = progress
+ if averageSpeed < 0:
+ averageSpeed = speed
+ else:
+ const factor = 0.25
+ averageSpeed = factor * speed + (1 - factor) * averageSpeed
+
+ let timeLeft = if averageSpeed > 0: (block:
+ let secondsLeft = ((1 - progress) / averageSpeed).int
+ let seconds = secondsLeft %% 60
+ let minutes = secondsLeft /% 60
+ let secondsStr = if seconds < 10: "0" & $seconds else: $seconds
+ let minutesStr = if minutes < 10: "0" & $minutes else: $minutes
+ minutesStr & ":" & secondsStr)
+ else:
+ "--:--"
+
+ stdout.write(prefix, title,
+ ' '.repeat(infoLen - prefix.runeLen - title.runeLen - 1 - timeLeft.len),
+ ' ', timeLeft, indicator, paddedProgressStr, "\x1b[0K\r")
+ stdout.flushFile()
+ discard
+
+ proc terminate() {.closure.} =
+ echo()
+ discard
+
+ update(" ", 0)
+
+ (update, terminate)
+
+proc printProgressShare*(bar: bool, title: string): ((int, int) -> void, () -> void) =
+ let (updateFull, terminate) = printProgressFull(bar, title)
+
+ proc update(current: int, total: int) {.closure.} =
+ let prefix = if total > 0:
+ "(" & ' '.repeat(($total).len - ($current).len) & $current & "/" &
+ $total & ") "
+ else:
+ " "
+
+ updateFull(prefix, current / total)
+
+ (update, terminate)
diff --git a/src/main.nim b/src/main.nim
new file mode 100644
index 0000000..9e98c30
--- /dev/null
+++ b/src/main.nim
@@ -0,0 +1,260 @@
+import
+ future, options, os, posix, re, sequtils, strutils,
+ args, config, format, pacman, utils
+
+import
+ "feature/syncinfo",
+ "feature/syncsearch",
+ "feature/syncinstall",
+ "feature/localquery"
+
+proc passValidation(args: seq[Argument], config: Config,
+ nonRootArgs: openArray[OptionPair], rootArgs: openArray[OptionPair],
+ opts: varargs[seq[CommandOption]]): int =
+ let checkArgs = args.filterOptions(true, false, true, opts)
+
+ if checkArgs.len == 0:
+ let needRoot = (nonRootArgs.len == 0 and args.check(rootArgs)) or
+ (nonRootArgs.len > 0 and (not args.check(nonRootArgs) or args.check(rootArgs)))
+ return pacmanExec(needRoot, config.color, args.filterExtensions(true, true))
+ else:
+ let extensions = args.filterExtensions(false, false)
+ if extensions.len == 0:
+ return pacmanExec(false, config.color, args)
+ else:
+ let arg = extensions[0]
+ if arg.isShort:
+ raise commandError(trp("invalid option '-%c'\n").strip
+ .replace("%c", "$#") % arg.key)
+ else:
+ raise commandError(trp("invalid option '--%s'\n").strip
+ .replace("%s", "$#") % arg.key)
+
+proc handleDatabase(args: seq[Argument], config: Config): int =
+ let nonRootArgs = [
+ (some("k"), "check")
+ ]
+
+ passValidation(args, config, nonRootArgs, [],
+ commonOptions, databaseOptions)
+
+proc handleFiles(args: seq[Argument], config: Config): int =
+ let rootArgs = [
+ (some("y"), "refresh")
+ ]
+
+ passValidation(args, config, [], rootArgs,
+ commonOptions, filesOptions)
+
+proc handleQuery(args: seq[Argument], config: Config): int =
+ let queryArgs = args.removeMatchOptions(commonOptions)
+
+ if queryArgs.checkOpGroup(OpGroup.localQuery) and
+ not queryArgs.check((some("e"), "explicit")) and
+ queryArgs.check((some("d"), "deps")) and
+ queryArgs.count((some("t"), "unrequired")) >= 3:
+ handleQueryOrphans(args, config)
+ else:
+ passValidation(args, config, [], [],
+ commonOptions, queryOptions)
+
+proc handleRemove(args: seq[Argument], config: Config): int =
+ let nonRootArgs = [
+ (some("p"), "print"),
+ (none(string), "print-format")
+ ]
+
+ passValidation(args, config, nonRootArgs, [],
+ commonOptions, transactionOptions, removeOptions)
+
+proc handleSync(args: seq[Argument], config: Config): int =
+ let syncArgs = args.removeMatchOptions(commonOptions, transactionOptions, upgradeOptions)
+ let conflict = args.checkConflicts(syncConflictingOptions)
+
+ if conflict.isSome:
+ let (left, right) = conflict.unsafeGet
+ printError(config.color, trp("invalid option: '%s' and '%s' may not be used together\n") %
+ ["--" & left, "--" & right])
+ 1
+ elif syncArgs.check((some("i"), "info")) and
+ syncArgs.checkOpGroup(OpGroup.syncQuery):
+ handleSyncInfo(args, config)
+ elif syncArgs.check((some("s"), "search")) and
+ syncArgs.checkOpGroup(OpGroup.syncSearch):
+ handleSyncSearch(args, config)
+ elif syncArgs.checkOpGroup(OpGroup.syncInstall) and
+ (args.check((some("u"), "sysupgrade")) or args.targets.len > 0):
+ let isNonDefaultRoot = not config.isRootDefault
+ let isDowngrade = args.count((some("u"), "sysupgrade")) >= 2
+ let isSkipDeps = args.check((some("d"), "nodeps"))
+ let isRoot = getuid() == 0
+
+ let build = args.check((none(string), "build"))
+ let noaur = args.check((none(string), "noaur"))
+
+ let noBuild = isNonDefaultRoot or isDowngrade or isSkipDeps or isRoot
+
+ if build and noBuild:
+ if isNonDefaultRoot:
+ printError(config.color, tr"non-default root path is specified" & " -- " &
+ tr"building is not allowed")
+ elif isDowngrade:
+ printError(config.color, tr"downgrades are enabled" & " -- " &
+ tr"building is not allowed")
+ elif isSkipDeps:
+ printError(config.color, tr"dependency check is skipped" & " -- " &
+ tr"building is not allowed")
+ elif isRoot:
+ printError(config.color, tr"running as root" & " -- " &
+ tr"building is not allowed")
+ 1
+ else:
+ let noaurAdd = noBuild and not noaur
+
+ if noaurAdd:
+ if isNonDefaultRoot:
+ printWarning(config.color, tr"non-default root path is specified" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+ elif isDowngrade:
+ printWarning(config.color, tr"downgrades are enabled" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+ elif isSkipDeps:
+ printWarning(config.color, tr"dependency check is skipped" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+ elif isRoot:
+ printWarning(config.color, tr"running as root" & " -- " &
+ tr"'$#' is assumed" % ["--noaur"])
+
+ if noaurAdd:
+ handleSyncInstall(args & ("noaur", none(string), ArgumentType.long), config)
+ else:
+ handleSyncInstall(args, config)
+ else:
+ let nonRootArgs = [
+ (some("p"), "print"),
+ (none(string), "print-format"),
+ (some("g"), "groups"),
+ (some("i"), "info"),
+ (some("l"), "list"),
+ (some("s"), "search")
+ ]
+
+ let rootArgs = [
+ (some("y"), "refresh"),
+ ]
+
+ passValidation(args, config, nonRootArgs, rootArgs,
+ commonOptions, transactionOptions, upgradeOptions, syncOptions)
+
+proc handleDeptest(args: seq[Argument], config: Config): int =
+ passValidation(args, config, [], [], commonOptions)
+
+proc handleUpgrade(args: seq[Argument], config: Config): int =
+ let nonRootArgs = [
+ (some("p"), "print"),
+ (none(string), "print-format")
+ ]
+
+ passValidation(args, config, nonRootArgs, [],
+ commonOptions, transactionOptions, upgradeOptions)
+
+proc handleHelp(operation: OperationType) =
+ proc printHelp(command: string, text: string) =
+ echo(' '.repeat(6), "--", command, ' '.repeat(15 - command.len), text)
+
+ let operationArgs = operations
+ .filter(o => o.otype == operation)
+ .map(o => @["-" & o.pair.short.get])
+ .optFirst.get(@[]) & @["-h"]
+
+ let lines = runProgram(pacmanCmd & operationArgs)
+
+ for line in lines:
+ echo(line.replace(re"\bpacman\b", "pakku"))
+
+ if lines.len > 0:
+ case operation:
+ of OperationType.sync:
+ printHelp("build", tr"build targets from source")
+ printHelp("noaur", tr"disable all AUR operations")
+ else:
+ discard
+
+const
+ version = $getenv("PROG_VERSION")
+ copyright = $getenv("PROG_COPYRIGHT")
+
+proc handleVersion(): int =
+ echo()
+ echo(' '.repeat(23), "Pakku v", version)
+ echo(' '.repeat(23), "Copyright (C) ", copyright)
+ pacmanExec(false, false, ("V", none(string), ArgumentType.short))
+
+proc signal(sign: cint, handler: pointer): pointer
+ {.importc, header: "<signal.h>".}
+
+discard setlocale(LC_ALL, "")
+discard signal(SIGINT, cast[pointer](SIG_DFL))
+
+template withErrorHandler(propColor: Option[bool], T: typedesc, body: untyped):
+ tuple[success: Option[T], code: int] =
+ try:
+ (some(body), 0)
+ except HaltError:
+ let e = (ref HaltError) getCurrentException()
+ (none(T), e.code)
+ except CommandError:
+ let e = (ref CommandError) getCurrentException()
+ if e.error:
+ printError(e.color.orElse(propColor).get(false), e.msg)
+ else:
+ stderr.writeLine(e.msg)
+ (none(T), 1)
+
+let init = withErrorHandler(none(bool),
+ tuple[parsedArgs: seq[Argument], config: Config]):
+ let parsedArgs = splitArgs(commandLineParams(), optionsWithParameter)
+ let pacmanConfig = obtainPacmanConfig(parsedArgs)
+ let config = obtainConfig(pacmanConfig)
+ (parsedArgs, config)
+
+proc run(parsedArgs: seq[Argument], config: Config):
+ tuple[success: Option[int], code: int] =
+ withErrorHandler(some(config.color), int):
+ let operation = getOperation(parsedArgs)
+ if operation != OperationType.invalid and
+ parsedArgs.check((some("h"), "help")):
+ handleHelp(operation)
+ 0
+ elif operation != OperationType.invalid and
+ parsedArgs.check((some("V"), "version")):
+ handleVersion()
+ else:
+ case operation:
+ of OperationType.database:
+ handleDatabase(parsedArgs, config)
+ of OperationType.files:
+ handleFiles(parsedArgs, config)
+ of OperationType.query:
+ handleQuery(parsedArgs, config)
+ of OperationType.remove:
+ handleRemove(parsedArgs, config)
+ of OperationType.sync:
+ handleSync(parsedArgs, config)
+ of OperationType.deptest:
+ handleDeptest(parsedArgs, config)
+ of OperationType.upgrade:
+ handleUpgrade(parsedArgs, config)
+ else:
+ pacmanExec(false, config.color,
+ parsedArgs.filterExtensions(true, true))
+
+let runResult = if init.success.isSome:
+ run(init.success.unsafeGet.parsedArgs, init.success.unsafeGet.config)
+ else:
+ (none(int), init.code)
+
+programResult = if runResult.success.isSome:
+ runResult.success.unsafeGet
+ else:
+ runResult.code
diff --git a/src/package.nim b/src/package.nim
new file mode 100644
index 0000000..699aee5
--- /dev/null
+++ b/src/package.nim
@@ -0,0 +1,249 @@
+import
+ future, options, os, re, sequtils, sets, strutils, tables,
+ utils
+
+type
+ ConstraintOperation* {.pure.} = enum
+ ge = ">=",
+ gt = ">",
+ eq = "=",
+ lt = "<",
+ le = "<="
+
+ VersionConstraint* = tuple[
+ operation: ConstraintOperation,
+ version: string
+ ]
+
+ PackageReference* = tuple[
+ name: string,
+ description: Option[string],
+ constraint: Option[VersionConstraint]
+ ]
+
+ ArchPackageReference* = tuple[
+ arch: Option[string],
+ reference: PackageReference
+ ]
+
+ RpcPackageInfo* = object of RootObj
+ repo*: string
+ base*: string
+ name*: string
+ version*: string
+ description*: Option[string]
+ maintainer*: Option[string]
+ firstSubmitted*: Option[int64]
+ lastModified*: Option[int64]
+ votes*: int
+ popularity*: float
+
+ PackageInfo* = object of RpcPackageInfo
+ archs*: seq[string]
+ url*: Option[string]
+ licenses*: seq[string]
+ groups*: seq[string]
+ depends*: seq[ArchPackageReference]
+ makeDepends*: seq[ArchPackageReference]
+ checkDepends*: seq[ArchPackageReference]
+ optional*: seq[ArchPackageReference]
+ provides*: seq[ArchPackageReference]
+ conflicts*: seq[ArchPackageReference]
+ replaces*: seq[ArchPackageReference]
+ gitUrl*: string
+ gitBranch*: Option[string]
+ gitCommit*: Option[string]
+ gitPath*: Option[string]
+
+ GitRepo* = tuple[
+ url: string,
+ branch: string,
+ path: string
+ ]
+
+ PackageRepo = tuple[
+ os: HashSet[string],
+ repo: HashSet[string],
+ git: GitRepo
+ ]
+
+ SrcInfoPair = tuple[key: string, value: string]
+
+const
+ packageRepos: seq[PackageRepo] = @[
+ (["arch"].toSet,
+ ["core", "extra", "testing"].toSet,
+ ("https://git.archlinux.org/svntogit/packages.git",
+ "packages/${BASE}", "repos/${REPO}-${ARCH}")),
+ (["arch"].toSet,
+ ["community", "community-testing", "multilib", "multilib-testing"].toSet,
+ ("https://git.archlinux.org/svntogit/community.git",
+ "packages/${BASE}", "repos/${REPO}-${ARCH}"))
+ ]
+
+static:
+ # test only single match available
+ let osSet = lc[x | (r <- packageRepos, x <- r.os), string].toSet
+ let repoSet = lc[x | (r <- packageRepos, x <- r.repo), string].toSet
+ for os in osSet:
+ for repo in repoSet:
+ let osValue = os
+ let repoValue = repo
+ if packageRepos.filter(pr => osValue in pr.os and repoValue in pr.repo).len >= 2:
+ raise newException(SystemError,
+ "only single matching repo available: " & os & ":" & repo)
+
+proc readOsId: string =
+ var file: File
+ if file.open("/usr/bin/os-release"):
+ try:
+ while true:
+ let rawLine = readLine(file)
+ if rawLine[0 .. 2] == "ID=":
+ return rawLine[3 .. ^1]
+ except EOFError:
+ discard
+ except IOError:
+ discard
+ finally:
+ file.close()
+ return "arch"
+
+let osId = readOsId()
+
+proc lookupGitRepo*(repo: string, base: string, arch: string): Option[GitRepo] =
+ template replaceAll(gitPart: string): string =
+ gitPart
+ .replace("${REPO}", repo)
+ .replace("${BASE}", base)
+ .replace("${ARCH}", arch)
+
+ packageRepos
+ .filter(pr => osId in pr.os and repo in pr.repo)
+ .map(pr => (pr.git.url.replaceAll, pr.git.branch.replaceAll, pr.git.path.replaceAll))
+ .optFirst
+
+template repoPath*(tmpRoot: string, base: string): string =
+ tmpRoot & "/" & base
+
+template buildPath*(repoPath: string, gitPath: Option[string]): string =
+ gitPath.map(p => repoPath & "/" & p).get(repoPath)
+
+template allDepends*(pkgInfo: PackageInfo): seq[ArchPackageReference] =
+ pkgInfo.depends & pkgInfo.makeDepends & pkgInfo.checkDepends
+
+proc parseSrcInfoKeys(srcInfo: string):
+ tuple[baseSeq: ref seq[SrcInfoPair], table: Table[string, ref seq[SrcInfoPair]]] =
+ var table = initTable[string, ref seq[SrcInfoPair]]()
+ var matches: array[2, string]
+ var baseSeq: ref seq[SrcInfoPair]
+ var values: ref seq[SrcInfoPair]
+
+ new(baseSeq)
+ baseSeq[] = newSeq[SrcInfoPair]()
+
+ for line in srcInfo.splitLines:
+ if line.match(re"[\t\ ]*(\w+)\ =\ (.*)", matches):
+ let key = matches[0]
+ let value = matches[1]
+
+ if key == "pkgbase":
+ values = baseSeq
+ elif key == "pkgname":
+ if table.hasKey(value):
+ values = table[value]
+ else:
+ new(values)
+ values[] = newSeq[SrcInfoPair]()
+ table[value] = values
+
+ if values != nil:
+ values[] &= (key: key, value: value)
+
+ (baseSeq: baseSeq, table: table)
+
+proc parseSrcInfoName(repo: string, name: string, rpcInfos: seq[RpcPackageInfo],
+ baseSeq: ref seq[SrcInfoPair], nameSeq: ref seq[SrcInfoPair],
+ gitUrl: string, gitBranch: Option[string], gitCommit: Option[string],
+ gitPath: Option[string]): Option[PackageInfo] =
+ let pairs = baseSeq[] & nameSeq[]
+ proc collect(keyName: string): seq[string] =
+ lc[x.value | (x <- pairs, x.key == keyName), string]
+
+ proc splitConstraint(name: string): PackageReference =
+ var matches: array[3, string]
+
+ let descIndex = name.find(": ")
+ let (description, workName) = if descIndex >= 0:
+ (some(name[descIndex + 2 .. ^1]), name[0 .. descIndex - 1])
+ else:
+ (none(string), name)
+
+ if workName.match(re"([^><=]*)\ *(>|<|=|>=|<=)\ *([^ ]*)", matches):
+ let constraints = toSeq(enumerate[ConstraintOperation]())
+ let index = constraints.map(s => $s).find(matches[1])
+
+ if index >= 0:
+ (matches[0], description, some((constraints[index], matches[2])))
+ else:
+ (matches[0], description, none(VersionConstraint))
+ else:
+ (workName, description, none(VersionConstraint))
+
+ proc collectArch(keyName: string, arch: Option[string]): seq[ArchPackageReference] =
+ collect(arch.map(a => keyName & "_" & a).get(keyName))
+ .map(splitConstraint)
+ .map(c => (arch, (c.name, c.description, c.constraint)))
+
+ proc collectArchs(keyName: string, archs: seq[string]): seq[ArchPackageReference] =
+ let archsFull = none(string) & archs.map(some)
+ lc[x | (a <- archsFull, x <- collectArch(keyName, a)), ArchPackageReference]
+
+ let base = lc[x.value | (x <- baseSeq[], x.key == "pkgbase"), string].optLast
+
+ let version = collect("pkgver").optLast
+ let release = collect("pkgrel").optLast
+ let epoch = collect("epoch").optLast
+ let versionFull = lc[(v & "-" & r) | (v <- version, r <- release), string].optLast
+ .map(v => epoch.map(e => e & ":" & v).get(v))
+
+ let description = collect("pkgdesc").optLast
+ let archs = collect("arch").filter(a => a != "any")
+ let url = collect("url").optLast
+ let licenses = collect("license")
+ let groups = collect("groups")
+
+ let depends = collectArchs("depends", archs)
+ let makeDepends = collectArchs("makedepends", archs)
+ let checkDepends = collectArchs("checkdepends", archs)
+ let optional = collectArchs("optdepends", archs)
+ let provides = collectArchs("provides", archs)
+ let conflicts = collectArchs("conflicts", archs)
+ let replaces = collectArchs("replaces", archs)
+
+ let info = rpcInfos.filter(i => i.name == name).optLast
+
+ lc[PackageInfo(repo: repo, base: b, name: name, version: v, description: description,
+ archs: archs, url: url, licenses: licenses, groups: groups,
+ depends: depends, makeDepends: makeDepends, checkdepends: checkDepends,
+ optional: optional, provides: provides, conflicts: conflicts, replaces: replaces,
+ maintainer: info.map(i => i.maintainer).flatten,
+ firstSubmitted: info.map( i => i.firstSubmitted).flatten,
+ lastModified: info.map( i => i.lastModified).flatten,
+ votes: info.map(i => i.votes).get(0),
+ popularity: info.map(i => i.popularity).get(0),
+ gitUrl: gitUrl, gitBranch: gitBranch, gitCommit: gitCommit, gitPath: gitPath) |
+ (b <- base, v <- versionFull), PackageInfo].optLast
+
+proc parseSrcInfo*(repo: string, srcInfo: string,
+ gitUrl: string, gitBranch: Option[string], gitCommit: Option[string],
+ gitPath: Option[string], rpcInfos: seq[RpcPackageInfo] = @[]): seq[PackageInfo] =
+ let parsed = parseSrcInfoKeys(srcInfo)
+ let packageSeq = toSeq(parsed.table.namedPairs)
+ lc[x | (pair <- packageSeq, x <- parseSrcInfoName(repo, pair.key, rpcInfos,
+ parsed.baseSeq, pair.value, gitUrl, gitBranch, gitCommit, gitPath)), PackageInfo]
+
+proc `$`*(reference: PackageReference): string =
+ reference.constraint
+ .map(c => reference.name & $c.operation & c.version)
+ .get(reference.name)
diff --git a/src/pacman.nim b/src/pacman.nim
new file mode 100644
index 0000000..c457f49
--- /dev/null
+++ b/src/pacman.nim
@@ -0,0 +1,367 @@
+import
+ future, macros, options, os, posix, re, sequtils, sets, strutils, tables,
+ args, config, utils
+
+type
+ OpGroup* {.pure.} = enum
+ syncInstall, syncSearch, syncQuery, localQuery
+
+ OperationType* {.pure.} = enum
+ unknown, invalid, database, files, query,
+ remove, sync, deptest, upgrade
+
+ Operation* = tuple[
+ pair: OptionPair,
+ otype: OperationType
+ ]
+
+ CommandOption* = tuple[
+ pair: OptionPair,
+ hasParam: bool,
+ extension: bool,
+ groups: set[OpGroup]
+ ]
+
+ ConflictingOptions* = tuple[
+ left: string,
+ right: seq[string]
+ ]
+
+proc calculateOptionsWithParameter(opts: seq[CommandOption]): seq[OptionKey] {.compileTime.} =
+ proc commandToSeq(co: CommandOption): seq[OptionKey] {.compileTime.} =
+ if co.hasParam:
+ co.pair.short
+ .map(s => @[(s, false), (co.pair.long, true)])
+ .get(@[(co.pair.long, true)])
+ else:
+ @[]
+
+ lc[x | (y <- opts, x <- commandToSeq(y)), OptionKey]
+
+proc o(long: string): CommandOption {.compileTime.} =
+ ((none(string), long), false, false, {})
+
+proc o(short: string, long: string): CommandOption {.compileTime.} =
+ ((some(short), long), false, false, {})
+
+proc `^`(opt: CommandOption): CommandOption {.compileTime.} =
+ (opt.pair, not opt.hasParam, opt.extension, opt.groups)
+
+proc `$`(opt: CommandOption): CommandOption {.compileTime.} =
+ (opt.pair, opt.hasParam, not opt.extension, opt.groups)
+
+proc `+`(opt: CommandOption, groups: set[OpGroup]): CommandOption {.compileTime.} =
+ (opt.pair, opt.hasParam, opt.extension, opt.groups + groups)
+
+macro g(gls: varargs[untyped]): untyped =
+ result = newNimNode(nnkCurly, gls)
+ for gl in gls:
+ add(result, newDotExpr(ident("OpGroup"), gl))
+
+const
+ operations*: seq[Operation] = @[
+ ((some("D"), "database"), OperationType.database),
+ ((some("F"), "files"), OperationType.files),
+ ((some("Q"), "query"), OperationType.query),
+ ((some("R"), "remove"), OperationType.remove),
+ ((some("S"), "sync"), OperationType.sync),
+ ((some("T"), "deptest"), OperationType.deptest),
+ ((some("U"), "upgrade"), OperationType.upgrade)
+ ]
+
+ commonOptions*: seq[CommandOption] = @[
+ ^o("b", "dbpath"),
+ ^o("r", "root"),
+ o("v", "verbose"),
+ ^o("arch"),
+ ^o("cachedir"),
+ ^o("color"),
+ ^o("config"),
+ o("debug"),
+ ^o("gpgdir"),
+ ^o("hookdir"),
+ ^o("logfile"),
+ o("noconfirm"),
+ o("confirm")
+ ]
+
+ transactionOptions*: seq[CommandOption] = @[
+ o("d", "nodeps"),
+ ^o("assume-installed"),
+ o("dbonly"),
+ o("noprogressbar"),
+ o("noscriptlet"),
+ o("p", "print"),
+ ^o("print-format")
+ ]
+
+ upgradeOptions*: seq[CommandOption] = @[
+ o("force"),
+ o("asdeps"),
+ o("asexplicit"),
+ ^o("ignore"),
+ ^o("ignoregroup"),
+ o("needed")
+ ]
+
+ queryOptions*: seq[CommandOption] = @[
+ o("c", "changelog") + g(localQuery),
+ o("d", "deps") + g(localQuery),
+ o("e", "explicit") + g(localQuery),
+ o("g", "groups"),
+ o("i", "info") + g(localQuery),
+ o("k", "check") + g(localQuery),
+ o("l", "list") + g(localQuery),
+ o("m", "foreign") + g(localQuery),
+ o("n", "native") + g(localQuery),
+ o("o", "owns"),
+ o("p", "file"),
+ o("q", "quiet") + g(localQuery),
+ o("s", "search"),
+ o("t", "unrequired") + g(localQuery),
+ o("u", "upgrades") + g(localQuery)
+ ]
+
+ removeOptions*: seq[CommandOption] = @[
+ o("c", "cascade"),
+ o("n", "nosave"),
+ o("s", "recursive"),
+ o("u", "unneeded")
+ ]
+
+ syncOptions*: seq[CommandOption] = @[
+ o("c", "clean"),
+ o("g", "groups"),
+ o("i", "info") + g(syncInstall, syncQuery),
+ o("l", "list"),
+ o("q", "quiet") + g(syncInstall, syncSearch, syncQuery),
+ o("s", "search") + g(syncSearch),
+ o("u", "sysupgrade") + g(syncInstall),
+ o("w", "downloadonly"),
+ o("y", "refresh") + g(syncInstall, syncSearch, syncQuery),
+ $o("build") + g(syncInstall),
+ $o("noaur") + g(syncInstall)
+ ]
+
+ databaseOptions*: seq[CommandOption] = @[
+ o("asdeps"),
+ o("asexplicit"),
+ o("k", "check")
+ ]
+
+ filesOptions*: seq[CommandOption] = @[
+ o("y", "refresh"),
+ o("l", "list"),
+ o("s", "search"),
+ o("x", "regex"),
+ o("o", "owns"),
+ o("q", "quiet"),
+ o("machinereadable")
+ ]
+
+ upgradeCommonOptions*: seq[CommandOption] = @[
+ o("noprogressbar"),
+ o("force")
+ ]
+
+ allOptions = commonOptions & transactionOptions &
+ upgradeOptions & queryOptions & removeOptions & syncOptions &
+ databaseOptions & filesOptions
+
+ optionsWithParameter*: HashSet[OptionKey] =
+ calculateOptionsWithParameter(allOptions).toSet
+
+ syncConflictingOptions*: seq[ConflictingOptions] = @[
+ ("asdeps", @["asexplicit"]),
+ ("build", @["nodeps", "assume-installed", "dbonly", "clean",
+ "groups", "info", "list", "search", "sysupgrade", "downloadonly"])
+ ]
+
+ allConflictingOptions = syncConflictingOptions
+
+proc checkOptions(check: seq[CommandOption],
+ where: openArray[seq[CommandOption]]) {.compileTime.} =
+ let whereSeq = @where
+ let whereSet = lc[x.pair | (y <- whereSeq, x <- y), OptionPair].toSet
+ for c in check:
+ if not (c.pair in whereSet):
+ raise newException(SystemError,
+ "invalid options definition: " & $c.pair)
+
+static:
+ # options test
+ checkOptions(upgradeCommonOptions, [commonOptions, transactionOptions, upgradeOptions])
+
+proc getOperation*(args: seq[Argument]): OperationType =
+ let matchedOps = args
+ .map(arg => operations
+ .filter(o => (arg.isShort and some(arg.key) == o.pair.short) or
+ (arg.isLong and arg.key == o.pair.long)))
+ .filter(ops => ops.len > 0)
+
+ if matchedOps.len == 0:
+ OperationType.unknown
+ elif matchedOps.len == 1:
+ matchedOps[0][0].otype
+ else:
+ OperationType.invalid
+
+proc filterOptions*(args: seq[Argument], removeMatches: bool, keepTargets: bool,
+ includeOptions: bool, opts: varargs[seq[CommandOption]]): seq[Argument] =
+ let optsSeq = @opts
+ let optsPairSeq = lc[x.pair | (y <- optsSeq, x <- y), OptionPair]
+
+ let work = if includeOptions:
+ (optsPairSeq & operations.map(o => o.pair))
+ else:
+ optsPairSeq
+
+ args.filter(removeMatches, keepTargets, work)
+
+template removeMatchOptions*(args: seq[Argument],
+ opts: varargs[seq[CommandOption]]): seq[Argument] =
+ filterOptions(args, true, true, true, opts)
+
+template keepOnlyOptions*(args: seq[Argument],
+ opts: varargs[seq[CommandOption]]): seq[Argument] =
+ filterOptions(args, false, false, false, opts)
+
+proc checkValid*(args: seq[Argument], opts: varargs[seq[CommandOption]]): bool =
+ filterOptions(args, true, false, true, opts).len == 0
+
+proc checkOpGroup*(args: seq[Argument], group: OpGroup): bool =
+ let toCheck = allOptions
+ .filter(o => group in o.groups)
+ .map(o => o.pair)
+
+ args.whitelisted(toCheck)
+
+proc filterExtensions*(args: seq[Argument],
+ removeMatches: bool, keepTargets: bool): seq[Argument] =
+ let argsSeq = lc[x.pair | (x <- allOptions, x.extension), OptionPair]
+ args.filter(removeMatches, keepTargets, argsSeq)
+
+proc obtainConflictsPairs(conflicts: seq[ConflictingOptions]): Table[string, seq[OptionPair]] =
+ let all = lc[x | (y <- conflicts, x <- y.left & y.right), string].deduplicate
+ all.map(c => (c, allOptions.filter(o => o.pair.long == c)
+ .map(o => o.pair).deduplicate)).toTable
+
+static:
+ # conflicting options test
+ for name, pairs in allConflictingOptions.obtainConflictsPairs:
+ if pairs.len != 1:
+ raise newException(SystemError,
+ "invalid conflicts definition: " & name & " " & $pairs)
+
+proc checkConflicts*(args: seq[Argument],
+ conflicts: seq[ConflictingOptions]): Option[(string, string)] =
+ let table = conflicts.obtainConflictsPairs
+ template full(s: string): OptionPair = table[s][0]
+
+ lc[(c.left, w) | (c <- conflicts, args.check(c.left.full),
+ w <- c.right, args.check(w.full)), (string, string)].optFirst
+
+proc checkExec(file: string): bool =
+ var statv: Stat
+ stat(file, statv) == 0 and (statv.st_mode and S_IXUSR) == S_IXUSR
+
+proc pacmanExec(root: bool, args: varargs[string]): int =
+ let exec = if root and checkExec(sudoCmd):
+ @[sudoCmd, pacmanCmd] & @args
+ elif root and checkExec(suCmd):
+ @[suCmd, "root", "-c", "exec \"$@\"", "--", "sh", pacmanCmd] & @args
+ else:
+ @[pacmanCmd] & @args
+
+ execResult(exec)
+
+proc pacmanExec*(root: bool, color: bool, args: varargs[Argument]): int =
+ let useRoot = root and getuid() != 0
+ let colorStr = if color: "always" else: "never"
+
+ let argsSeq = ("color", some(colorStr), ArgumentType.long) &
+ @args.filter(arg => not arg.matchOption((none(string), "color")))
+ let collectedArgs = lc[x | (y <- argsSeq, x <- y.collectArg), string]
+
+ pacmanExec(useRoot, collectedArgs)
+
+proc pacmanRun*(root: bool, color: bool, args: varargs[Argument]): int =
+ let argsSeq = @args
+ forkWait(() => pacmanExec(root, color, argsSeq))
+
+proc pacmanValidateAndThrow(args: varargs[Argument]): void =
+ let argsSeq = @args
+ let collectedArgs = lc[x | (y <- argsSeq, x <- y.collectArg), string]
+ let code = forkWait(() => pacmanExec(false, "-T" & collectedArgs))
+ if code != 0:
+ raise haltError(code)
+
+proc getMachineName: Option[string] =
+ var utsname: Utsname
+ let length = if uname(utsname) == 0: utsname.machine.find('\0') else: -1
+ if length > 0: some(utsname.machine.toString(some(length))) else: none(string)
+
+proc createConfigFromTable(table: Table[string, string], dbs: seq[string]): PacmanConfig =
+ let root = table.opt("RootDir")
+ let db = table.opt("DBPath")
+ let color = if table.hasKey("Color"): ColorMode.colorAuto else: ColorMode.colorNever
+ let verbosePkgList = table.hasKey("VerbosePkgLists")
+ let arch = table.opt("Architecture").get("auto")
+ let ignorePkgs = table.opt("IgnorePkg").get("").splitWhitespace.toSet
+ let ignoreGroups = table.opt("IgnoreGroup").get("").splitWhitespace.toSet
+
+ let archFinal = if arch.len == 0 or arch == "auto": getMachineName().get(arch) else: arch
+ if archFinal.len == 0 or archFinal == "auto":
+ raise commandError(tr"can not get the architecture",
+ colorNeeded = some(color.get))
+
+ PacmanConfig(rootOption: root, dbOption: db, dbs: dbs,
+ arch: archFinal, colorMode: color, debug: false,
+ progressBar: true, verbosePkgList: verbosePkgList,
+ ignorePkgs: ignorePkgs, ignoreGroups: ignoreGroups)
+
+proc obtainPacmanConfig*(args: seq[Argument]): PacmanConfig =
+ proc getAll(pair: OptionPair): seq[string] =
+ args.filter(arg => arg.matchOption(pair)).map(arg => arg.value.get)
+
+ let configFile = getAll((none(string), "config")).optLast.get(sysConfDir & "/pacman.conf")
+ let (configTable, wasError) = readConfigFile(configFile)
+
+ let options = configTable.opt("options").map(t => t[]).get(initTable[string, string]())
+ let dbs = toSeq(configTable.keys).filter(k => k != "options")
+ let defaultConfig = createConfigFromTable(options, dbs)
+
+ if wasError:
+ pacmanValidateAndThrow(("config", some(configFile), ArgumentType.long))
+
+ proc getColor(color: string): ColorMode =
+ let colors = toSeq(enumerate[ColorMode]())
+ colors.filter(c => $c == color).optLast.get(ColorMode.colorNever)
+
+ let root = getAll((some("r"), "root")).optLast.orElse(defaultConfig.rootOption)
+ let db = getAll((some("b"), "dbpath")).optLast.orElse(defaultConfig.dbOption)
+ let arch = getAll((none(string), "arch")).optLast.get(defaultConfig.arch)
+ let colorStr = getAll((none(string), "color")).optLast.get($defaultConfig.colorMode)
+ let color = getColor(colorStr)
+
+ let debug = args.check((none(string), "debug"))
+ let progressBar = not args.check((none(string), "noprogressbar"))
+ let ignorePkgs = getAll((none(string), "ignore")).toSet
+ let ignoreGroups = getAll((none(string), "ignoregroups")).toSet
+
+ let config = PacmanConfig(rootOption: root, dbOption: db, dbs: defaultConfig.dbs,
+ arch: arch, colorMode: color, debug: debug,
+ progressBar: progressBar, verbosePkgList: defaultConfig.verbosePkgList,
+ ignorePkgs: ignorePkgs + defaultConfig.ignorePkgs,
+ ignoreGroups: ignoreGroups + defaultConfig.ignoreGroups)
+
+ if config.dbs.find("aur") >= 0:
+ raise commandError(tr"repo '$#' is reserved by this program" % ["aur"],
+ colorNeeded = some(color.get))
+
+ pacmanValidateAndThrow(("root", some(config.root), ArgumentType.long),
+ ("dbpath", some(config.db), ArgumentType.long),
+ ("arch", some(config.arch), ArgumentType.long),
+ ("color", some(colorStr), ArgumentType.long))
+
+ config
diff --git a/src/utils.nim b/src/utils.nim
new file mode 100644
index 0000000..1d7c6bd
--- /dev/null
+++ b/src/utils.nim
@@ -0,0 +1,203 @@
+import
+ future, hashes, options, os, osproc, posix, strutils, tables
+
+type
+ HaltError* = object of Exception
+ code*: int
+
+ CommandError* = object of Exception
+ color*: Option[bool]
+ error*: bool
+
+const
+ pkgLibDir* = getenv("PROG_PKGLIBDIR")
+ localStateDir* = getenv("PROG_LOCALSTATEDIR")
+ sysConfDir* = getenv("PROG_SYSCONFDIR")
+
+ bashCmd* = "/bin/bash"
+ suCmd* = "/usr/bin/su"
+ sudoCmd* = "/usr/bin/sudo"
+ gitCmd* = "/usr/bin/git"
+ pacmanCmd* = "/usr/bin/pacman"
+ makepkgCmd* = "/usr/bin/makepkg"
+
+template haltError*(code: int): untyped =
+ var e: ref HaltError
+ new(e)
+ e.code = code
+ e
+
+template commandError*(message: string, colorNeeded: Option[bool] = none(bool),
+ showError: bool = true): untyped =
+ var e: ref CommandError
+ new(e)
+ e.msg = message
+ e.color = colorNeeded
+ e.error = showError
+ e
+
+iterator items*[T](self: Option[T]): T {.raises: [].} =
+ if self.isSome:
+ yield self.unsafeGet
+
+template len*[T](self: Option[T]): int =
+ if self.isSome: 1 else: 0
+
+template orElse*[T, R](opt1: Option[T], opt2: Option[R]): Option[R] =
+ if opt1.isSome: opt1 else: opt2
+
+template hash*[T](opt: Option[T]): int =
+ opt.map(hash).get(0)
+
+proc opt*[K, V](table: Table[K, V], key: K): Option[V] =
+ if table.hasKey(key): some(table[key]) else: none(V)
+
+proc opt*[K, V](table: OrderedTable[K, V], key: K): Option[V] =
+ if table.hasKey(key): some(table[key]) else: none(V)
+
+proc optFirst*[T](s: openArray[T]): Option[T] =
+ if s.len > 0: some(s[s.low]) else: none(T)
+
+proc optLast*[T](s: openArray[T]): Option[T] =
+ if s.len > 0: some(s[s.high]) else: none(T)
+
+iterator enumerate*[T: enum]: T =
+ let elow = T.low.ord
+ let ehigh = T.high.ord
+ for i in elow .. ehigh:
+ yield T(i)
+
+iterator namedPairs*[K, V](table: Table[K, V]): tuple[key: K, value: V] =
+ for key, value in table.pairs:
+ yield (key, value)
+
+iterator reversed*[T](s: openArray[T]): T =
+ for i in countdown(s.len - 1, 0):
+ yield s[i]
+
+proc groupBy*[T, X](s: seq[T], callback: T -> X): seq[tuple[key: X, values: seq[T]]] =
+ var table = initOrderedTable[X, ref seq[T]]()
+ for value in s:
+ let key = callback(value)
+ var work: ref seq[T]
+ if table.hasKey(key):
+ work = table[key]
+ else:
+ new(work)
+ work[] = newSeq[T]()
+ table[key] = work
+ work[] &= value
+
+ result = newSeq[tuple[key: X, values: seq[T]]]()
+ for key, values in table.pairs:
+ result &= (key, values[])
+
+proc perror*(s: cstring): void {.importc, header: "<stdio.h>".}
+template perror*: void = perror(getAppFilename())
+
+proc execResult*(args: varargs[string]): int =
+ let cexec = allocCStringArray(args)
+ let code = execvp(cexec[0], cexec)
+ perror()
+ deallocCStringArray(cexec)
+ code
+
+const
+ interruptSignals* = [SIGINT, SIGTERM]
+
+template blockSignals*(signals: openArray[cint],
+ unblock: untyped, body: untyped): untyped =
+ block:
+ var sigset: Sigset
+ var sigoldset: Sigset
+
+ discard sigemptyset(sigset)
+ for s in signals:
+ discard sigaddset(sigset, s)
+ discard sigprocmask(SIG_BLOCK, sigset, sigoldset)
+
+ var unblocked = false
+ let unblock = () => (block:
+ if not unblocked:
+ discard sigprocmask(SIG_SETMASK, sigoldset, sigset)
+ unblocked = true)
+
+ try:
+ body
+ finally:
+ unblock()
+
+proc forkWait*(call: () -> int): int =
+ blockSignals(interruptSignals, unblock):
+ let pid = fork()
+ if pid == 0:
+ unblock()
+ quit(call())
+ else:
+ var status: cint = 1
+ discard waitpid(pid, status, 0)
+ if WIFEXITED(status):
+ return WEXITSTATUS(status)
+ else:
+ discard kill(getpid(), status)
+ return 1
+
+proc runProgram*(args: varargs[string]): seq[string] =
+ let output = execProcess(args[0], @args[1 .. ^1], options = {})
+ if output.len == 0:
+ @[]
+ elif output.len > 0 and $output[^1] == "\n":
+ output[0 .. ^2].split("\n")
+ else:
+ output.split("\n")
+
+proc setenv*(name: cstring, value: cstring, override: cint): cint
+ {.importc, header: "<stdlib.h>".}
+
+proc getUser*: (int, string) =
+ let uid = getuid()
+ while true:
+ var pw = getpwent()
+ if pw == nil:
+ raise newException(SystemError, "")
+ if pw.pw_uid == uid:
+ return (uid.int, $pw.pw_name)
+
+proc toString*[T](arr: array[T, char], length: Option[int]): string =
+ var workLength = length.get(T.high + 1)
+ var str = newStringOfCap(workLength)
+ for i in 0 .. workLength - 1:
+ let c = arr[i]
+ if length.isNone and c == '\0':
+ break
+ str.add(arr[i])
+ str
+
+proc removeDirQuiet*(s: string) =
+ try:
+ removeDir(s)
+ except:
+ discard
+
+proc dgettext(domain: cstring, s: cstring): cstring
+ {.cdecl, importc: "dgettext".}
+
+proc gettext(domain: string, s: string): string =
+ let translated = dgettext(domain, s)
+ if translated != nil: $translated else: s
+
+proc gettextHandle(domain: string, s: string): string =
+ let res = gettext(domain, s).replace("%s", "$#").replace("%c", "$#")
+ if res.len > 0 and res[^1 .. ^1] == "\n": res[0 .. ^2] else: res
+
+template tr*(s: string): string =
+ gettext("pakku", s)
+
+template trp*(s: string): string =
+ gettextHandle("pacman", s)
+
+template tra*(s: string): string =
+ gettextHandle("libalpm", s)
+
+template trc*(s: string): string =
+ gettextHandle("libc", s)
diff --git a/src/wrapper/alpm.nim b/src/wrapper/alpm.nim
new file mode 100644
index 0000000..00b5b86
--- /dev/null
+++ b/src/wrapper/alpm.nim
@@ -0,0 +1,146 @@
+import
+ strutils,
+ "../utils"
+
+type
+ AlpmHandle* = object
+ AlpmDatabase* = object
+ AlpmPackage* = object
+
+ AlpmList*[T] = object
+ data*: T
+ prev*: ptr AlpmList[T]
+ next*: ptr AlpmList[T]
+
+ AlpmDepMod* {.pure, size: sizeof(cint).} = enum
+ no = 1,
+ eq = 2,
+ ge = 3,
+ le = 4,
+ gt = 5,
+ lt = 6
+
+ AlpmReason* {.pure, size: sizeof(cint).} = enum
+ explicit = 0,
+ depend = 1
+
+ AlpmDependency* = object
+ name*: cstring
+ version*: cstring
+ desc*: cstring
+ nameHash: culong
+ depMod*: AlpmDepMod
+
+ AlpmGroup* = object
+ name*: cstring
+ packages*: ptr AlpmList[ptr AlpmPackage]
+
+{.passL: "-lalpm".}
+
+proc newAlpmHandle*(root: cstring, dbpath: cstring, err: var cint): ptr AlpmHandle
+ {.cdecl, importc: "alpm_initialize".}
+
+proc release*(handle: ptr AlpmHandle): cint
+ {.cdecl, importc: "alpm_release".}
+
+proc setArch*(handle: ptr AlpmHandle, arch: cstring): cint
+ {.cdecl, importc: "alpm_option_set_arch".}
+
+proc vercmp*(a: cstring, b: cstring): cint
+ {.cdecl, importc: "alpm_pkg_vercmp".}
+
+proc errno*(handle: ptr AlpmHandle): cint
+ {.cdecl, importc: "alpm_errno".}
+
+proc errorAlpm*(errno: cint): cstring
+ {.cdecl, importc: "alpm_strerror".}
+
+proc register*(handle: ptr AlpmHandle, treeName: cstring, level: cint): ptr AlpmDatabase
+ {.cdecl, importc: "alpm_register_syncdb".}
+
+proc local*(handle: ptr AlpmHandle): ptr AlpmDatabase
+ {.cdecl, importc: "alpm_get_localdb".}
+
+proc packages*(db: ptr AlpmDatabase): ptr AlpmList[ptr AlpmPackage]
+ {.cdecl, importc: "alpm_db_get_pkgcache".}
+
+proc groups*(db: ptr AlpmDatabase): ptr AlpmList[ptr AlpmGroup]
+ {.cdecl, importc: "alpm_db_get_groupcache".}
+
+proc name*(db: ptr AlpmDatabase): cstring
+ {.cdecl, importc: "alpm_db_get_name".}
+
+proc `[]`*(db: ptr AlpmDatabase, name: cstring): ptr AlpmPackage
+ {.cdecl, importc: "alpm_db_get_pkg".}
+
+proc base*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_base".}
+
+proc name*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_name".}
+
+proc version*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_version".}
+
+proc arch*(pkg: ptr AlpmPackage): cstring
+ {.cdecl, importc: "alpm_pkg_get_arch".}
+
+proc groups*(pkg: ptr AlpmPackage): ptr AlpmList[cstring]
+ {.cdecl, importc: "alpm_pkg_get_groups".}
+
+proc reason*(pkg: ptr AlpmPackage): AlpmReason
+ {.cdecl, importc: "alpm_pkg_get_reason".}
+
+proc depends*(pkg: ptr AlpmPackage): ptr AlpmList[ptr AlpmDependency]
+ {.cdecl, importc: "alpm_pkg_get_depends".}
+
+proc optional*(pkg: ptr AlpmPackage): ptr AlpmList[ptr AlpmDependency]
+ {.cdecl, importc: "alpm_pkg_get_optdepends".}
+
+proc provides*(pkg: ptr AlpmPackage): ptr AlpmList[ptr AlpmDependency]
+ {.cdecl, importc: "alpm_pkg_get_provides".}
+
+proc cfree*(data: pointer)
+ {.cdecl, importc: "free", header: "<stdlib.h>".}
+
+proc freeList*[T](list: ptr AlpmList[T])
+ {.cdecl, importc: "alpm_list_free".}
+
+proc freeListInner*[T](list: ptr AlpmList[T], fn: proc (data: pointer): void {.cdecl.})
+ {.cdecl, importc: "alpm_list_free_inner".}
+
+proc freeListFull*[T](list: ptr AlpmList[T]) =
+ list.freeListInner(cfree)
+ list.freeList()
+
+template withAlpm*(root: string, db: string, dbs: seq[string], arch: string,
+ handle: untyped, alpmDbs: untyped, errors: untyped, body: untyped): untyped =
+ block:
+ var errno: cint = 0
+ let handle = newAlpmHandle(root, db, errno)
+
+ if handle == nil:
+ raise commandError(trp("failed to initialize alpm library\n(%s: %s)\n").strip
+ .replace("%s", "$#") % [$errno.errorAlpm, db])
+
+ var alpmDbs = newSeq[ptr AlpmDatabase]()
+ var errors = newSeq[string]()
+ for dbName in dbs:
+ let alpmDb = handle.register(dbName, 1 shl 30)
+ if alpmDb != nil:
+ alpmDbs &= alpmDb
+ else:
+ errors &= trp("could not register '%s' database (%s)\n").strip
+ .replace("%s", "$#") % [dbName, $handle.errno.errorAlpm]
+
+ try:
+ discard handle.setArch(arch)
+ body
+ finally:
+ discard handle.release()
+
+iterator items*[T](list: ptr AlpmList[T]): T =
+ var listi = list
+ while listi != nil:
+ yield listi.data
+ listi = listi.next
diff --git a/src/wrapper/curl.nim b/src/wrapper/curl.nim
new file mode 100644
index 0000000..5f3e814
--- /dev/null
+++ b/src/wrapper/curl.nim
@@ -0,0 +1,123 @@
+import
+ strutils,
+ "../utils"
+
+type
+ CurlHandle* = object
+
+ CurlInstance* = object
+ handle: ptr CurlHandle
+ data: seq[char]
+
+ CurlOption {.pure, size: sizeof(cint).} = enum
+ followLocation = 52,
+ noSignal = 99,
+ timeoutMs = 155,
+ connectTimeoutMs = 156,
+ writeData = 10001,
+ url = 10002,
+ writeFunction = 20011
+
+ CurlError* = object of Exception
+
+{.passL: "-lcurl".}
+
+proc initCurlGlobal*(flags: clong): cint
+ {.cdecl, importc: "curl_global_init".}
+
+proc cleanupCurlGlobal*: void
+ {.cdecl, importc: "curl_global_cleanup".}
+
+proc newCurlHandle*: ptr CurlHandle
+ {.cdecl, importc: "curl_easy_init".}
+
+proc cleanup*(handle: ptr CurlHandle)
+ {.cdecl, importc: "curl_easy_cleanup".}
+
+proc errorCurl*(error: cint): cstring
+ {.cdecl, importc: "curl_easy_strerror".}
+
+proc setOption*(handle: ptr CurlHandle, option: CurlOption): cint
+ {.cdecl, importc: "curl_easy_setopt", varargs.}
+
+proc perform*(handle: ptr CurlHandle): cint
+ {.cdecl, importc: "curl_easy_perform".}
+
+proc escape*(handle: ptr CurlHandle, input: cstring, length: cint): cstring
+ {.cdecl, importc: "curl_easy_escape".}
+
+proc freeCurl*(data: pointer)
+ {.cdecl, importc: "curl_free".}
+
+proc escape*(instance: ref CurlInstance, s: string): string =
+ let esc = instance.handle.escape(s, 0)
+ if esc != nil:
+ let nesc = $esc
+ freeCurl(esc)
+ nesc
+ else:
+ ""
+
+proc curlWriteMemory(mem: array[csize.high, char], size: csize, nmemb: csize,
+ userdata: ref CurlInstance): csize {.cdecl.} =
+ let total = size * nmemb
+ if total > 0:
+ userData.data &= mem[0 .. total - 1]
+ total
+
+var refCount = 0
+
+template withCurlGlobal*(body: untyped): untyped =
+ block:
+ if refCount == 0:
+ if initCurlGlobal(0) != 0:
+ raise commandError(tr"failed to initialize curl library")
+ refCount += 1
+ try:
+ body
+ finally:
+ refCount -= 1
+ if refCount == 0:
+ cleanupCurlGlobal()
+
+template withCurl*(instance: untyped, body: untyped): untyped =
+ block:
+ let handle = newCurlHandle()
+ if handle == nil:
+ raise commandError(tr"failed to initialize curl handle")
+
+ var instance: ref CurlInstance
+ new(instance)
+ instance.handle = handle
+ instance.data = newSeq[char]()
+
+ proc raiseError(code: cint) =
+ if code != 0:
+ let msg = code.errorCurl
+ if msg != nil:
+ raise newException(CurlError, tr"failed to perform request" & (": $#" % [$msg]))
+ else:
+ raise newException(CurlError, tr"failed to perform request")
+
+ proc performInternal(url: string): seq[char] =
+ raiseError(handle.setOption(CurlOption.followLocation, (clong) 1))
+ raiseError(handle.setOption(CurlOption.noSignal, (clong) 1))
+ raiseError(handle.setOption(CurlOption.timeoutMs, (clong) 15000))
+ raiseError(handle.setOption(CurlOption.connectTimeoutMs, (clong) 15000))
+ raiseError(handle.setOption(CurlOption.url, url))
+ raiseError(handle.setOption(CurlOption.writeFunction, cast[pointer](curlWriteMemory)))
+ raiseError(handle.setOption(CurlOption.writeData, instance))
+ raiseError(handle.perform())
+ instance.data
+
+ proc performString(url: string): string =
+ let data = performInternal(url)
+ var str = newStringOfCap(data.len)
+ for c in data:
+ str.add(c)
+ str
+
+ try:
+ body
+ finally:
+ handle.cleanup()