pax_global_header00006660000000000000000000000064140244147260014516gustar00rootroot0000000000000052 comment=ff697fae4259e7b0f4bef27e3817f848bc06233a click-0.5.0/000077500000000000000000000000001402441472600126055ustar00rootroot00000000000000click-0.5.0/.coveragerc000066400000000000000000000000761402441472600147310ustar00rootroot00000000000000[run] omit = /usr/lib/*, /usr/local/*, click/tests/*, tests/* click-0.5.0/.gitignore000066400000000000000000000030071402441472600145750ustar00rootroot00000000000000__pycache__ ./.coverage ./aclocal.m4 ./autom4te.cache ./click.egg-info ./config.h ./config.h.in ./config.log ./config.status ./configure ./coverage.info ./coverage*.xml ./coveragereport ./libtool ./setup.py ./stamp-h1 .deps .libs *.la *.lo ABOUT-NLS Makefile Makefile.in _build/doctrees _build/html doc/_build/doctrees doc/_build/html doc/_build/man build *.egg-info dist .tox click_package/paths.py click_package/tests/config.py click_package/tests/preload.gir click_package/tests/test_paths.py build-aux/compile build-aux/config.guess build-aux/config.rpath build-aux/config.sub build-aux/depcomp build-aux/install-sh build-aux/ltmain.sh build-aux/missing conf/databases/99_default.conf debhelper/*.1 debian/*.debhelper* debian/*.substvars debian/autoreconf.* debian/click-dev debian/click debian/click-doc debian/files debian/gir1.2-click-0.4 debian/libclick-0.4-0 debian/libclick-0.4-dev debian/packagekit-plugin-click debian/python3-click-package debian/tmp init/systemd/click-system-hooks.service init/systemd/click-user-hooks.service lib/click/*.c lib/click/*.gir lib/click/*.typelib lib/click/click.h lib/click/click-0.4.pc lib/click/click-0.4.vapi lib/click/libclick_0_4_la_vala.stamp lib/click/paths.vala lib/click/valac-wrapper m4/intltool.m4 m4/libtool.m4 m4/ltoptions.m4 m4/ltsugar.m4 m4/ltversion.m4 m4/lt~obsolete.m4 pk-plugin/com.ubuntu.click.policy po/.intltool-merge-cache po/ChangeLog po/Makefile.in.in po/Makevars.template po/POTFILES po/Rules-quot po/remove-potcdate.sed po/stamp-it po/*.gmo po/*.header po/*.sed po/*.sin click-0.5.0/Jenkinsfile000066400000000000000000000001011402441472600147610ustar00rootroot00000000000000@Library('ubports-build-tools') _ buildAndProvideDebianPackage() click-0.5.0/LICENSE000066400000000000000000001045151402441472600136200ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . click-0.5.0/MANIFEST.in000066400000000000000000000000331402441472600143370ustar00rootroot00000000000000recursive-include debian * click-0.5.0/Makefile.am000066400000000000000000000016431402441472600146450ustar00rootroot00000000000000SUBDIRS = lib preload click_package conf debhelper init schroot ACLOCAL_AMFLAGS = -I m4 all-local: set -e; for python in $(PYTHON_INTERPRETERS); do \ $$python setup.py build; \ done check-local: set -e; for python in $(PYTHON_INTERPRETERS); do \ $$python setup.py test; \ done coverage-python.xml: $(MAKE) $(AM_MAKEFLAGS) check # setuptools likes to leave some debris around, which confuses things. install-exec-hook: find build -name \*.pyc -print0 | xargs -0r rm -f find build -name __pycache__ -print0 | xargs -0r rm -rf find build -name \*.egg-info -print0 | xargs -0r rm -rf set -e; for python in $(PYTHON_INTERPRETERS); do \ $$python setup.py install $(PYTHON_INSTALL_FLAGS); \ done clean-local: rm -rf build *.egg-info .tox find -name \*.pyc -print0 | xargs -0r rm -f find -name __pycache__ -print0 | xargs -0r rm -rf rm -f .coverage coverage-python.xml include $(top_srcdir)/Makefile.am.coverage click-0.5.0/Makefile.am.coverage000066400000000000000000000030761402441472600164410ustar00rootroot00000000000000 # Coverage targets .PHONY: clean-gcno clean-gcda \ coverage-html generate-coverage-html clean-coverage-html \ coverage-gcovr generate-coverage-gcovr clean-coverage-gcovr \ clean-merged-coverage clean-local: clean-gcno clean-coverage-html clean-coverage-gcovr clean-merged-coverage if HAVE_GCOV clean-gcno: @echo Removing old coverage instrumentation -find -name '*.gcno' -print | xargs -r rm clean-gcda: @echo Removing old coverage results -find -name '*.gcda' -print | xargs -r rm coverage-html: coverage-python.xml $(MAKE) $(AM_MAKEFLAGS) generate-coverage-html generate-coverage-html: @echo Collecting coverage data $(LCOV) --directory $(top_builddir) --capture --output-file coverage.info --no-checksum --compat-libtool LANG=C $(GENHTML) --prefix $(top_builddir) --output-directory coveragereport --title "Code Coverage" --legend --show-details coverage.info clean-coverage-html: clean-gcda -$(LCOV) --directory $(top_builddir) -z rm -rf coverage.info coveragereport if HAVE_GCOVR coverage-gcovr: coverage-python.xml $(MAKE) $(AM_MAKEFLAGS) generate-coverage-gcovr generate-coverage-gcovr: @echo Generating coverage GCOVR report $(GCOVR) -x -r $(top_builddir) -o coverage-c.xml sed -i 's/\( # 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; version 3 of the License. # # 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 . """Operations on Click packages.""" from __future__ import print_function from optparse import OptionParser import os import signal import sys from textwrap import dedent # Support running from the build tree. sys.path.insert(0, os.path.join(sys.path[0], os.pardir)) import gi gi.require_version('Click', '0.4') # There is an unfortunate name clash with # https://pypi.python.org/pypi/click; try to detect this and take evasive # action. import click_package as click if not getattr(click, "_CLICK_IS_A_PACKAGING_FORMAT_", None): import site wrong_click_mods = [ mod for mod in sys.modules if mod.split(".")[0] == "click"] for mod in wrong_click_mods: del sys.modules[mod] try: user_site_index = sys.path.index(site.getusersitepackages()) except ValueError: print( "Cannot start click due to a conflict with a different " "locally-installed Python 'click' package. Remove it using " "Python packaging tools and try again.", file=sys.stderr) sys.exit(1) del sys.path[user_site_index] from click_package import commands def fix_stdout(): if sys.version >= "3": # Force encoding to UTF-8 even in non-UTF-8 locales. import io sys.stdout = io.TextIOWrapper( sys.stdout.detach(), encoding="UTF-8", line_buffering=True) else: # Avoid having to do .encode("UTF-8") everywhere. import codecs sys.stdout = codecs.EncodedFile(sys.stdout, "UTF-8") def null_decode(input, errors="strict"): return input, len(input) sys.stdout.decode = null_decode def main(): fix_stdout() # Python's default handling of SIGPIPE is not helpful to us. signal.signal(signal.SIGPIPE, signal.SIG_DFL) parser = OptionParser(dedent("""\ %%prog COMMAND [options] Commands are as follows ('%%prog COMMAND --help' for more): %s""") % commands.help_text()) parser.disable_interspersed_args() _, args = parser.parse_args() if not args: parser.print_help() return 0 command = args[0] args = args[1:] if command == "help": if args and args[0] in commands.all_commands: mod = commands.load_command(args[0]) mod.run(["--help"]) else: parser.print_help() return 0 if command not in commands.all_commands: parser.error("unknown command: %s" % command) mod = commands.load_command(command) return mod.run(args) if __name__ == "__main__": sys.exit(main()) click-0.5.0/click_package/000077500000000000000000000000001402441472600153455ustar00rootroot00000000000000click-0.5.0/click_package/Makefile.am000066400000000000000000000005221402441472600174000ustar00rootroot00000000000000SUBDIRS = tests noinst_SCRIPTS = paths.py CLEANFILES = $(noinst_SCRIPTS) do_subst = sed \ -e 's,[@]sysconfdir[@],$(sysconfdir),g' \ -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' \ -e 's,[@]pkglibdir[@],$(pkglibdir),g' \ -e 's,[@]DEFAULT_ROOT[@],$(DEFAULT_ROOT),g' paths.py: paths.py.in Makefile $(do_subst) < $(srcdir)/paths.py.in > $@ click-0.5.0/click_package/__init__.py000066400000000000000000000002211402441472600174510ustar00rootroot00000000000000# Marker to help resolve unfortunate name clash between this package and # https://pypi.python.org/pypi/click. _CLICK_IS_A_PACKAGING_FORMAT_ = 1 click-0.5.0/click_package/arfile.py000066400000000000000000000063461402441472600171720ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Basic support for writing ar archive files. We do things this way so that Click packages can be created with minimal dependencies (e.g. on non-Ubuntu systems). No read support is needed, since Click packages are always installed on systems that have dpkg. Some method names and general approach come from the tarfile module in Python's standard library; details of the format come from dpkg. """ from __future__ import print_function __metaclass__ = type __all__ = [ 'ArFile', ] import os import shutil import time class ArFile: def __init__(self, name=None, mode="w", fileobj=None): if mode != "w": raise ValueError("only mode 'w' is supported") self.mode = mode self.real_mode = "wb" if fileobj: if name is None and hasattr(fileobj, "name"): name = fileobj.name if hasattr(fileobj, "mode"): if fileobj.mode != "wb": raise ValueError("fileobj must be opened with mode='wb'") self._mode = fileobj.mode self.opened_fileobj = False else: fileobj = open(name, self.real_mode) self.opened_fileobj = True self.name = name self.fileobj = fileobj self.closed = False def close(self): if self.opened_fileobj: self.fileobj.close() self.closed = True def _check(self): if self.closed: raise IOError("ArFile %s is closed" % self.name) def __enter__(self): self._check() return self def __exit__(self, *args): self.close() def add_magic(self): self.fileobj.write(b"!\n") def add_header(self, name, size): if len(name) > 15: raise ValueError("ar member name '%s' length too long" % name) if size > 9999999999: raise ValueError("ar member size %d too large" % size) header = ("%-16s%-12u0 0 100644 %-10d`\n" % ( name, int(time.time()), size)).encode() assert len(header) == 60 # sizeof(struct ar_hdr) self.fileobj.write(header) def add_data(self, name, data): size = len(data) self.add_header(name, size) self.fileobj.write(data) if size & 1: self.fileobj.write(b"\n") # padding def add_file(self, name, path): with open(path, "rb") as fobj: size = os.fstat(fobj.fileno()).st_size self.add_header(name, size) shutil.copyfileobj(fobj, self.fileobj) if size & 1: self.fileobj.write(b"\n") # padding click-0.5.0/click_package/build.py000066400000000000000000000272161402441472600170260ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Building Click packages.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'ClickBuildError', 'ClickBuilder', 'ClickSourceBuilder', ] import contextlib import hashlib import io import json import os import re import shutil import subprocess import sys import tarfile import tempfile from textwrap import dedent try: import apt_pkg apt_pkg.init_system() except ImportError: # "click build" is required to work with only the Python standard library. pass from click_package import osextras from click_package.arfile import ArFile from click_package.preinst import static_preinst from click_package.versions import spec_version from click_package.framework import ( validate_framework, ClickFrameworkInvalid, ) @contextlib.contextmanager def make_temp_dir(): temp_dir = tempfile.mkdtemp(prefix="click") try: os.chmod(temp_dir, 0o755) yield temp_dir finally: shutil.rmtree(temp_dir) class FakerootTarFile(tarfile.TarFile): """A version of TarFile which pretends all files are owned by root:root.""" def gettarinfo(self, *args, **kwargs): tarinfo = super(FakerootTarFile, self).gettarinfo(*args, **kwargs) tarinfo.uid = tarinfo.gid = 0 tarinfo.uname = tarinfo.gname = "root" return tarinfo class ClickBuildError(Exception): pass class ClickBuilderBase: def __init__(self): self.file_map = {} # From @Dpkg::Source::Package::tar_ignore_default_pattern. # (more in ClickSourceBuilder) self._ignore_patterns = [ "*.click", ".*.sw?", "*~", ",,*", ".[#~]*", ".arch-ids", ".arch-inventory", ".bzr", ".bzr-builddeb", ".bzr.backup", ".bzr.tags", ".bzrignore", ".cvsignore", ".git", ".gitattributes", ".gitignore", ".gitmodules", ".hg", ".hgignore", ".hgsigs", ".hgtags", ".shelf", ".svn", "CVS", "DEADJOE", "RCS", "_MTN", "_darcs", "{arch}", ] def add_ignore_pattern(self, pattern): self._ignore_patterns.append(pattern) def add_file(self, source_path, dest_path): self.file_map[source_path] = dest_path def read_manifest(self, manifest_path): with io.open(manifest_path, encoding="UTF-8") as manifest: try: self.manifest = json.load(manifest) except Exception as e: raise ClickBuildError( "Error reading manifest from %s: %s" % (manifest_path, e)) keys = sorted(self.manifest) for key in keys: if key.startswith("_"): print( "Ignoring reserved dynamic key '%s'." % key, file=sys.stderr) del self.manifest[key] @property def name(self): return self.manifest["name"] @property def version(self): return self.manifest["version"] @property def epochless_version(self): return re.sub(r"^\d+:", "", self.version) @property def maintainer(self): return self.manifest["maintainer"] @property def title(self): return self.manifest["title"] @property def architecture(self): manifest_arch = self.manifest.get("architecture", "all") if isinstance(manifest_arch, list): return "multi" else: return manifest_arch class ClickBuilder(ClickBuilderBase): def list_files(self, root_path): for dirpath, _, filenames in os.walk(root_path): rel_dirpath = os.path.relpath(dirpath, root_path) if rel_dirpath == ".": rel_dirpath = "" for filename in filenames: yield os.path.join(rel_dirpath, filename) def _filter_dot_click(self, tarinfo): """Filter out attempts to include .click at the top level.""" if tarinfo.name == './.click' or tarinfo.name.startswith('./.click/'): return None return tarinfo def _pack(self, temp_dir, control_dir, data_dir, package_path): data_tar_path = os.path.join(temp_dir, "data.tar.gz") with contextlib.closing(FakerootTarFile.open( name=data_tar_path, mode="w:gz", format=tarfile.GNU_FORMAT )) as data_tar: data_tar.add(data_dir, arcname="./", filter=self._filter_dot_click) control_tar_path = os.path.join(temp_dir, "control.tar.gz") control_tar = tarfile.open( name=control_tar_path, mode="w:gz", format=tarfile.GNU_FORMAT) control_tar.add(control_dir, arcname="./") control_tar.close() with ArFile(name=package_path, mode="w") as package: package.add_magic() package.add_data("debian-binary", b"2.0\n") package.add_data( "_click-binary", ("%s\n" % spec_version).encode("UTF-8")) package.add_file("control.tar.gz", control_tar_path) package.add_file("data.tar.gz", data_tar_path) def _validate_framework(self, framework_string): """Apply policy checks to framework declarations.""" try: validate_framework( framework_string, ignore_missing_frameworks=True) except ClickFrameworkInvalid as e: raise ClickBuildError(str(e)) def build(self, dest_dir, manifest_path="manifest.json"): with make_temp_dir() as temp_dir: # Prepare data area. root_path = os.path.join(temp_dir, "data") for source_path, dest_path in self.file_map.items(): if dest_path.startswith("/"): dest_path = dest_path[1:] real_dest_path = os.path.join(root_path, dest_path) shutil.copytree( source_path, real_dest_path, symlinks=True, ignore=shutil.ignore_patterns(*self._ignore_patterns)) # Prepare control area. control_dir = os.path.join(temp_dir, "DEBIAN") osextras.ensuredir(control_dir) if os.path.isabs(manifest_path): full_manifest_path = manifest_path else: full_manifest_path = os.path.join(root_path, manifest_path) self.read_manifest(full_manifest_path) if "framework" in self.manifest: self._validate_framework(self.manifest["framework"]) du_output = subprocess.check_output( ["du", "-k", "-s", "--apparent-size", "."], cwd=temp_dir, universal_newlines=True).rstrip("\n") match = re.match(r"^(\d+)\s+\.$", du_output) if not match: raise Exception("du gave unexpected output '%s'" % du_output) installed_size = match.group(1) self.manifest["installed-size"] = installed_size control_path = os.path.join(control_dir, "control") osextras.ensuredir(os.path.dirname(control_path)) with io.open(control_path, "w", encoding="UTF-8") as control: print(dedent("""\ Package: %s Version: %s Click-Version: %s Architecture: %s Maintainer: %s Installed-Size: %s Description: %s""" % ( self.name, self.version, spec_version, self.architecture, self.maintainer, installed_size, self.title)), file=control) # Control file names must not contain a dot, hence "manifest" # rather than "manifest.json" in the control area. real_manifest_path = os.path.join(control_dir, "manifest") with io.open( real_manifest_path, "w", encoding="UTF-8") as manifest: print( json.dumps( self.manifest, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": ")), file=manifest) os.unlink(full_manifest_path) os.chmod(real_manifest_path, 0o644) md5sums_path = os.path.join(control_dir, "md5sums") with open(md5sums_path, "w") as md5sums: for path in sorted(self.list_files(root_path)): md5 = hashlib.md5() p = os.path.join(root_path, path) if not os.path.exists(p): continue with open(p, "rb") as f: while True: buf = f.read(16384) if not buf: break md5.update(buf) print("%s %s" % (md5.hexdigest(), path), file=md5sums) preinst_path = os.path.join(control_dir, "preinst") with open(preinst_path, "w") as preinst: preinst.write(static_preinst) # Pack everything up. package_name = "%s_%s_%s.click" % ( self.name, self.epochless_version, self.architecture) package_path = os.path.join(dest_dir, package_name) self._pack(temp_dir, control_dir, root_path, package_path) return package_path class ClickSourceBuilder(ClickBuilderBase): def __init__(self): super(ClickSourceBuilder, self).__init__() # From @Dpkg::Source::Package::tar_ignore_default_pattern. # (more in ClickBuilderBase) self._ignore_patterns += [ "*.a", ".be", ".deps", "*.la", "*.o", "*.so", ] def build(self, dest_dir, manifest_path=None): with make_temp_dir() as temp_dir: root_path = os.path.join(temp_dir, "source") for source_path, dest_path in self.file_map.items(): if dest_path.startswith("/"): dest_path = dest_path[1:] real_dest_path = os.path.join(root_path, dest_path) shutil.copytree( source_path, real_dest_path, symlinks=True, ignore=shutil.ignore_patterns(*self._ignore_patterns)) real_manifest_path = os.path.join(root_path, "manifest.json") if manifest_path is not None: shutil.copy2(manifest_path, real_manifest_path) os.chmod(real_manifest_path, 0o644) self.read_manifest(real_manifest_path) package_name = "%s_%s.tar.gz" % (self.name, self.epochless_version) package_path = os.path.join(dest_dir, package_name) with contextlib.closing(FakerootTarFile.open( name=package_path, mode="w:gz", format=tarfile.GNU_FORMAT )) as tar: tar.add(root_path, arcname="./") return package_path click-0.5.0/click_package/chroot.py000066400000000000000000000633251402441472600172260ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Authors: Colin Watson , # Brian Murray # Michael Vogt # # 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; version 3 of the License. # # 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 . """Chroot management for building Click packages.""" from __future__ import print_function __metaclass__ = type __all__ = [ "ClickChroot", "ClickChrootException", "ClickChrootAlreadyExistsException", "ClickChrootDoesNotExistException", ] try: from urllib.error import URLError from urllib.request import urlopen except ImportError: from urllib2 import URLError, urlopen import os import pwd import fileinput import re import shutil import stat import subprocess import sys from textwrap import dedent from xml.etree import ElementTree framework_base = { "ubuntu-sdk-13.10": "ubuntu-sdk-13.10", # 14.04 "ubuntu-sdk-14.04-html": "ubuntu-sdk-14.04", "ubuntu-sdk-14.04-papi": "ubuntu-sdk-14.04", "ubuntu-sdk-14.04-qml": "ubuntu-sdk-14.04", # 14.10 "ubuntu-sdk-14.10-html": "ubuntu-sdk-14.10", "ubuntu-sdk-14.10-papi": "ubuntu-sdk-14.10", "ubuntu-sdk-14.10-qml": "ubuntu-sdk-14.10", # 15.04 "ubuntu-sdk-15.04-html": "ubuntu-sdk-15.04", "ubuntu-sdk-15.04-papi": "ubuntu-sdk-15.04", "ubuntu-sdk-15.04-qml": "ubuntu-sdk-15.04", # 15.10 "ubuntu-sdk-15.10-html-dev1": "ubuntu-sdk-15.10-dev1", "ubuntu-sdk-15.10-papi-dev1": "ubuntu-sdk-15.10-dev1", "ubuntu-sdk-15.10-qml-dev1": "ubuntu-sdk-15.10-dev1", } framework_series = { "ubuntu-sdk-13.10": "saucy", "ubuntu-sdk-14.04": "trusty", "ubuntu-sdk-14.10": "utopic", "ubuntu-sdk-15.04": "vivid", "ubuntu-sdk-15.10": "wily", } # Please keep the lists of package names sorted. extra_packages = { "ubuntu-sdk-13.10": [ "libqt5opengl5-dev:{TARGET}", "libqt5svg5-dev:{TARGET}", "libqt5v8-5-dev:{TARGET}", "libqt5webkit5-dev:{TARGET}", "libqt5xmlpatterns5-dev:{TARGET}", "qmlscene:{TARGET}", "qt3d5-dev:{TARGET}", "qt5-default:{TARGET}", "qt5-qmake:{TARGET}", "qtbase5-dev:{TARGET}", "qtdeclarative5-dev:{TARGET}", "qtmultimedia5-dev:{TARGET}", "qtquick1-5-dev:{TARGET}", "qtscript5-dev:{TARGET}", "qtsensors5-dev:{TARGET}", "qttools5-dev:{TARGET}", "ubuntu-ui-toolkit-doc", ], "ubuntu-sdk-14.04": [ "cmake", "google-mock:{TARGET}", "intltool", "libboost1.54-dev:{TARGET}", "libjsoncpp-dev:{TARGET}", "libprocess-cpp-dev:{TARGET}", "libproperties-cpp-dev:{TARGET}", "libqt5svg5-dev:{TARGET}", "libqt5webkit5-dev:{TARGET}", "libqt5xmlpatterns5-dev:{TARGET}", "libunity-scopes-dev:{TARGET}", # bug #1316930, needed for autopilot "python3", "qmlscene:{TARGET}", "qt3d5-dev:{TARGET}", "qt5-default:{TARGET}", "qtbase5-dev:{TARGET}", "qtdeclarative5-dev:{TARGET}", "qtdeclarative5-dev-tools", "qtlocation5-dev:{TARGET}", "qtmultimedia5-dev:{TARGET}", "qtscript5-dev:{TARGET}", "qtsensors5-dev:{TARGET}", "qttools5-dev:{TARGET}", "qttools5-dev-tools:{TARGET}", "ubuntu-ui-toolkit-doc", ], "ubuntu-sdk-14.10": [ "cmake", "cmake-extras", "google-mock:{TARGET}", "intltool", "libboost1.55-dev:{TARGET}", "libcontent-hub-dev:{TARGET}", "libjsoncpp-dev:{TARGET}", "libnet-cpp-dev:{TARGET}", "libprocess-cpp-dev:{TARGET}", "libproperties-cpp-dev:{TARGET}", "libqt5keychain0:{TARGET}", "libqt5sensors5-dev:{TARGET}", "libqt5svg5-dev:{TARGET}", "libqt5webkit5-dev:{TARGET}", "libqt5xmlpatterns5-dev:{TARGET}", "libunity-scopes-dev:{TARGET}", # bug #1316930, needed for autopilot "python3", "qml-module-qt-labs-settings:{TARGET}", "qml-module-qtmultimedia:{TARGET}", "qml-module-qtquick-layouts:{TARGET}", "qml-module-qtsensors:{TARGET}", "qml-module-qtwebkit:{TARGET}", "qmlscene:{TARGET}", "qt3d5-dev:{TARGET}", "qt5-default:{TARGET}", "qtdeclarative5-accounts-plugin:{TARGET}", "qtdeclarative5-dev-tools", "qtdeclarative5-folderlistmodel-plugin:{TARGET}", "qtdeclarative5-localstorage-plugin:{TARGET}", "qtdeclarative5-online-accounts-client0.1:{TARGET}", "qtdeclarative5-particles-plugin:{TARGET}", "qtdeclarative5-poppler1.0:{TARGET}", "qtdeclarative5-qtlocation-plugin:{TARGET}", "qtdeclarative5-qtorganizer-plugin:{TARGET}", "qtdeclarative5-qtpositioning-plugin:{TARGET}", "qtdeclarative5-u1db1.0:{TARGET}", "qtdeclarative5-ubuntu-content0.1:{TARGET}", "qtdeclarative5-ubuntu-download-manager0.1:{TARGET}", "qtdeclarative5-ubuntu-mediascanner0.1:{TARGET}", "qtdeclarative5-ubuntu-syncmonitor0.1:{TARGET}", "qtdeclarative5-ubuntu-telephony-phonenumber0.1:{TARGET}", "qtdeclarative5-ubuntu-ui-toolkit-plugin:{TARGET}", "qtdeclarative5-usermetrics0.1:{TARGET}", "qtdeclarative5-xmllistmodel-plugin:{TARGET}", "qtlocation5-dev:{TARGET}", "qtmultimedia5-dev:{TARGET}", "qtscript5-dev:{TARGET}", "qttools5-dev:{TARGET}", "qttools5-dev-tools:{TARGET}", "ubuntu-html5-theme:{TARGET}", "ubuntu-ui-toolkit-doc", ], "ubuntu-sdk-15.04": [ # the sdk libs "ubuntu-sdk-libs:{TARGET}", "ubuntu-sdk-libs-dev:{TARGET}", # the native build tools "ubuntu-sdk-libs-tools", # FIXME: see # http://pad.lv/~mvo/oxide/crossbuild-friendly/+merge/234093 # we help the apt resolver here until the # oxideqt-codecs/oxidec-codecs-extras is sorted "oxideqt-codecs-extra", ], "ubuntu-sdk-15.10-dev1": [ # the sdk libs "ubuntu-sdk-libs:{TARGET}", "ubuntu-sdk-libs-dev:{TARGET}", # the native build tools "ubuntu-sdk-libs-tools", # FIXME: see # http://pad.lv/~mvo/oxide/crossbuild-friendly/+merge/234093 # we help the apt resolver here until the # oxideqt-codecs/oxidec-codecs-extras is sorted "oxideqt-codecs-extra", ], } primary_arches = ["amd64", "i386"] non_meta_re = re.compile(r'^[a-zA-Z0-9+,./:=@_-]+$') GEOIP_SERVER = "http://geoip.ubuntu.com/lookup" overlay_ppa = "ci-train-ppa-service/stable-phone-overlay" def get_geoip_country_code_prefix(): click_no_local_mirror = os.environ.get('CLICK_NO_LOCAL_MIRROR', 'auto') if click_no_local_mirror == '1': return "" try: with urlopen(GEOIP_SERVER) as f: xml_data = f.read() et = ElementTree.fromstring(xml_data) cc = et.find("CountryCode") if not cc: return "" return cc.text.lower()+"." except (ElementTree.ParseError, URLError): pass return "" def generate_sources(series, native_arch, target_arch, archive_mirror, ports_mirror, components): """Generate a list of strings for apts sources.list. Arguments: series -- the distro series (e.g. vivid) native_arch -- the native architecture (e.g. amd64) target_arch -- the target architecture (e.g. armhf) archive_mirror -- main mirror, e.g. http://archive.ubuntu.com/ubuntu ports_mirror -- ports mirror, e.g. http://ports.ubuntu.com/ubuntu-ports components -- the components as string, e.g. "main restricted universe" """ pockets = ['%s' % series] for pocket in ['updates', 'security']: pockets.append('%s-%s' % (series, pocket)) sources = [] # write binary lines arches = [target_arch] if native_arch != target_arch: arches.append(native_arch) for arch in arches: if arch not in primary_arches: mirror = ports_mirror else: mirror = archive_mirror for pocket in pockets: sources.append("deb [arch=%s] %s %s %s" % (arch, mirror, pocket, components)) # write source lines for pocket in pockets: sources.append("deb-src %s %s %s" % (archive_mirror, pocket, components)) return sources def shell_escape(command): escaped = [] for arg in command: if non_meta_re.match(arg): escaped.append(arg) else: escaped.append("'%s'" % arg.replace("'", "'\\''")) return " ".join(escaped) def strip_dev_series_from_framework(framework): """Remove trailing -dev[0-9]+ from a framework name""" return re.sub(r'^(.*)-dev[0-9]+$', r'\1', framework) class ClickChrootException(Exception): """A generic issue with the chroot""" pass class ClickChrootAlreadyExistsException(ClickChrootException): """The chroot already exists""" pass class ClickChrootDoesNotExistException(ClickChrootException): """A chroot with that name does not exist yet""" pass class ClickChroot: DAEMON_POLICY = dedent("""\ #!/bin/sh while true; do case "$1" in -*) shift ;; makedev) exit 0;; x11-common) exit 0;; *) exit 101;; esac done """) def __init__(self, target_arch, framework, name=None, series=None, session=None, chroots_dir=None): self.target_arch = target_arch self.framework = strip_dev_series_from_framework(framework) if name is None: name = "click" self.name = name if series is None: series = framework_series[self.framework_base] self.series = series self.session = session system_arch = subprocess.check_output( ["dpkg", "--print-architecture"], universal_newlines=True).strip() self.native_arch = self._get_native_arch(system_arch, self.target_arch) if chroots_dir is None: chroots_dir = "/var/lib/schroot/chroots" self.chroots_dir = chroots_dir if "SUDO_USER" in os.environ: self.user = os.environ["SUDO_USER"] elif "PKEXEC_UID" in os.environ: self.user = pwd.getpwuid(int(os.environ["PKEXEC_UID"])).pw_name else: self.user = pwd.getpwuid(os.getuid()).pw_name self.dpkg_architecture = self._dpkg_architecture() def _get_native_arch(self, system_arch, target_arch): """Determine the proper native architecture for a chroot. Some combinations of system and target architecture do not require cross-building, so in these cases we just create a chroot suitable for native building. """ if (system_arch, target_arch) in ( ("amd64", "i386"), # This will only work if the system is running a 64-bit # kernel; but there's no alternative since no i386-to-amd64 # cross-compiler is available in the Ubuntu archive. ("i386", "amd64"), ): return target_arch else: return system_arch def _dpkg_architecture(self): dpkg_architecture = {} command = ["dpkg-architecture", "-a%s" % self.target_arch] env = dict(os.environ) env["CC"] = "true" # Force dpkg-architecture to recalculate everything rather than # picking up values from the environment, which will be present when # running the test suite under dpkg-buildpackage. for key in list(env): if key.startswith("DEB_BUILD_") or key.startswith("DEB_HOST_"): del env[key] lines = subprocess.check_output( command, env=env, universal_newlines=True).splitlines() for line in lines: try: key, value = line.split("=", 1) except ValueError: continue dpkg_architecture[key] = value if self.native_arch == self.target_arch: # We may have overridden the native architecture (see # _get_native_arch above), so we need to force DEB_BUILD_* to # match. for key in list(dpkg_architecture): if key.startswith("DEB_HOST_"): new_key = "DEB_BUILD_" + key[len("DEB_HOST_"):] dpkg_architecture[new_key] = dpkg_architecture[key] return dpkg_architecture def _get_overlayfs_name(self): for line in fileinput.input("/proc/filesystems"): if line.strip() == "nodev\toverlay": fileinput.close() return "overlay" return "overlayfs" def _generate_chroot_config(self, mount): admin_user = "root" users = [] for key in ("users", "root-users", "source-root-users"): users.append("%s=%s,%s" % (key, admin_user, self.user)) with open(self.chroot_config, "w") as target: target.write(dedent("""\ [{full_name}] description=Build chroot for click packages on {target_arch} {users} type=directory profile=default setup.fstab=click/fstab # Not protocols or services see # debian bug 557730 setup.nssdatabases=sbuild/nssdatabases union-type={overlayfs_name} directory={mount} """).format(full_name=self.full_name, target_arch=self.target_arch, users="\n".join(users), mount=mount, overlayfs_name=self._get_overlayfs_name())) def _generate_daemon_policy(self, mount): daemon_policy = "%s/usr/sbin/policy-rc.d" % mount with open(daemon_policy, "w") as policy: policy.write(self.DAEMON_POLICY) return daemon_policy def _generate_apt_proxy_file(self, mount, proxy): apt_conf_d = os.path.join(mount, "etc", "apt", "apt.conf.d") if not os.path.exists(apt_conf_d): os.makedirs(apt_conf_d) apt_conf_f = os.path.join(apt_conf_d, "99-click-chroot-proxy") if proxy: with open(apt_conf_f, "w") as f: f.write(dedent("""\ // proxy settings copied by click chroot Acquire { HTTP { Proxy "%s"; }; }; """) % proxy) return apt_conf_f def _generate_finish_script(self, mount, build_pkgs): finish_script = "%s/finish.sh" % mount with open(finish_script, 'w') as finish: finish.write(dedent("""\ #!/bin/bash set -e # Configure target arch dpkg --add-architecture {target_arch} # Reload package lists apt-get update || true # Pull down signature requirements apt-get -y --force-yes install gnupg ubuntu-keyring """).format(target_arch=self.target_arch)) if self.series == "vivid": finish.write(dedent("""\ apt-get -y --force-yes install software-properties-common add-apt-repository -y ppa:{ppa} echo "Package: *" \ > /etc/apt/preferences.d/stable-phone-overlay.pref echo \ "Pin: release o=LP-PPA-{pin_ppa}" \ >> /etc/apt/preferences.d/stable-phone-overlay.pref echo "Pin-Priority: 1001" \ >> /etc/apt/preferences.d/stable-phone-overlay.pref """).format(ppa=overlay_ppa, pin_ppa=re.sub('/', '-', overlay_ppa))) finish.write(dedent("""\ # Reload package lists apt-get update || true # Disable debconf questions # so that automated builds won't prompt echo set debconf/frontend Noninteractive | debconf-communicate echo set debconf/priority critical | debconf-communicate apt-get -y --force-yes dist-upgrade # Install basic build tool set to match buildd apt-get -y --force-yes install {build_pkgs} # Set up expected /dev entries if [ ! -r /dev/stdin ]; then ln -s /proc/self/fd/0 /dev/stdin fi if [ ! -r /dev/stdout ]; then ln -s /proc/self/fd/1 /dev/stdout fi if [ ! -r /dev/stderr ]; then ln -s /proc/self/fd/2 /dev/stderr fi # Clean up rm /finish.sh apt-get clean """).format(build_pkgs=' '.join(build_pkgs))) return finish_script def _debootstrap(self, components, mount, archive_mirror, ports_mirror): if self.native_arch in primary_arches: mirror = archive_mirror else: mirror = ports_mirror subprocess.check_call([ "debootstrap", "--arch", self.native_arch, "--variant=buildd", "--components=%s" % ','.join(components), self.series, mount, mirror, ]) @property def framework_base(self): if self.framework in framework_base: return framework_base[self.framework] else: return self.framework @property def full_name(self): return "%s-%s-%s" % (self.name, self.framework_base, self.target_arch) @property def full_session_name(self): return "%s-%s" % (self.full_name, self.session) @property def chroot_config(self): return "/etc/schroot/chroot.d/%s" % self.full_name def exists(self): command = ["schroot", "-c", self.full_name, "-i"] with open("/dev/null", "w") as devnull: return subprocess.call( command, stdout=devnull, stderr=devnull) == 0 def _make_executable(self, path): mode = stat.S_IMODE(os.stat(path).st_mode) os.chmod(path, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) def _make_cross_package(self, prefix): if self.native_arch == self.target_arch: return prefix else: target_tuple = self.dpkg_architecture["DEB_HOST_GNU_TYPE"] return "%s-%s" % (prefix, target_tuple) def create(self, keep_broken_chroot_on_fail=False): if self.exists(): raise ClickChrootAlreadyExistsException( "Chroot %s already exists" % self.full_name) components = ["main", "restricted", "universe", "multiverse"] mount = "%s/%s" % (self.chroots_dir, self.full_name) proxy = None if not proxy and "http_proxy" in os.environ: proxy = os.environ["http_proxy"] if not proxy: proxy = subprocess.check_output( 'unset x; eval "$(apt-config shell x Acquire::HTTP::Proxy)"; \ echo "$x"', shell=True, universal_newlines=True).strip() build_pkgs = [ # sort alphabetically "apt-utils", "build-essential", "cmake", "dpkg-cross", "fakeroot", "libc-dev:%s" % self.target_arch, # build pkg names dynamically self._make_cross_package("g++"), self._make_cross_package("pkg-config"), ] for package in extra_packages.get(self.framework_base, []): package = package.format(TARGET=self.target_arch) build_pkgs.append(package) os.makedirs(mount) country_code = get_geoip_country_code_prefix() archive_mirror = "http://%sarchive.ubuntu.com/ubuntu" % country_code ports_mirror = "http://%sports.ubuntu.com/ubuntu-ports" % country_code # this doesn't work because we are running this under sudo if 'DEBOOTSTRAP_MIRROR' in os.environ: archive_mirror = os.environ['DEBOOTSTRAP_MIRROR'] self._debootstrap(components, mount, archive_mirror, ports_mirror) sources = generate_sources(self.series, self.native_arch, self.target_arch, archive_mirror, ports_mirror, ' '.join(components)) with open("%s/etc/apt/sources.list" % mount, "w") as sources_list: for line in sources: print(line, file=sources_list) shutil.copy2("/etc/localtime", "%s/etc/" % mount) shutil.copy2("/etc/timezone", "%s/etc/" % mount) self._generate_chroot_config(mount) daemon_policy = self._generate_daemon_policy(mount) self._make_executable(daemon_policy) initctl = "%s/sbin/initctl" % mount if os.path.exists(initctl): os.remove(initctl) os.symlink("%s/bin/true" % mount, initctl) self._generate_apt_proxy_file(mount, proxy) finish_script = self._generate_finish_script(mount, build_pkgs) self._make_executable(finish_script) command = ["/finish.sh"] ret_code = self.maint(*command) if ret_code != 0 and not keep_broken_chroot_on_fail: # cleanup on failure self.destroy() raise ClickChrootException( "Failed to create chroot '{}' (exit status {})".format( self.full_name, ret_code)) return ret_code def run(self, *args): if not self.exists(): raise ClickChrootDoesNotExistException( "Chroot %s does not exist" % self.full_name) command = ["schroot", "-c"] if self.session: command.extend([self.full_session_name, "--run-session"]) else: command.append(self.full_name) command.extend(["--", "env"]) for key, value in self.dpkg_architecture.items(): command.append("%s=%s" % (key, value)) command.extend(args) ret = subprocess.call(command) if ret == 0: return 0 else: print("Command returned %d: %s" % (ret, shell_escape(command)), file=sys.stderr) return ret def maint(self, *args): command = ["schroot", "-u", "root", "-c"] if self.session: command.extend([self.full_session_name, "--run-session"]) else: command.append("source:%s" % self.full_name) command.append("--") command.extend(args) ret = subprocess.call(command) if ret == 0: return 0 else: print("Command returned %d: %s" % (ret, shell_escape(command)), file=sys.stderr) return ret def install(self, *pkgs): if not self.exists(): raise ClickChrootDoesNotExistException( "Chroot %s does not exist" % self.full_name) ret = self.update() if ret != 0: return ret command = ["apt-get", "install", "--yes"] command.extend(pkgs) ret = self.maint(*command) if ret != 0: return ret return self.clean() def clean(self): command = ["apt-get", "clean"] return self.maint(*command) def update(self): command = ["apt-get", "update", "--yes"] return self.maint(*command) def upgrade(self): if not self.exists(): raise ClickChrootDoesNotExistException( "Chroot %s does not exist" % self.full_name) ret = self.update() if ret != 0: return ret command = ["apt-get", "dist-upgrade", "--yes"] ret = self.maint(*command) if ret != 0: return ret return self.clean() def destroy(self): # remove config if os.path.exists(self.chroot_config): os.remove(self.chroot_config) # find all schroot mount points, this is actually quite complicated mount_dir = os.path.abspath( os.path.join(self.chroots_dir, "..", "mount")) needle = os.path.join(mount_dir, self.full_name) all_mounts = [] with open("/proc/mounts") as f: for line in f.readlines(): mp = line.split()[1] if mp.startswith(needle): all_mounts.append(mp) # reverse order is important in case of submounts for mp in sorted(all_mounts, key=len, reverse=True): subprocess.call(["umount", mp]) # now remove the rest chroot_dir = "%s/%s" % (self.chroots_dir, self.full_name) if os.path.exists(chroot_dir): shutil.rmtree(chroot_dir) return 0 def begin_session(self): if not self.exists(): raise ClickChrootDoesNotExistException( "Chroot %s does not exist" % self.full_name) command = ["schroot", "-c", self.full_name, "--begin-session", "--session-name", self.full_session_name] subprocess.check_call(command) return 0 def end_session(self): if not self.exists(): raise ClickChrootDoesNotExistException( "Chroot %s does not exist" % self.full_name) command = ["schroot", "-c", self.full_session_name, "--end-session"] subprocess.check_call(command) return 0 click-0.5.0/click_package/commands/000077500000000000000000000000001402441472600171465ustar00rootroot00000000000000click-0.5.0/click_package/commands/__init__.py000066400000000000000000000025221402441472600212600ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """click commands.""" import importlib all_commands = ( "build", "buildsource", "chroot", "contents", "desktophook", "framework", "hook", "info", "install", "list", "pkgdir", "register", "unregister", "verify", ) hidden_commands = ( "desktophook", ) def load_command(command): return importlib.import_module("click_package.commands.%s" % command) def help_text(): lines = [] for command in all_commands: if command in hidden_commands: continue mod = load_command(command) lines.append(" %-21s %s" % (command, mod.__doc__.splitlines()[0])) return "\n".join(lines) click-0.5.0/click_package/commands/build.py000066400000000000000000000060051402441472600206200ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Build a Click package.""" from __future__ import print_function from optparse import OptionParser import os import sys import subprocess from gi.repository import Click from click_package.build import ClickBuildError, ClickBuilder def run(argv): parser = OptionParser("%prog build [options] DIRECTORY") parser.add_option( "-m", "--manifest", metavar="PATH", default="manifest.json", help="read package manifest from PATH (default: manifest.json)") parser.add_option( "--no-validate", action="store_false", default=True, dest="validate", help="Don't run click-reviewers-tools check on resulting .click") parser.add_option( "-I", "--ignore", metavar="file-pattern", action='append', default=[], help="Ignore the given pattern when building the package") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need directory") directory = args[0] if not os.path.isdir(directory): parser.error('directory "%s" does not exist' % directory) if os.path.isdir(os.path.join(directory, options.manifest)): options.manifest = os.path.join(options.manifest, "manifest.json") if not os.path.exists(os.path.join(directory, options.manifest)): parser.error( 'directory "%s" does not contain manifest file "%s"' % (directory, options.manifest)) builder = ClickBuilder() builder.add_file(directory, "./") for ignore in options.ignore: builder.add_ignore_pattern(ignore) try: path = builder.build(".", manifest_path=options.manifest) except ClickBuildError as e: print(e, file=sys.stderr) return 1 if options.validate and Click.find_on_path('click-review'): print("Now executing: click-review %s" % path) try: subprocess.check_call(['click-review', path]) except subprocess.CalledProcessError: # qtcreator-plugin-ubuntu relies on return code 0 # to establish if a .click package has been built # at all. # # If we want to distinguish between # - click build failed # - click build succeeded, but validation failed # both tools will have to learn this at the same # time. pass print("Successfully built package in '%s'." % path) return 0 click-0.5.0/click_package/commands/buildsource.py000066400000000000000000000043751402441472600220510ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Build a Click source package.""" from __future__ import print_function from optparse import OptionParser import os import sys from click_package.build import ClickBuildError, ClickSourceBuilder def run(argv): parser = OptionParser("%prog buildsource [options] DIRECTORY") parser.add_option( "-m", "--manifest", metavar="PATH", help="read package manifest from PATH") parser.add_option( "-I", "--ignore", metavar="file-pattern", action='append', default=[], help="Ignore the given pattern when building the package") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need directory") directory = args[0] if not os.path.isdir(directory): parser.error('directory "%s" does not exist' % directory) if not options.manifest: options.manifest = os.path.join(directory, "manifest.json") if os.path.isdir(os.path.join(directory, options.manifest)): options.manifest = os.path.join(options.manifest, "manifest.json") if not os.path.exists(os.path.join(directory, options.manifest)): parser.error( 'directory "%s" does not contain manifest file "%s"' % (directory, options.manifest)) builder = ClickSourceBuilder() builder.add_file(directory, "./") for ignore in options.ignore: builder.add_ignore_pattern(ignore) try: path = builder.build(".", manifest_path=options.manifest) except ClickBuildError as e: print(e, file=sys.stderr) return 1 print("Successfully built source package in '%s'." % path) return 0 click-0.5.0/click_package/commands/chroot.py000066400000000000000000000215671402441472600210310ustar00rootroot00000000000000#! /usr/bin/python3 # Copyright (C) 2013 Canonical Ltd. # Author: Brian Murray # 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; version 3 of the License. # # 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 . """Use and manage a Click chroot.""" from __future__ import print_function from argparse import ArgumentParser, REMAINDER from contextlib import contextmanager import os from click_package.chroot import ( ClickChroot, ClickChrootAlreadyExistsException, ClickChrootDoesNotExistException, ) from click_package import osextras def requires_root(parser): if os.getuid() != 0: parser.error("must be run as root; try sudo") @contextmanager def message_on_error(exc, msg): """ Context Manager that prints the error message 'msg' on exception 'exc' """ try: yield except exc: print(msg) # FIXME: i18n(?) class ErrorMessages: EXISTS = """A chroot for that name and architecture already exists. Please see the man-page how to use it.""" NOT_EXISTS = """A chroot for that name and architecture does not exist. Please use 'create' to create it.""" def create(parser, args): if not osextras.find_on_path("debootstrap"): parser.error( "debootstrap not installed and configured; install click-dev and " "debootstrap") requires_root(parser) chroot = ClickChroot( args.architecture, args.framework, name=args.name, series=args.series) with message_on_error( ClickChrootAlreadyExistsException, ErrorMessages.EXISTS): return chroot.create(args.keep_broken_chroot) # if we reach this point there was a error so return exit_status 1 return 1 def install(parser, args): packages = args.packages chroot = ClickChroot( args.architecture, args.framework, name=args.name, session=args.session) with message_on_error( ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): return chroot.install(*packages) # if we reach this point there was a error so return exit_status 1 return 1 def destroy(parser, args): requires_root(parser) # ask for confirmation? chroot = ClickChroot(args.architecture, args.framework, name=args.name) with message_on_error( ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): return chroot.destroy() # if we reach this point there was a error so return exit_status 1 return 1 def execute(parser, args): program = args.program if not program: program = ["/bin/bash"] chroot = ClickChroot( args.architecture, args.framework, name=args.name, session=args.session) with message_on_error( ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): return chroot.run(*program) # if we reach this point there was a error so return exit_status 1 return 1 def maint(parser, args): program = args.program if not program: program = ["/bin/bash"] chroot = ClickChroot( args.architecture, args.framework, name=args.name, session=args.session) with message_on_error( ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): return chroot.maint(*program) # if we reach this point there was a error so return exit_status 1 return 1 def upgrade(parser, args): chroot = ClickChroot( args.architecture, args.framework, name=args.name, session=args.session) with message_on_error( ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): return chroot.upgrade() # if we reach this point there was a error so return exit_status 1 return 1 def begin_session(parser, args): chroot = ClickChroot( args.architecture, args.framework, name=args.name, session=args.session) with message_on_error( ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): return chroot.begin_session() # if we reach this point there was a error so return exit_status 1 return 1 def end_session(parser, args): chroot = ClickChroot( args.architecture, args.framework, name=args.name, session=args.session) with message_on_error( ClickChrootDoesNotExistException, ErrorMessages.NOT_EXISTS): return chroot.end_session() # if we reach this point there was a error so return exit_status 1 return 1 def exists(parser, args): chroot = ClickChroot(args.architecture, args.framework, name=args.name) # return shell exit codes 0 on success, 1 on failure if chroot.exists(): return 0 else: return 1 def run(argv): parser = ArgumentParser("click chroot") subparsers = parser.add_subparsers( description="management subcommands", help="valid commands") parser.add_argument( "-a", "--architecture", required=True, help="architecture for the chroot") parser.add_argument( "-f", "--framework", default="ubuntu-sdk-14.04", help="framework for the chroot (default: ubuntu-sdk-14.04)") parser.add_argument( "-s", "--series", help="series to use for a newly-created chroot (defaults to a series " "appropriate for the framework)") parser.add_argument( "-n", "--name", default="click", help=( "name of the chroot (default: click; the framework and " "architecture will be appended)")) create_parser = subparsers.add_parser( "create", help="create a chroot of the provided architecture") create_parser.add_argument( "-k", "--keep-broken-chroot", default=False, action="store_true", help="Keep the chroot even if creating it fails (default is to delete " "it)") create_parser.set_defaults(func=create) destroy_parser = subparsers.add_parser( "destroy", help="destroy the chroot") destroy_parser.set_defaults(func=destroy) upgrade_parser = subparsers.add_parser( "upgrade", help="upgrade the chroot") upgrade_parser.add_argument( "-n", "--session-name", dest='session', help="persistent chroot session name to upgrade") upgrade_parser.set_defaults(func=upgrade) install_parser = subparsers.add_parser( "install", help="install packages in the chroot") install_parser.add_argument( "-n", "--session-name", dest='session', help="persistent chroot session name to install packages in") install_parser.add_argument( "packages", nargs="+", help="packages to install") install_parser.set_defaults(func=install) execute_parser = subparsers.add_parser( "run", help="run a program in the chroot") execute_parser.add_argument( "-n", "--session-name", dest='session', help="persistent chroot session name to run a program in") execute_parser.add_argument( "program", nargs=REMAINDER, help="program to run with arguments") execute_parser.set_defaults(func=execute) maint_parser = subparsers.add_parser( "maint", help="run a maintenance command in the chroot") maint_parser.add_argument( "-n", "--session-name", dest='session', help="persistent chroot session name to run a maintenance command in") maint_parser.add_argument( "program", nargs=REMAINDER, help="program to run with arguments") maint_parser.set_defaults(func=maint) begin_parser = subparsers.add_parser( "begin-session", help="begin a persistent chroot session") begin_parser.add_argument( "session", help="new session name") begin_parser.set_defaults(func=begin_session) end_parser = subparsers.add_parser( "end-session", help="end a persistent chroot session") end_parser.add_argument( "session", help="session name to end") end_parser.set_defaults(func=end_session) exists_parser = subparsers.add_parser( "exists", help="test if the given chroot exists") exists_parser.set_defaults(func=exists) args = parser.parse_args(argv) if not hasattr(args, "func"): parser.print_help() return 1 if (not osextras.find_on_path("schroot") or not os.path.exists("/etc/schroot/click/fstab")): parser.error( "schroot not installed and configured; install click-dev and " "schroot") return args.func(parser, args) click-0.5.0/click_package/commands/contents.py000066400000000000000000000020671402441472600213620ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Show the file-list contents of a Click package file.""" from __future__ import print_function from optparse import OptionParser import subprocess def run(argv): parser = OptionParser("%prog contents [options] PATH") _, args = parser.parse_args(argv) if len(args) < 1: parser.error("need file name") path = args[0] subprocess.check_call(["dpkg-deb", "-c", path]) return 0 click-0.5.0/click_package/commands/desktophook.py000066400000000000000000000136351402441472600220620ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Click desktop hook. (Temporary; do not rely on this.)""" from __future__ import print_function import errno import io import json from optparse import OptionParser import os from gi.repository import Click from click_package import osextras COMMENT = \ '# Generated by "click desktophook"; changes here will be overwritten.' def desktop_entries(directory, only_ours=False): for entry in osextras.listdir_force(directory): if not entry.endswith(".desktop"): continue path = os.path.join(directory, entry) if only_ours: try: with io.open(path, encoding="UTF-8") as f: if COMMENT not in f.read(): continue except Exception: continue yield entry def split_entry(entry): entry = entry[:-8] # strip .desktop return entry.split("_", 2) def older(source_path, target_path): """Return True iff source_path is older than target_path. It's also OK for target_path to be missing. """ try: source_mtime = os.stat(source_path).st_mtime except OSError as e: if e.errno == errno.ENOENT: return False try: target_mtime = os.stat(target_path).st_mtime except OSError as e: if e.errno == errno.ENOENT: return True return source_mtime < target_mtime def read_hooks_for(path, package, app_name): try: directory = Click.find_package_directory(path) manifest_path = os.path.join( directory, ".click", "info", "%s.manifest" % package) with io.open(manifest_path, encoding="UTF-8") as manifest: return json.load(manifest).get("hooks", {}).get(app_name, {}) except Exception: return {} def quote_for_desktop_exec(s): """Quote a string for Exec in a .desktop file. The rules are fairly awful. See: http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html """ for c in s: if c in " \t\n\"'\\><~|&;$*?#()`%": break else: return s quoted = [] for c in s: if c in "\"`$\\": quoted.append("\\" + c) elif c == "%": quoted.append("%%") else: quoted.append(c) escaped = [] for c in "".join(quoted): if c == "\\": escaped.append("\\\\") else: escaped.append(c) return '"%s"' % "".join(escaped) # TODO: This is a very crude .desktop file mangler; we should instead # implement proper (de)serialisation. def write_desktop_file(target_path, source_path, profile): Click.ensuredir(os.path.dirname(target_path)) with io.open(source_path, encoding="UTF-8") as source, \ io.open(target_path, "w", encoding="UTF-8") as target: source_dir = Click.find_package_directory(source_path) written_comment = False seen_path = False for line in source: if not line.rstrip("\n") or line.startswith("#"): # Comment target.write(line) elif line.startswith("["): # Group header target.write(line) if not written_comment: print(COMMENT, file=target) elif "=" not in line: # Who knows? target.write(line) else: key, value = line.split("=", 1) key = key.strip() value = value.strip() if key == "Exec": target.write( "%s=aa-exec-click -p %s -- %s\n" % (key, quote_for_desktop_exec(profile), value)) elif key == "Path": target.write("%s=%s\n" % (key, source_dir)) seen_path = True elif key == "Icon": icon_path = os.path.join(source_dir, value) if os.path.exists(icon_path): target.write("%s=%s\n" % (key, icon_path)) else: target.write("%s=%s\n" % (key, value)) else: target.write("%s=%s\n" % (key, value)) if not seen_path: target.write("Path=%s\n" % source_dir) def run(argv): parser = OptionParser("%prog desktophook [options]") parser.parse_args(argv) source_dir = os.path.expanduser("~/.local/share/click/hooks/desktop") target_dir = os.path.expanduser("~/.local/share/applications") source_entries = set(desktop_entries(source_dir)) target_entries = set(desktop_entries(target_dir, only_ours=True)) for new_entry in source_entries: package, app_name, version = split_entry(new_entry) source_path = os.path.join(source_dir, new_entry) target_path = os.path.join(target_dir, new_entry) if older(source_path, target_path): hooks = read_hooks_for(source_path, package, app_name) if "apparmor" in hooks: profile = "%s_%s_%s" % (package, app_name, version) else: profile = "unconfined" write_desktop_file(target_path, source_path, profile) for remove_entry in target_entries - source_entries: os.unlink(os.path.join(target_dir, remove_entry)) return 0 click-0.5.0/click_package/commands/framework.py000066400000000000000000000044141402441472600215200ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """List available frameworks.""" from __future__ import print_function from argparse import ArgumentParser from gi.repository import Click def list(parser, args): for framework in Click.Framework.get_frameworks(): print("%s" % framework.props.name) return 0 def info(parser, args): framework = Click.Framework.open(args.framework_name) for field in sorted(framework.get_fields()): print("%s: %s" % (field, framework.get_field(field))) def get_field(parser, args): framework = Click.Framework.open(args.framework_name) print(framework.get_field(args.field_name)) def run(argv): parser = ArgumentParser("click framework") subparsers = parser.add_subparsers() list_parser = subparsers.add_parser( "list", help="list available frameworks") list_parser.set_defaults(func=list) info_parser = subparsers.add_parser( "info", help="show info about a specific framework") info_parser.add_argument( "framework_name", help="framework name with the information") info_parser.set_defaults(func=info) get_field_parser = subparsers.add_parser( "get-field", help="get a field from a given framework") get_field_parser.add_argument( "framework_name", help="framework name with the information") get_field_parser.add_argument( "field_name", help="the field name (e.g. base-version)") get_field_parser.set_defaults(func=get_field) args = parser.parse_args(argv) if not hasattr(args, "func"): parser.print_help() return 1 return args.func(parser, args) click-0.5.0/click_package/commands/hook.py000066400000000000000000000057241402441472600204700ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Install or remove a Click system hook.""" from __future__ import print_function from optparse import OptionParser import sys from textwrap import dedent from gi.repository import Click, GLib per_hook_subcommands = { "install": "install", "remove": "remove", } def run(argv): parser = OptionParser(dedent("""\ %prog hook [options] SUBCOMMAND [...] Subcommands are as follows: install HOOK remove HOOK run-system run-user [--user=USER]""")) parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help=( "run user-level hooks for USER (default: current user; only " "applicable to run-user)")) options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need subcommand (install, remove, run-system, run-user)") subcommand = args[0] if subcommand in per_hook_subcommands: if len(args) < 2: parser.error("need hook name") db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) name = args[1] hook = Click.Hook.open(db, name) getattr(hook, per_hook_subcommands[subcommand])(user_name=None) elif subcommand == "run-system": db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) try: Click.run_system_hooks(db) except GLib.GError as e: if e.domain == "click-hooks-error-quark": print(e.message, file=sys.stderr) return 1 else: raise elif subcommand == "run-user": db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) try: Click.run_user_hooks(db, user_name=options.user) except GLib.GError as e: if e.domain == "click-hooks-error-quark": print(e.message, file=sys.stderr) return 1 else: raise else: parser.error( "unknown subcommand '%s' (known: install, remove, run-system," "run-user)" % subcommand) return 0 click-0.5.0/click_package/commands/info.py000066400000000000000000000055521402441472600204620ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Show manifest information for a Click package.""" from __future__ import print_function from contextlib import closing import glob import json from optparse import OptionParser import os import sys from gi.repository import Click from click_package.install import DebFile from click_package.json_helpers import json_object_to_python def _load_manifest(manifest_file): manifest = json.load(manifest_file) keys = list(manifest) for key in keys: if key.startswith("_"): del manifest[key] return manifest def get_manifest(options, arg): if "/" not in arg: db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) registry = Click.User.for_user(db, name=options.user) if registry.has_package_name(arg): return json_object_to_python(registry.get_manifest(arg)) try: with closing(DebFile(filename=arg)) as package: with package.control.get_file( "manifest", encoding="UTF-8") as manifest_file: return _load_manifest(manifest_file) except Exception: pkgdir = Click.find_package_directory(arg) manifest_path = glob.glob( os.path.join(pkgdir, ".click", "info", "*.manifest")) if len(manifest_path) > 1: raise Exception("Multiple manifest files found in '%s'" % ( manifest_path)) with open(manifest_path[0]) as f: return _load_manifest(f) def run(argv): parser = OptionParser("%prog info [options] PATH") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="look up PACKAGE-NAME for USER (if you have permission; " "default: current user)") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need file name") try: manifest = get_manifest(options, args[0]) except Exception as e: print(e, file=sys.stderr) return 1 json.dump( manifest, sys.stdout, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": ")) print() return 0 click-0.5.0/click_package/commands/install.py000066400000000000000000000051601402441472600211700ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Install a Click package (low-level; consider pkcon instead).""" from __future__ import print_function from optparse import OptionParser import sys from textwrap import dedent from gi.repository import Click from click_package.install import ClickInstaller, ClickInstallerError def run(argv): parser = OptionParser(dedent("""\ %prog install [options] PACKAGE-FILE This is a low-level tool; to install a package as an ordinary user you should generally use "pkcon install-local PACKAGE-FILE" instead.""")) parser.add_option( "--root", metavar="PATH", help="install packages underneath PATH") parser.add_option( "--force-missing-framework", action="store_true", default=False, help="install despite missing system framework") parser.add_option( "--user", metavar="USER", help="register package for USER") parser.add_option( "--all-users", default=False, action="store_true", help="register package for all users") parser.add_option( "--allow-unauthenticated", default=False, action="store_true", help="allow installing packages with no signatures") parser.add_option( "--verbose", default=False, action="store_true", help="be more verbose on install") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package file name") db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package_path = args[0] installer = ClickInstaller( db=db, force_missing_framework=options.force_missing_framework, allow_unauthenticated=options.allow_unauthenticated) try: installer.install( package_path, user=options.user, all_users=options.all_users, quiet=not options.verbose) except ClickInstallerError as e: print("Cannot install %s: %s" % (package_path, e), file=sys.stderr) return 1 return 0 click-0.5.0/click_package/commands/list.py000066400000000000000000000042441402441472600204770ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """List installed Click packages.""" from __future__ import print_function import json from optparse import OptionParser import sys from gi.repository import Click from click_package.json_helpers import json_array_to_python def list_packages(options): db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) if options.all: return json_array_to_python(db.get_manifests(all_versions=True)) else: registry = Click.User.for_user(db, name=options.user) return json_array_to_python(registry.get_manifests()) def run(argv): parser = OptionParser("%prog list [options]") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--all", default=False, action="store_true", help="list all installed packages") parser.add_option( "--user", metavar="USER", help="list packages registered by USER (if you have permission)") parser.add_option( "--manifest", default=False, action="store_true", help="format output as a JSON array of manifests") options, _ = parser.parse_args(argv) json_output = list_packages(options) if options.manifest: json.dump( json_output, sys.stdout, ensure_ascii=False, sort_keys=True, indent=4, separators=(",", ": ")) print() else: for manifest in json_output: print("%s\t%s" % (manifest["name"], manifest["version"])) return 0 click-0.5.0/click_package/commands/pkgdir.py000066400000000000000000000033631402441472600210050ustar00rootroot00000000000000#! /usr/bin/python3 # Copyright (C) 2013 Canonical Ltd. # 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; version 3 of the License. # # 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 . """Print the directory where a Click package is unpacked.""" from __future__ import print_function from optparse import OptionParser import sys from gi.repository import Click def run(argv): parser = OptionParser("%prog pkgdir [options] {PACKAGE-NAME|PATH}") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="look up PACKAGE-NAME for USER (if you have permission; " "default: current user)") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package name") try: if "/" in args[0]: print(Click.find_package_directory(args[0])) else: db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package_name = args[0] registry = Click.User.for_user(db, name=options.user) print(registry.get_path(package_name)) except Exception as e: print(e, file=sys.stderr) return 1 return 0 click-0.5.0/click_package/commands/register.py000066400000000000000000000037101402441472600213450ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Register an installed Click package for a user.""" from __future__ import print_function from optparse import OptionParser from gi.repository import Click, GLib def run(argv): parser = OptionParser("%prog register [options] PACKAGE-NAME VERSION") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="register package for USER (default: current user)") parser.add_option( "--all-users", default=False, action="store_true", help="register package for all users") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package name") if len(args) < 2: parser.error("need version") db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package = args[0] version = args[1] if options.all_users: registry = Click.User.for_all_users(db) else: registry = Click.User.for_user(db, name=options.user) try: old_version = registry.get_version(package) except GLib.GError: old_version = None registry.set_version(package, version) if old_version is not None: db.maybe_remove(package, old_version) return 0 click-0.5.0/click_package/commands/unregister.py000066400000000000000000000045051402441472600217130ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unregister an installed Click package for a user.""" from __future__ import print_function from optparse import OptionParser import os import sys from gi.repository import Click def run(argv): parser = OptionParser("%prog unregister [options] PACKAGE-NAME [VERSION]") parser.add_option( "--root", metavar="PATH", help="look for additional packages in PATH") parser.add_option( "--user", metavar="USER", help="unregister package for USER (default: $SUDO_USER, if known)") parser.add_option( "--all-users", default=False, action="store_true", help="unregister package that was previously registered for all users") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package name") if os.geteuid() != 0: parser.error( "click unregister must be started as root, since it may need to " "remove packages from disk") if options.user is None and "SUDO_USER" in os.environ: options.user = os.environ["SUDO_USER"] db = Click.DB() db.read(db_dir=None) if options.root is not None: db.add(options.root) package = args[0] if options.all_users: registry = Click.User.for_all_users(db) else: registry = Click.User.for_user(db, name=options.user) old_version = registry.get_version(package) if len(args) >= 2 and old_version != args[1]: print( "Not removing %s %s; expected version %s" % (package, old_version, args[1]), file=sys.stderr) sys.exit(1) registry.remove(package) db.maybe_remove(package, old_version) # TODO: remove data return 0 click-0.5.0/click_package/commands/verify.py000066400000000000000000000030661402441472600210310ustar00rootroot00000000000000#! /usr/bin/python3 # Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Verify a Click package.""" from __future__ import print_function from optparse import OptionParser from click_package.install import ClickInstaller def run(argv): parser = OptionParser("%prog verify [options] PACKAGE-FILE") parser.add_option( "--force-missing-framework", action="store_true", default=False, help="ignore missing system framework") parser.add_option( "--allow-unauthenticated", action="store_true", default=False, help="allow installing packages with no sigantures") options, args = parser.parse_args(argv) if len(args) < 1: parser.error("need package file name") package_path = args[0] installer = ClickInstaller( db=None, force_missing_framework=options.force_missing_framework, allow_unauthenticated=options.allow_unauthenticated) installer.audit(package_path, slow=True) return 0 click-0.5.0/click_package/framework.py000066400000000000000000000124251402441472600177200ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Pure python click framework handling support.""" import logging import os import re try: import apt_pkg except: pass import click_package.paths class ClickFrameworkInvalid(Exception): pass # python version of the vala parse_deb822_file() def parse_deb822_file(filename): data = {} with open(filename) as f: for line in f: line = line.strip() # from deb822.vala field_re_posix = ( r'^([^:[:space:]]+)[[:space:]]*:[[:space:]]' '([^[:space:]].*?)[[:space:]]*$') # python does not do posix char classes field_re = field_re_posix.replace("[:space:]", "\s") blank_re_posix = r'^[[:space:]]*$' blank_re = blank_re_posix.replace("[:space:]", "\s") if re.match(blank_re, line): break match = re.match(field_re, line) if match and match.group(1) and match.group(2): data[match.group(1).lower()] = match.group(2) return data # python version of vala get_frameworks_dir def get_frameworks_dir(): return click_package.paths.frameworks_dir def get_framework_path(framework_name): framework_path = os.path.join( get_frameworks_dir(), framework_name+".framework") return framework_path # python version of the vala click_framework_get_base_version() def click_framework_get_base_version(framework_name): deb822 = parse_deb822_file(get_framework_path(framework_name)) return deb822.get("base-version", None) # python version of the vala click_framework_get_base_version() def click_framework_get_base_name(framework_name): deb822 = parse_deb822_file(get_framework_path(framework_name)) return deb822.get("base-name", None) # python version of the vala click_framework_has_framework def click_framework_has_framework(framework_name): return os.path.exists(get_framework_path(framework_name)) def validate_framework(framework_string, ignore_missing_frameworks=False): try: apt_pkg except NameError: logging.warning("No apt_pkg module, skipping validate_framework") return try: parsed_framework = apt_pkg.parse_depends(framework_string) except ValueError: raise ClickFrameworkInvalid( 'Could not parse framework "%s"' % framework_string) base_name_versions = {} missing_frameworks = [] for or_dep in parsed_framework: if len(or_dep) > 1: raise ClickFrameworkInvalid( 'Alternative dependencies in framework "%s" not yet ' 'allowed' % framework_string) if or_dep[0][1] or or_dep[0][2]: raise ClickFrameworkInvalid( 'Version relationship in framework "%s" not yet allowed' % framework_string) # now verify that different base versions are not mixed framework_name = or_dep[0][0] if not click_framework_has_framework(framework_name): missing_frameworks.append(framework_name) continue # ensure we do not use different base versions for the same base-name framework_base_name = click_framework_get_base_name( framework_name) framework_base_version = click_framework_get_base_version( framework_name) prev = base_name_versions.get(framework_base_name, None) if prev and prev != framework_base_version: raise ClickFrameworkInvalid( 'Multiple frameworks with different base versions are not ' 'allowed. Found: {} ({} != {})'.format( framework_base_name, framework_base_version, base_name_versions[framework_base_name])) base_name_versions[framework_base_name] = framework_base_version if not ignore_missing_frameworks: if len(missing_frameworks) > 1: raise ClickFrameworkInvalid( 'Frameworks %s not present on system (use ' '--force-missing-framework option to override)' % ", ".join('"%s"' % f for f in missing_frameworks)) elif missing_frameworks: raise ClickFrameworkInvalid( 'Framework "%s" not present on system (use ' '--force-missing-framework option to override)' % missing_frameworks[0]) else: if len(missing_frameworks) > 1: logging.warning("Ignoring missing frameworks %s" % ( ", ".join('"%s"' % f for f in missing_frameworks))) elif missing_frameworks: logging.warning('Ignoring missing framework "%s"' % ( missing_frameworks[0])) click-0.5.0/click_package/install.py000066400000000000000000000447501402441472600173770ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Installing Click packages.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'ClickInstaller', 'ClickInstallerAuditError', 'ClickInstallerError', 'ClickInstallerPermissionDenied', ] from functools import partial import grp import inspect import json import logging import os import pwd import shutil import stat import subprocess import sys import tempfile from textwrap import dedent from contextlib import closing from debian.debfile import DebFile as _DebFile from debian.debian_support import Version from gi.repository import Click from click_package.paths import preload_path from click_package.preinst import static_preinst_matches from click_package.versions import spec_version from click_package.framework import ( validate_framework, ClickFrameworkInvalid, ) try: _DebFile.close DebFile = _DebFile except AttributeError: # Yay! The Ubuntu 13.04 version of python-debian 0.1.21 # debian.debfile.DebFile has a .close() method but the PyPI version of # 0.1.21 does not. It's worse than that because DebFile.close() really # delegates to DebPart.close() and *that's* missing in the PyPI version. # To get that working, we have to reach inside the object and name mangle # the attribute. class DebFile(_DebFile): def close(self): self.control._DebPart__member.close() self.data._DebPart__member.close() class DebsigVerifyError(Exception): pass class DebsigVerify: """Tiny wrapper around the debsig-verify commandline""" # from debsig-verify-0.9/debsigs.h DS_SUCCESS = 0 DS_FAIL_NOSIGS = 10 DS_FAIL_UNKNOWN_ORIGIN = 11 DS_FAIL_NOPOLICIES = 12 DS_FAIL_BADSIG = 13 DS_FAIL_INTERNAL = 14 # should be a property, but python does not support support # class properties easily @classmethod def available(cls): return Click.find_on_path("debsig-verify") @classmethod def verify(cls, path, allow_unauthenticated): command = ["debsig-verify"] + [path] try: subprocess.check_output(command, universal_newlines=True) except subprocess.CalledProcessError as e: if (allow_unauthenticated and e.returncode in (DebsigVerify.DS_FAIL_NOSIGS, DebsigVerify.DS_FAIL_UNKNOWN_ORIGIN, DebsigVerify.DS_FAIL_NOPOLICIES)): logging.warning( "Signature check failed, but installing anyway " "as requested") else: raise DebsigVerifyError( "Signature verification error: %s" % e.output) return True class ClickInstallerError(Exception): pass class ClickInstallerPermissionDenied(ClickInstallerError): pass class ClickInstallerAuditError(ClickInstallerError): pass class ClickInstaller: def __init__(self, db, force_missing_framework=False, allow_unauthenticated=False): self.db = db self.force_missing_framework = force_missing_framework self.allow_unauthenticated = allow_unauthenticated def _preload_path(self): if "CLICK_PACKAGE_PRELOAD" in os.environ: return os.environ["CLICK_PACKAGE_PRELOAD"] my_path = inspect.getsourcefile(ClickInstaller) preload = os.path.join( os.path.dirname(my_path), os.pardir, "preload", ".libs", "libclickpreload.so") if os.path.exists(preload): return os.path.abspath(preload) return preload_path def _dpkg_architecture(self): return subprocess.check_output( ["dpkg", "--print-architecture"], universal_newlines=True).rstrip("\n") def extract(self, path, target): command = ["dpkg-deb", "-R", path, target] with open(path, "rb") as fd: env = dict(os.environ) preloads = [self._preload_path()] if "LD_PRELOAD" in env: preloads.append(env["LD_PRELOAD"]) env["LD_PRELOAD"] = " ".join(preloads) env["CLICK_BASE_DIR"] = target env["CLICK_PACKAGE_PATH"] = path env["CLICK_PACKAGE_FD"] = str(fd.fileno()) env.pop("HOME", None) kwargs = {} if sys.version >= "3.2": kwargs["pass_fds"] = (fd.fileno(),) subprocess.check_call(command, env=env, **kwargs) def audit(self, path, slow=False, check_arch=False): # always do the signature check first if DebsigVerify.available(): try: DebsigVerify.verify(path, self.allow_unauthenticated) except DebsigVerifyError as e: raise ClickInstallerAuditError(str(e)) else: logging.warning( "debsig-verify not available; cannot check signatures") # fail early if the file cannot be opened try: with closing(DebFile(filename=path)) as package: pass except Exception as e: raise ClickInstallerError("Failed to read %s: %s" % ( path, str(e))) # then perform the audit with closing(DebFile(filename=path)) as package: control_fields = package.control.debcontrol() try: click_version = Version(control_fields["Click-Version"]) except KeyError: raise ClickInstallerAuditError("No Click-Version field") if click_version > spec_version: raise ClickInstallerAuditError( "Click-Version: %s newer than maximum supported version " "%s" % (click_version, spec_version)) for field in ( "Pre-Depends", "Depends", "Recommends", "Suggests", "Enhances", "Conflicts", "Breaks", "Provides", ): if field in control_fields: raise ClickInstallerAuditError( "%s field is forbidden in Click packages" % field) scripts = package.control.scripts() if ("preinst" in scripts and static_preinst_matches(scripts["preinst"])): scripts.pop("preinst", None) if scripts: raise ClickInstallerAuditError( "Maintainer scripts are forbidden in Click packages " "(found: %s)" % " ".join(sorted(scripts))) if not package.control.has_file("manifest"): raise ClickInstallerAuditError("Package has no manifest") with package.control.get_file("manifest", encoding="UTF-8") as f: manifest = json.load(f) try: package_name = manifest["name"] except KeyError: raise ClickInstallerAuditError('No "name" entry in manifest') # TODO: perhaps just do full name validation? if "/" in package_name: raise ClickInstallerAuditError( 'Invalid character "/" in "name" entry: %s' % package_name) if "_" in package_name: raise ClickInstallerAuditError( 'Invalid character "_" in "name" entry: %s' % package_name) try: package_version = manifest["version"] except KeyError: raise ClickInstallerAuditError( 'No "version" entry in manifest') # TODO: perhaps just do full version validation? if "/" in package_version: raise ClickInstallerAuditError( 'Invalid character "/" in "version" entry: %s' % package_version) if "_" in package_version: raise ClickInstallerAuditError( 'Invalid character "_" in "version" entry: %s' % package_version) try: framework = manifest["framework"] except KeyError: raise ClickInstallerAuditError( 'No "framework" entry in manifest') try: validate_framework(framework, self.force_missing_framework) except ClickFrameworkInvalid as e: raise ClickInstallerAuditError(str(e)) if check_arch: architecture = manifest.get("architecture", "all") if architecture != "all": dpkg_architecture = self._dpkg_architecture() if isinstance(architecture, list): if dpkg_architecture not in architecture: raise ClickInstallerAuditError( 'Package architectures "%s" not compatible ' 'with system architecture "%s"' % (" ".join(architecture), dpkg_architecture)) elif architecture != dpkg_architecture: raise ClickInstallerAuditError( 'Package architecture "%s" not compatible ' 'with system architecture "%s"' % (architecture, dpkg_architecture)) # This isn't ideally quick, since it has to decompress the data # part of the package, but dpkg's path filtering code assumes # that all paths start with "./" so we must check it before # passing the package to dpkg. for data_name in package.data: if data_name != "." and not data_name.startswith("./"): raise ClickInstallerAuditError( 'File name "%s" in package does not start with "./"' % data_name) if slow: temp_dir = tempfile.mkdtemp(prefix="click") try: self.extract(path, temp_dir) command = [ "md5sum", "-c", "--quiet", os.path.join("DEBIAN", "md5sums"), ] subprocess.check_call(command, cwd=temp_dir) finally: shutil.rmtree(temp_dir) return package_name, package_version def _drop_privileges(self, username): if os.geteuid() != 0: return pw = pwd.getpwnam(username) os.setgroups( [g.gr_gid for g in grp.getgrall() if username in g.gr_mem]) # Portability note: this assumes that we have [gs]etres[gu]id, which # is true on Linux but not necessarily elsewhere. If you need to # support something else, there are reasonably standard alternatives # involving other similar calls; see e.g. gnulib/lib/idpriv-drop.c. os.setresgid(pw.pw_gid, pw.pw_gid, pw.pw_gid) os.setresuid(pw.pw_uid, pw.pw_uid, pw.pw_uid) assert os.getresuid() == (pw.pw_uid, pw.pw_uid, pw.pw_uid) assert os.getresgid() == (pw.pw_gid, pw.pw_gid, pw.pw_gid) os.umask(0o022) def _euid_access(self, username, path, mode): """Like os.access, but for the effective UID.""" # TODO: Dropping privileges and calling # os.access(effective_ids=True) ought to work, but for some reason # appears not to return False when it should. It seems that we need # a subprocess to check this reliably. At least we don't have to # exec anything. pid = os.fork() if pid == 0: # child self._drop_privileges(username) os._exit(0 if os.access(path, mode) else 1) else: # parent _, status = os.waitpid(pid, 0) return status == 0 def _check_write_permissions(self, path): while True: if os.path.exists(path): break path = os.path.dirname(path) if path == "/": break if not self._euid_access("clickpkg", path, os.W_OK): raise ClickInstallerPermissionDenied( 'Cannot acquire permission to write to %s; either run as root ' 'with --user, or use "pkcon install-local" instead' % path) def _install_preexec(self, inst_dir): self._drop_privileges("clickpkg") admin_dir = os.path.join(inst_dir, ".click") if not os.path.exists(admin_dir): os.makedirs(admin_dir) with open(os.path.join(admin_dir, "available"), "w"): pass with open(os.path.join(admin_dir, "status"), "w"): pass os.mkdir(os.path.join(admin_dir, "info")) os.mkdir(os.path.join(admin_dir, "updates")) os.mkdir(os.path.join(admin_dir, "triggers")) def _unpack(self, path, user=None, all_users=False, quiet=True): package_name, package_version = self.audit(path, check_arch=True) # Is this package already unpacked in an underlay (non-topmost) # database? if self.db.has_package_version(package_name, package_version): overlay = self.db.get(self.db.props.size - 1) if not overlay.has_package_version(package_name, package_version): return package_name, package_version, None package_dir = os.path.join(self.db.props.overlay, package_name) inst_dir = os.path.join(package_dir, package_version) assert ( os.path.dirname(os.path.dirname(inst_dir)) == self.db.props.overlay) self._check_write_permissions(self.db.props.overlay) root_click = os.path.join(self.db.props.overlay, ".click") if not os.path.exists(root_click): os.makedirs(root_click) if os.geteuid() == 0: pw = pwd.getpwnam("clickpkg") os.chown(root_click, pw.pw_uid, pw.pw_gid) # TODO: sandbox so that this can only write to the unpack directory command = [ "dpkg", # We normally run dpkg as non-root. "--force-not-root", # /sbin and /usr/sbin may not necessarily be on $PATH; we don't # use the tools dpkg gets from there. "--force-bad-path", # We check the package architecture ourselves in audit(). "--force-architecture", "--instdir", inst_dir, "--admindir", os.path.join(inst_dir, ".click"), "--path-exclude", "*/.click/*", "--log", os.path.join(root_click, "log"), "--no-triggers", "--install", path, ] with open(path, "rb") as fd: env = dict(os.environ) preloads = [self._preload_path()] if "LD_PRELOAD" in env: preloads.append(env["LD_PRELOAD"]) env["LD_PRELOAD"] = " ".join(preloads) env["CLICK_BASE_DIR"] = self.db.props.overlay env["CLICK_PACKAGE_PATH"] = path env["CLICK_PACKAGE_FD"] = str(fd.fileno()) env.pop("HOME", None) kwargs = {} if sys.version >= "3.2": kwargs["pass_fds"] = (fd.fileno(),) if quiet: fn = subprocess.check_output kwargs["stderr"] = subprocess.STDOUT else: fn = subprocess.check_call try: fn(command, preexec_fn=partial(self._install_preexec, inst_dir), env=env, universal_newlines=True, **kwargs) except subprocess.CalledProcessError as e: logging.error("%s failed with exit_code %s:\n%s" % ( command, e.returncode, e.output)) raise for dirpath, dirnames, filenames in os.walk(inst_dir): for entry in dirnames + filenames: entry_path = os.path.join(dirpath, entry) entry_mode = os.lstat(entry_path).st_mode new_entry_mode = entry_mode | stat.S_IRGRP | stat.S_IROTH if entry_mode & stat.S_IXUSR: new_entry_mode |= stat.S_IXGRP | stat.S_IXOTH if new_entry_mode != entry_mode: try: os.chmod(entry_path, new_entry_mode) except OSError: pass current_path = os.path.join(package_dir, "current") if os.path.islink(current_path): old_version = os.readlink(current_path) if "/" in old_version: old_version = None else: old_version = None Click.package_install_hooks( self.db, package_name, old_version, package_version, user_name=None) new_path = os.path.join(package_dir, "current.new") Click.symlink_force(package_version, new_path) if os.geteuid() == 0: # shutil.chown would be more convenient, but it doesn't support # follow_symlinks=False in Python 3.3. # http://bugs.python.org/issue18108 pw = pwd.getpwnam("clickpkg") os.chown(new_path, pw.pw_uid, pw.pw_gid, follow_symlinks=False) os.rename(new_path, current_path) return package_name, package_version, old_version def install(self, path, user=None, all_users=False, quiet=True): package_name, package_version, old_version = self._unpack( path, user=user, all_users=all_users, quiet=quiet) if user is not None or all_users: if all_users: registry = Click.User.for_all_users(self.db) else: registry = Click.User.for_user(self.db, name=user) registry.set_version(package_name, package_version) else: print(dedent("""\ %s %s has not been registered for any users. It may be garbage-collected the next time the system starts. To avoid this, use "click register". """) % (package_name, package_version)) if old_version is not None: self.db.maybe_remove(package_name, old_version) click-0.5.0/click_package/json_helpers.py000066400000000000000000000033671402441472600204230ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Helper functions to turn json-glib objects into Python objects.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'ClickJsonError', 'json_array_to_python', 'json_node_to_python', 'json_object_to_python', ] from gi.repository import Json class ClickJsonError(Exception): pass def json_array_to_python(array): return [json_node_to_python(element) for element in array.get_elements()] def json_object_to_python(obj): ret = {} for name in obj.get_members(): ret[name] = json_node_to_python(obj.get_member(name)) return ret def json_node_to_python(node): node_type = node.get_node_type() if node_type == Json.NodeType.ARRAY: return json_array_to_python(node.get_array()) elif node_type == Json.NodeType.OBJECT: return json_object_to_python(node.get_object()) elif node_type == Json.NodeType.NULL: return None elif node_type == Json.NodeType.VALUE: return node.get_value() else: raise ClickJsonError( "Unknown JSON node type \"%s\"" % node_type.value_nick) click-0.5.0/click_package/osextras.py000066400000000000000000000051341402441472600175720ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Extra OS-level utility functions. Usually we can instead use the functions exported from lib/click/osextras.vala via GObject Introspection. These pure-Python versions are preserved so that they can be used from code that needs to be maximally portable: for example, click.build is intended to be usable even on systems that lack GObject, as long as they have a reasonably recent version of Python. """ __all__ = [ 'ensuredir', 'find_on_path', 'unlink_force', ] import errno import os try: # Python 3.3 from shutil import which def find_on_path(command): # http://bugs.python.org/issue17012 path = os.environ.get('PATH', os.pathsep) return which(command, path=os.environ.get('PATH', path)) is not None except ImportError: # Python 2 def find_on_path(command): """Is command on the executable search path?""" if 'PATH' not in os.environ: return False path = os.environ['PATH'] for element in path.split(os.pathsep): if not element: continue filename = os.path.join(element, command) if os.path.isfile(filename) and os.access(filename, os.X_OK): return True return False def ensuredir(directory): if not os.path.isdir(directory): os.makedirs(directory) def listdir_force(directory): try: return os.listdir(directory) except OSError as e: if e.errno == errno.ENOENT: return [] raise def unlink_force(path): """Unlink path, without worrying about whether it exists.""" try: os.unlink(path) except OSError as e: if e.errno != errno.ENOENT: raise def symlink_force(source, link_name): """Create symlink link_name -> source, even if link_name exists.""" unlink_force(link_name) os.symlink(source, link_name) def get_umask(): mask = os.umask(0) os.umask(mask) return mask click-0.5.0/click_package/paths.py.in000066400000000000000000000014141402441472600174430ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Click paths.""" preload_path = "@pkglibdir@/libclickpreload.so" frameworks_dir = "@pkgdatadir@/frameworks" click-0.5.0/click_package/preinst.py000066400000000000000000000035621402441472600174110ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Preinst for Click packages. In general there is a rule that Click packages may not have maintainer scripts. However, there is one exception: a static preinst used to cause dpkg to fail if people attempt to install Click packages directly using dpkg rather than via "click install". This avoids accidents, since Click packages use a different root of their filesystem tarball. """ from __future__ import print_function __metaclass__ = type __all__ = [ 'static_preinst', 'static_preinst_matches', ] _older_static_preinst = """\ #! /bin/sh echo "Click packages may not be installed directly using dpkg." echo "Use click-install instead." exit 1 """ _old_static_preinst = """\ #! /bin/sh echo "Click packages may not be installed directly using dpkg." echo "Use 'click-package install' instead." exit 1 """ static_preinst = """\ #! /bin/sh echo "Click packages may not be installed directly using dpkg." echo "Use 'click install' instead." exit 1 """ def static_preinst_matches(preinst): for allow_preinst in ( _older_static_preinst, _old_static_preinst, static_preinst, ): if preinst == allow_preinst.encode(): return True return False click-0.5.0/click_package/tests/000077500000000000000000000000001402441472600165075ustar00rootroot00000000000000click-0.5.0/click_package/tests/Makefile.am000066400000000000000000000011751402441472600205470ustar00rootroot00000000000000noinst_DATA = preload.gir CLEANFILES = $(noinst_DATA) preload.gir: preload.h PKG_CONFIG_PATH=$(top_builddir)/lib/click g-ir-scanner \ -n preload --nsversion 0 -l c \ --pkg glib-2.0 --pkg gee-0.8 --pkg json-glib-1.0 \ --pkg click-0.4 \ -I$(top_builddir)/lib/click -L$(top_builddir)/lib/click \ --accept-unprefixed --warn-all \ --libtool "$(LIBTOOL)" \ $< --output $@ noinst_SCRIPTS = test_paths.py CLEANFILES += $(noinst_SCRIPTS) do_subst = sed \ -e 's,[@]sysconfdir[@],$(sysconfdir),g' \ -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' test_paths.py: test_paths.py.in Makefile $(do_subst) < $(srcdir)/test_paths.py.in > $@ click-0.5.0/click_package/tests/__init__.py000066400000000000000000000032111402441472600206150ustar00rootroot00000000000000from __future__ import print_function import os import sys from click_package.tests import config def _append_env_path(envname, value): if envname in os.environ: if value in os.environ[envname].split(":"): return False os.environ[envname] = "%s:%s" % (os.environ[envname], value) else: os.environ[envname] = value return True def get_executable(): """Get python executable (respecting if python-coverage was used)""" coverage_executable = sys.executable+"-coverage" if "coverage" in sys.modules and os.path.isfile(coverage_executable): return [coverage_executable, "run", "-p"] return [sys.executable] # Don't do any of this in interactive mode. if not hasattr(sys, "ps1"): _lib_click_dir = os.path.join(config.abs_top_builddir, "lib", "click") changed = False if _append_env_path( "LD_LIBRARY_PATH", os.path.join(_lib_click_dir, ".libs")): changed = True if _append_env_path("GI_TYPELIB_PATH", _lib_click_dir): changed = True if changed: coverage_executable = get_executable() # We have to re-exec ourselves to get the dynamic loader to pick up # the new value of LD_LIBRARY_PATH. if "-m unittest" in sys.argv[0]: # unittest does horrible things to sys.argv in the name of # "usefulness", making the re-exec more painful than it needs to # be. os.execvp( coverage_executable[0], coverage_executable + ["-m", "unittest"] + sys.argv[1:]) else: os.execvp(coverage_executable[0], coverage_executable + sys.argv) os._exit(1) click-0.5.0/click_package/tests/config.py.in000066400000000000000000000015351402441472600207370ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . abs_top_builddir = "@abs_top_builddir@" STAT_OFFSET_UID = @STAT_OFFSET_UID@ STAT_OFFSET_GID = @STAT_OFFSET_GID@ STAT64_OFFSET_UID = @STAT64_OFFSET_UID@ STAT64_OFFSET_GID = @STAT64_OFFSET_GID@ click-0.5.0/click_package/tests/gimock.py000066400000000000000000000552401402441472600203400ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Mock function support based on GObject Introspection. (Note to reviewers: I expect to rewrite this from scratch on my own time as a more generalised set of Python modules for unit testing of C code, although using similar core ideas. This is a first draft for the purpose of getting Click's test suite to work expediently, rather than an interface I'm prepared to commit to long-term.) Python is a versatile and concise language for writing tests, and GObject Introspection (GI) makes it straightforward (often trivial) to bind native code into Python. However, writing tests for native code quickly runs into the problem of how to build mock functions. You might reasonably have code that calls chown(), for instance, and want to test how it's called rather than worrying about setting up a fakeroot-type environment where chown() will work. The obvious solution is to use `LD_PRELOAD` wrappers, but there are various problems to overcome in practice: * You can only set up a new `LD_PRELOAD` by going through the run-time linker; you can't just set it for a single in-process test case. * Generating the preloaded wrapper involves a fair bit of boilerplate code. * Having to write per-test mock code in C is inconvenient, and makes it difficult to get information back out of the mock (such as "how often was this function called, and with what arguments?"). The first problem can be solved by a decorator that knows how to run individual tests in a subprocess. This is made somewhat more inconvenient by the fact that there is no way for a context manager's `__enter__` method to avoid executing the context-managed block other than by throwing an exception, which makes it hard to silently avoid executing the test case in the parent process, but we can work around this at the cost of an extra line of code per invocation. For the rest, a combination of GI itself and ctypes can help. We can use GI to keep track of argument and return types of the mocked C functions in a reasonably sane way, by parsing header files. We're operating in the other direction from how GI is normally used, so PyGObject can't deal with bridging the two calling conventions for us. ctypes can: but we still need to be careful! We have to construct the callback functions in the child process, ensure that we keep references to them, and inject function pointers into the preloaded library via specially-named helper functions; until those function pointers are set up we must make sure to call the libc functions instead (since some of them might be called during Python startup). The combination of all of this allows us to bridge C functions somewhat transparently into Python. This lets you supply a Python function or method as the mock replacement for a C library function, making it much simpler to record state. It's still not perfect: * We're using GI in an upside-down kind of way, and we specifically need GIR files rather than typelibs so that we can extract the original C type, so some fiddling is required for each new function you want to mock. * The subprocess arrangements are unavoidably slow and it's possible that they may cause problems with some test runners. * Some C functions (such as `stat`) tend to have multiple underlying entry points in the C library which must be preloaded independently. * You have to be careful about how your libraries are linked, because `ld -Wl,-Bsymbolic-functions` prevents `LD_PRELOAD` working for intra-library calls. * `ctypes should return composite types from callbacks `_. The least awful approach for now seems to be to construct the composite type in question, stash a reference to it forever, and then return a pointer to it as a void *; we can only get away with this because tests are by nature relatively short-lived. * The ctypes module's handling of 64-bit pointers is basically just awful. The right answer is probably to use a different callback-generation framework entirely (maybe extending PyGObject so that we can get at the pieces we need), but I've hacked around it for now. * It doesn't appear to be possible to install mock replacements for functions that are called directly from Python code using their GI wrappers. You can work around this by simply patching the GI wrapper instead, using `mock.patch`. I think the benefits, in terms of local clarity of tests, are worth the downsides. """ from __future__ import print_function __metaclass__ = type __all__ = ['GIMockTestCase'] import contextlib import ctypes import fcntl from functools import partial import os import pickle import shutil import subprocess import sys import tempfile from textwrap import dedent import traceback import unittest try: from unittest import mock except ImportError: import mock try: import xml.etree.cElementTree as etree except ImportError: import xml.etree.ElementTree as etree from click_package.tests.gimock_types import Stat, Stat64 from click_package.tests import config, get_executable # Borrowed from giscanner.girparser. CORE_NS = "http://www.gtk.org/introspection/core/1.0" C_NS = "http://www.gtk.org/introspection/c/1.0" GLIB_NS = "http://www.gtk.org/introspection/glib/1.0" def _corens(tag): return '{%s}%s' % (CORE_NS, tag) def _glibns(tag): return '{%s}%s' % (GLIB_NS, tag) def _cns(tag): return '{%s}%s' % (C_NS, tag) # Override some c:type annotations that g-ir-scanner gets a bit wrong. _c_type_override = { "passwd*": "struct passwd*", "stat*": "struct stat*", "stat64*": "struct stat64*", } # Mapping of GI type name -> ctypes type. _typemap = { "GError**": ctypes.c_void_p, "gboolean": ctypes.c_int, "gint": ctypes.c_int, "gint*": ctypes.POINTER(ctypes.c_int), "gint32": ctypes.c_int32, "gpointer": ctypes.c_void_p, "guint": ctypes.c_uint, "guint8**": ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)), "guint32": ctypes.c_uint32, "none": None, "utf8": ctypes.c_char_p, "utf8*": ctypes.POINTER(ctypes.c_char_p), } class GIMockTestCase(unittest.TestCase): def setUp(self): super(GIMockTestCase, self).setUp() self._preload_func_refs = [] self._composite_refs = [] self._delegate_funcs = {} def tearDown(self): self._preload_func_refs = [] self._composite_refs = [] self._delegate_funcs = {} def doCleanups(self): # we do not want to run the cleanups twice, just run it in the parent if "GIMOCK_SUBPROCESS" not in os.environ: return super(GIMockTestCase, self).doCleanups() def _gir_get_type(self, obj): ret = {} arrayinfo = obj.find(_corens("array")) if arrayinfo is not None: typeinfo = arrayinfo.find(_corens("type")) raw_ctype = arrayinfo.get(_cns("type")) else: typeinfo = obj.find(_corens("type")) raw_ctype = typeinfo.get(_cns("type")) gi_type = typeinfo.get("name") if obj.get("direction", "in") == "out": gi_type += "*" if arrayinfo is not None: gi_type += "*" ret["gi"] = gi_type ret["c"] = _c_type_override.get(raw_ctype, raw_ctype) return ret def _parse_gir(self, path): # A very, very crude GIR parser. We might have used # giscanner.girparser, but it's not importable in Python 3 at the # moment. tree = etree.parse(path) root = tree.getroot() assert root.tag == _corens("repository") assert root.get("version") == "1.2" ns = root.find(_corens("namespace")) assert ns is not None funcs = {} for func in ns.findall(_corens("function")): name = func.get(_cns("identifier")) # g-ir-scanner skips identifiers starting with "__", which we # need in order to mock stat effectively. Work around this. name = name.replace("under_under_", "__") headers = None for attr in func.findall(_corens("attribute")): if attr.get("name") == "headers": headers = attr.get("value") break rv = func.find(_corens("return-value")) assert rv is not None params = [] paramnode = func.find(_corens("parameters")) if paramnode is not None: for param in paramnode.findall(_corens("parameter")): params.append({ "name": param.get("name"), "type": self._gir_get_type(param), }) if func.get("throws", "0") == "1": params.append({ "name": "error", "type": {"gi": "GError**", "c": "GError**"}, }) funcs[name] = { "name": name, "headers": headers, "rv": self._gir_get_type(rv), "params": params, } return funcs def _ctypes_type(self, gi_type): return _typemap[gi_type["gi"]] def make_preloads(self, preloads): rpreloads = [] std_headers = set([ "dlfcn.h", # Not strictly needed, but convenient for ad-hoc debugging. "stdio.h", "stdint.h", "stdlib.h", "string.h", "sys/types.h", "unistd.h", ]) preload_headers = set() funcs = self._parse_gir("click_package/tests/preload.gir") for name, func in preloads.items(): info = funcs[name] rpreloads.append([info, func]) headers = info["headers"] if headers is not None: preload_headers.update(headers.split(",")) if "GIMOCK_SUBPROCESS" in os.environ: return None, rpreloads self._gimock_temp_dir = tempfile.mkdtemp(prefix="gimock") self.addCleanup(shutil.rmtree, self._gimock_temp_dir) preloads_dir = os.path.join(self._gimock_temp_dir, "_preloads") os.makedirs(preloads_dir) c_path = os.path.join(preloads_dir, "gimockpreload.c") with open(c_path, "w") as c: print("#define _GNU_SOURCE", file=c) for header in sorted(std_headers | preload_headers): print("#include <%s>" % header, file=c) print(file=c) for info, _ in rpreloads: conv = {} conv["name"] = info["name"] argtypes = [p["type"]["c"] for p in info["params"]] argnames = [p["name"] for p in info["params"]] conv["ret"] = info["rv"]["c"] conv["bareproto"] = ", ".join(argtypes) conv["proto"] = ", ".join( "%s %s" % pair for pair in zip(argtypes, argnames)) conv["args"] = ", ".join(argnames) if conv["ret"] == "gchar*": conv["need_strdup"] = "strdup" else: conv["need_strdup"] = "" # The delegation scheme used here is needed because trying # to pass pointers back and forward through ctypes is a # recipe for having them truncated to 32 bits at the drop of # a hat. This approach is less obvious but much safer. print(dedent("""\ typedef %(ret)s preloadtype_%(name)s (%(bareproto)s); preloadtype_%(name)s *ctypes_%(name)s = (void *) 0; preloadtype_%(name)s *real_%(name)s = (void *) 0; static volatile int delegate_%(name)s = 0; extern void _gimock_init_%(name)s (preloadtype_%(name)s *f) { ctypes_%(name)s = f; if (! real_%(name)s) { /* Retry lookup in case the symbol wasn't * resolvable until the program under test was * loaded. */ char *err; dlerror (); real_%(name)s = dlsym (RTLD_NEXT, "%(name)s"); if ((err = dlerror ()) != NULL) { fprintf (stderr, "Error getting address of symbol " "'%(name)s': %%s\\n", err); fflush (stderr); _exit (1); } } } """) % conv, file=c) if conv["ret"] == "void": print(dedent("""\ void %(name)s (%(proto)s) { if (ctypes_%(name)s) { delegate_%(name)s = 0; (*ctypes_%(name)s) (%(args)s); if (! delegate_%(name)s) return; } if (!real_%(name)s) _gimock_init_%(name)s (ctypes_%(name)s); (*real_%(name)s) (%(args)s); } """) % conv, file=c) else: print(dedent("""\ %(ret)s %(name)s (%(proto)s) { if (ctypes_%(name)s) { %(ret)s ret; delegate_%(name)s = 0; ret = (*ctypes_%(name)s) (%(args)s); if (! delegate_%(name)s) return %(need_strdup)s(ret); } if (!real_%(name)s) _gimock_init_%(name)s (ctypes_%(name)s); return (*real_%(name)s) (%(args)s); } """) % conv, file=c) print(dedent("""\ extern void _gimock_delegate_%(name)s (void) { delegate_%(name)s = 1; } """) % conv, file=c) print(dedent("""\ static void __attribute__ ((constructor)) gimockpreload_init (void) { char *err; dlerror (); """), file=c) print("\n".join(" " + line for line in (dedent("""\ real_%(name)s = dlsym (RTLD_NEXT, "%(name)s"); if ((err = dlerror ()) != NULL) { fprintf (stderr, "Error getting address of symbol " "'%(name)s': %%s\\n", err); fflush (stderr); _exit (1); } """) % {"name": name}).split("\n")), file=c) print("}", file=c) if "GIMOCK_PRELOAD_DEBUG" in os.environ: with open(c_path) as c: print(c.read()) # TODO: Use libtool or similar rather than hardcoding gcc invocation. lib_path = os.path.join(preloads_dir, "libgimockpreload.so") libraries = ["glib-2.0", "gee-0.8", "json-glib-1.0"] cflags = subprocess.check_output( ["pkg-config", "--cflags"] + libraries, universal_newlines=True).rstrip("\n").split() libs = subprocess.check_output( ["pkg-config", "--libs"] + libraries, universal_newlines=True).rstrip("\n").split() compile_cmd = [ "gcc", "-O0", "-g", "-shared", "-fPIC", "-DPIC", "-I", "lib/click", "-L", os.path.join(config.abs_top_builddir, "lib", "click", ".libs"), ] compile_cmd.extend(cflags) compile_cmd.extend(["-Wl,-soname", "-Wl,libgimockpreload.so"]) compile_cmd.append("-Wl,--no-as-needed") compile_cmd.append(c_path) compile_cmd.append("-lclick-0.4") compile_cmd.extend(libs) compile_cmd.append("-ldl") compile_cmd.extend(["-o", lib_path]) subprocess.check_call(compile_cmd) return lib_path, rpreloads # Use as: # with self.run_in_subprocess("func", ...) as (enter, preloads): # enter() # # test case body; preloads["func"] will be a mock.MagicMock # # instance @contextlib.contextmanager def run_in_subprocess(self, *patches): preloads = {} for patch in patches: preloads[patch] = mock.MagicMock() if preloads: lib_path, rpreloads = self.make_preloads(preloads) else: lib_path, rpreloads = None, None class ParentProcess(Exception): pass def helper(lib_path, rpreloads): if "GIMOCK_SUBPROCESS" in os.environ: del os.environ["LD_PRELOAD"] preload_lib = ctypes.cdll.LoadLibrary(lib_path) delegate_cfunctype = ctypes.CFUNCTYPE(None) for info, func in rpreloads: signature = [info["rv"]] + [ p["type"] for p in info["params"]] signature = [self._ctypes_type(t) for t in signature] cfunctype = ctypes.CFUNCTYPE(*signature) init = getattr( preload_lib, "_gimock_init_%s" % info["name"]) cfunc = cfunctype(func) self._preload_func_refs.append(cfunc) init(cfunc) delegate = getattr( preload_lib, "_gimock_delegate_%s" % info["name"]) self._delegate_funcs[info["name"]] = delegate_cfunctype( delegate) return rfd, wfd = os.pipe() # It would be cleaner to use subprocess.Popen(pass_fds=[wfd]), but # that isn't available in Python 2.7. if hasattr(os, "set_inheritable"): os.set_inheritable(wfd, True) else: fcntl.fcntl(rfd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) args = get_executable() + [ "-m", "unittest", "%s.%s.%s" % ( self.__class__.__module__, self.__class__.__name__, self._testMethodName)] env = os.environ.copy() env["GIMOCK_SUBPROCESS"] = str(wfd) if lib_path is not None: env["LD_PRELOAD"] = lib_path subp = subprocess.Popen(args, close_fds=False, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) os.close(wfd) reader = os.fdopen(rfd, "rb") stdout, stderr = subp.communicate() exctype = pickle.load(reader) # "normal" exit if exctype is not None and issubclass(exctype, SystemExit): pass elif exctype is not None and issubclass(exctype, AssertionError): print(stdout, file=sys.stdout) print(stderr, file=sys.stderr) raise AssertionError("Subprocess failed a test!") elif exctype is not None or subp.returncode != 0: print(stdout, file=sys.stdout) print(stderr, file=sys.stderr) raise Exception("Subprocess returned an error! (%s, %s)" % ( exctype, subp.returncode)) reader.close() raise ParentProcess() try: yield partial(helper, lib_path, rpreloads), preloads if "GIMOCK_SUBPROCESS" in os.environ: wfd = int(os.environ["GIMOCK_SUBPROCESS"]) writer = os.fdopen(wfd, "wb") # a sys.ext will generate a SystemExit exception, so we # push it into the pipe so that the parent knows whats going on pickle.dump(SystemExit, writer) writer.flush() sys.exit(0) except ParentProcess: pass except Exception as e: if "GIMOCK_SUBPROCESS" in os.environ: wfd = int(os.environ["GIMOCK_SUBPROCESS"]) writer = os.fdopen(wfd, "wb") # It would be better to use tblib to pickle the traceback so # that we can re-raise it properly from the parent process. # Until that's packaged and available to us, just print the # traceback and send the exception type. print() traceback.print_exc() pickle.dump(type(e), writer) writer.flush() os._exit(1) else: raise def make_pointer(self, composite): # Store a reference to a composite type and return a pointer to it, # working around http://bugs.python.org/issue5710. self._composite_refs.append(composite) return ctypes.addressof(composite) def make_string(self, s): # As make_pointer, but for a string. copied = ctypes.create_string_buffer(s.encode()) self._composite_refs.append(copied) return ctypes.addressof(copied) def convert_pointer(self, composite_type, address): # Return a ctypes composite type instance at a given address. return composite_type.from_address(address) def convert_stat_pointer(self, name, address): # As convert_pointer, but for a "struct stat *" or "struct stat64 *" # depending on the wrapped function name. stat_type = {"__xstat": Stat, "__xstat64": Stat64} return self.convert_pointer(stat_type[name], address) def delegate_to_original(self, name): # Cause the wrapper function to delegate to the original version # after the callback returns. (Note that the callback still needs # to return something type-compatible with the declared result type, # although the return value will otherwise be ignored.) self._delegate_funcs[name]() click-0.5.0/click_package/tests/gimock_types.py000066400000000000000000000063311402441472600215610ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """A collection of variously hacky ctypes definitions for use with gimock.""" import ctypes from click_package.tests.config import ( STAT_OFFSET_GID, STAT_OFFSET_UID, STAT64_OFFSET_GID, STAT64_OFFSET_UID, ) class Passwd(ctypes.Structure): _fields_ = [ ("pw_name", ctypes.c_char_p), ("pw_passwd", ctypes.c_char_p), ("pw_uid", ctypes.c_uint32), ("pw_gid", ctypes.c_uint32), ("pw_gecos", ctypes.c_char_p), ("pw_dir", ctypes.c_char_p), ("pw_shell", ctypes.c_char_p), ] # TODO: This is pretty awful. The layout of "struct stat" is complicated # enough that we have to use offsetof() in configure to pick out the fields # we care about. Fortunately, we only care about a couple of fields, and # since this is an output parameter it doesn't matter if our structure is # too short (if we cared about this then we could use AC_CHECK_SIZEOF to # figure it out). class Stat(ctypes.Structure): _pack_ = 1 _fields_ = [] _fields_.append( ("pad0", ctypes.c_ubyte * min(STAT_OFFSET_UID, STAT_OFFSET_GID))) if STAT_OFFSET_UID < STAT_OFFSET_GID: _fields_.append(("st_uid", ctypes.c_uint32)) pad = (STAT_OFFSET_GID - STAT_OFFSET_UID - ctypes.sizeof(ctypes.c_uint32)) assert pad >= 0 if pad > 0: _fields_.append(("pad1", ctypes.c_ubyte * pad)) _fields_.append(("st_gid", ctypes.c_uint32)) else: _fields_.append(("st_gid", ctypes.c_uint32)) pad = (STAT_OFFSET_UID - STAT_OFFSET_GID - ctypes.sizeof(ctypes.c_uint32)) assert pad >= 0 if pad > 0: _fields_.append(("pad1", ctypes.c_ubyte * pad)) _fields_.append(("st_uid", ctypes.c_uint32)) class Stat64(ctypes.Structure): _pack_ = 1 _fields_ = [] _fields_.append( ("pad0", ctypes.c_ubyte * min(STAT64_OFFSET_UID, STAT64_OFFSET_GID))) if STAT64_OFFSET_UID < STAT64_OFFSET_GID: _fields_.append(("st_uid", ctypes.c_uint32)) pad = (STAT64_OFFSET_GID - STAT64_OFFSET_UID - ctypes.sizeof(ctypes.c_uint32)) assert pad >= 0 if pad > 0: _fields_.append(("pad1", ctypes.c_ubyte * pad)) _fields_.append(("st_gid", ctypes.c_uint32)) else: _fields_.append(("st_gid", ctypes.c_uint32)) pad = (STAT64_OFFSET_UID - STAT64_OFFSET_GID - ctypes.sizeof(ctypes.c_uint32)) assert pad >= 0 if pad > 0: _fields_.append(("pad1", ctypes.c_ubyte * pad)) _fields_.append(("st_uid", ctypes.c_uint32)) click-0.5.0/click_package/tests/helpers.py000066400000000000000000000302451402441472600205270ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . # # This file contains code from Python 3.3, released under the Python license # (http://docs.python.org/3/license.html). """Testing helpers.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestCase', 'mkfile', 'touch', ] import contextlib from functools import wraps import json import os import re import shutil import sys import tempfile import unittest try: from unittest import mock except ImportError: import mock from gi.repository import Click, GLib from click_package.tests import gimock def disable_logging(func): """Decorator to disable logging e.g. during a test""" @wraps(func) def wrapper(*args, **kwargs): import logging logging.disable(logging.CRITICAL) try: return func(*args, **kwargs) finally: logging.disable(logging.NOTSET) return wrapper def make_installed_click(db, db_dir, package="test-1", version="1.0", json_data={}, make_current=True, user="@all"): """Create a fake installed click package for the given db/db_dir""" json_data["name"] = package json_data["version"] = version with mkfile_utf8(os.path.join( db_dir, package, version, ".click", "info", "%s.manifest" % package)) as f: json.dump(json_data, f, ensure_ascii=False) if make_current: os.symlink( version, os.path.join(db_dir, package, "current")) if user == "@all": registry = Click.User.for_all_users(db) else: registry = Click.User.for_user(db, user) registry.set_version(package, version) return os.path.join(db_dir, package, version) class TestCase(gimock.GIMockTestCase): def setUp(self): super(TestCase, self).setUp() self.temp_dir = None self.save_env = dict(os.environ) self.maxDiff = None def tearDown(self): for key in set(os.environ) - set(self.save_env): del os.environ[key] for key, value in os.environ.items(): if value != self.save_env[key]: os.environ[key] = self.save_env[key] for key in set(self.save_env) - set(os.environ): os.environ[key] = self.save_env[key] def use_temp_dir(self): if self.temp_dir is not None: return self.temp_dir self.temp_dir = tempfile.mkdtemp(prefix="click") self.assertTrue(os.path.exists(self.temp_dir)) self.addCleanup(shutil.rmtree, self.temp_dir) return self.temp_dir # Monkey-patch for Python 2/3 compatibility. if not hasattr(unittest.TestCase, 'assertCountEqual'): assertCountEqual = unittest.TestCase.assertItemsEqual if not hasattr(unittest.TestCase, 'assertRegex'): assertRegex = unittest.TestCase.assertRegexpMatches # Renamed in Python 3.2 to omit the trailing 'p'. if not hasattr(unittest.TestCase, 'assertRaisesRegex'): assertRaisesRegex = unittest.TestCase.assertRaisesRegexp def assertRaisesGError(self, domain_name, code, callableObj, *args, **kwargs): with self.assertRaises(GLib.GError) as cm: callableObj(*args, **kwargs) self.assertEqual(domain_name, cm.exception.domain) self.assertEqual(code, cm.exception.code) def assertRaisesFileError(self, code, callableObj, *args, **kwargs): self.assertRaisesGError( "g-file-error-quark", code, callableObj, *args, **kwargs) def assertRaisesDatabaseError(self, code, callableObj, *args, **kwargs): self.assertRaisesGError( "click-database-error-quark", code, callableObj, *args, **kwargs) def assertRaisesFrameworkError(self, code, callableObj, *args, **kwargs): self.assertRaisesGError( "click-framework-error-quark", code, callableObj, *args, **kwargs) def assertRaisesHooksError(self, code, callableObj, *args, **kwargs): self.assertRaisesGError( "click-hooks-error-quark", code, callableObj, *args, **kwargs) def assertRaisesQueryError(self, code, callableObj, *args, **kwargs): self.assertRaisesGError( "click-query-error-quark", code, callableObj, *args, **kwargs) def assertRaisesUserError(self, code, callableObj, *args, **kwargs): self.assertRaisesGError( "click-user-error-quark", code, callableObj, *args, **kwargs) def _setup_frameworks(self, preloads, frameworks_dir=None, frameworks=[]): frameworks_dir = self._create_mock_framework_dir(frameworks_dir) shutil.rmtree(frameworks_dir, ignore_errors=True) for framework in frameworks: self._create_mock_framework_file(framework) preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(frameworks_dir)) def _create_mock_framework_dir(self, frameworks_dir=None): if frameworks_dir is None: frameworks_dir = os.path.join(self.temp_dir, "frameworks") patcher = mock.patch('click_package.paths.frameworks_dir', frameworks_dir) patcher.start() self.addCleanup(patcher.stop) Click.ensuredir(frameworks_dir) return frameworks_dir def _create_mock_framework_file(self, framework_name): self.use_temp_dir() self._create_mock_framework_dir() r = r'(?P[a-z]+-sdk)-(?P[0-9.]+)(-[a-z0-9-]+)?' match = re.match(r, framework_name) if match is None: name = "unknown" ver = "1.0" else: name = match.group("name") ver = match.group("ver") framework_filename = os.path.join( self.temp_dir, "frameworks", "{0}.framework".format(framework_name)) with open(framework_filename, "w") as f: f.write("Base-Name: {0}\n".format(name)) f.write("Base-Version: {0}\n".format(ver)) if not hasattr(mock, "call"): # mock 0.7.2, the version in Ubuntu 12.04 LTS, lacks mock.ANY and # mock.call. Since it's so convenient, monkey-patch a partial backport # (from Python 3.3 unittest.mock) into place here. class _ANY(object): "A helper object that compares equal to everything." def __eq__(self, other): return True def __ne__(self, other): return False def __repr__(self): return '' mock.ANY = _ANY() class _Call(tuple): """ A tuple for holding the results of a call to a mock, either in the form `(args, kwargs)` or `(name, args, kwargs)`. If args or kwargs are empty then a call tuple will compare equal to a tuple without those values. This makes comparisons less verbose:: _Call(('name', (), {})) == ('name',) _Call(('name', (1,), {})) == ('name', (1,)) _Call(((), {'a': 'b'})) == ({'a': 'b'},) The `_Call` object provides a useful shortcut for comparing with call:: _Call(((1, 2), {'a': 3})) == call(1, 2, a=3) _Call(('foo', (1, 2), {'a': 3})) == call.foo(1, 2, a=3) If the _Call has no name then it will match any name. """ def __new__(cls, value=(), name=None, parent=None, two=False, from_kall=True): name = '' args = () kwargs = {} _len = len(value) if _len == 3: name, args, kwargs = value elif _len == 2: first, second = value if isinstance(first, str): name = first if isinstance(second, tuple): args = second else: kwargs = second else: args, kwargs = first, second elif _len == 1: value, = value if isinstance(value, str): name = value elif isinstance(value, tuple): args = value else: kwargs = value if two: return tuple.__new__(cls, (args, kwargs)) return tuple.__new__(cls, (name, args, kwargs)) def __init__(self, value=(), name=None, parent=None, two=False, from_kall=True): self.name = name self.parent = parent self.from_kall = from_kall def __eq__(self, other): if other is mock.ANY: return True try: len_other = len(other) except TypeError: return False self_name = '' if len(self) == 2: self_args, self_kwargs = self else: self_name, self_args, self_kwargs = self other_name = '' if len_other == 0: other_args, other_kwargs = (), {} elif len_other == 3: other_name, other_args, other_kwargs = other elif len_other == 1: value, = other if isinstance(value, tuple): other_args = value other_kwargs = {} elif isinstance(value, str): other_name = value other_args, other_kwargs = (), {} else: other_args = () other_kwargs = value else: # len 2 # could be (name, args) or (name, kwargs) or (args, kwargs) first, second = other if isinstance(first, str): other_name = first if isinstance(second, tuple): other_args, other_kwargs = second, {} else: other_args, other_kwargs = (), second else: other_args, other_kwargs = first, second if self_name and other_name != self_name: return False # this order is important for ANY to work! return (other_args, other_kwargs) == (self_args, self_kwargs) def __ne__(self, other): return not self.__eq__(other) def __call__(self, *args, **kwargs): if self.name is None: return _Call(('', args, kwargs), name='()') name = self.name + '()' return _Call((self.name, args, kwargs), name=name, parent=self) def __getattr__(self, attr): if self.name is None: return _Call(name=attr, from_kall=False) name = '%s.%s' % (self.name, attr) return _Call(name=name, parent=self, from_kall=False) mock.call = _Call(from_kall=False) @contextlib.contextmanager def mkfile(path, mode="w"): Click.ensuredir(os.path.dirname(path)) with open(path, mode) as f: yield f @contextlib.contextmanager def mkfile_utf8(path, mode="w"): Click.ensuredir(os.path.dirname(path)) if sys.version < "3": import codecs with codecs.open(path, mode, "UTF-8") as f: yield f else: # io.open is available from Python 2.6, but we only use it with # Python 3 because it raises exceptions when passed bytes. import io with io.open(path, mode, encoding="UTF-8") as f: yield f def touch(path): with mkfile(path, mode="a"): pass def make_file_with_content(filename, content, mode=0o644): """Create a file with the given content and mode""" Click.ensuredir(os.path.dirname(filename)) with open(filename, "w") as f: f.write(content) os.chmod(filename, mode) click-0.5.0/click_package/tests/integration/000077500000000000000000000000001402441472600210325ustar00rootroot00000000000000click-0.5.0/click_package/tests/integration/__init__.py000066400000000000000000000000001402441472600231310ustar00rootroot00000000000000click-0.5.0/click_package/tests/integration/data/000077500000000000000000000000001402441472600217435ustar00rootroot00000000000000click-0.5.0/click_package/tests/integration/data/evil-keyring/000077500000000000000000000000001402441472600243505ustar00rootroot00000000000000click-0.5.0/click_package/tests/integration/data/evil-keyring/pubring.gpg000066400000000000000000000013021402441472600265110ustar00rootroot00000000000000˜SÑäkcîÅ?ŽY¸‘@;ÚÝh_U¦ø¨odDÖSYetOÿrø¯çºmûM†ð¼Y©‚¢íß"ú¹³Ñ¿ßò¥ºã/š5•BûÖ±h•ô̧?¢ I·îXMœ¡ÌëÞ¢ÅeͰkCä¤? ™¼¼RÍ,-´?Morbid Malory (A click integration test key) ˆ¸"SÑ  € ³‹šÁ¶z ‡æÿI×~_w ƒNûµV“ýû¸ò\&·ÁþX‚ýûpÛ)ý‚á0ÀËŸÚºÒ•Œ{¯{˜_޾¯Íè½iµ”uoNûÔ/©!55øæ:>¶TNÈ“úE͵êæêÞæN$vPšö[ŸÒY²I+ËŒ¹HwÓ*éoƒ~°¸SÑůʺ*)Z¥RëEËOð ú³þÆ #a¼¨Hrb쵯^uÖ'ˆ.ÜOÔ4#S„iá×*DÐ×r’±o¢L3Íý;<–²Ž„!wJÈ œ]ƒâíN*/ÊIœÄ†þ]›pÿ"t.ª¶jüGFaà&ÑF3´ig„ܬCß&„MO*ˆŸ SÑ ³‹šÁ¶z Ñ*´-•QÉLJ±Ž+ÃmDœÆ ë8ÿÒe¡a#»íèËJe¸É‹¯/9AúЩ’ú-Ås¬Þy§<ùS+– ï SñçŽG÷DæâÃ? ¢V{•“?Tâ¿O=‰Ç!FÖÀ,T »‚C%SMÁÈ¡VÞ"‘c-¸”wÇ•ÖÝÁ°click-0.5.0/click_package/tests/integration/data/evil-keyring/secring.gpg000066400000000000000000000025321402441472600265030ustar00rootroot00000000000000•ØSÑäkcîÅ?ŽY¸‘@;ÚÝh_U¦ø¨odDÖSYetOÿrø¯çºmûM†ð¼Y©‚¢íß"ú¹³Ñ¿ßò¥ºã/š5•BûÖ±h•ô̧?¢ I·îXMœ¡ÌëÞ¢ÅeͰkCä¤? ™¼¼RÍ,-þ"ÒSX›‚©¼;uʉ” ZC’vé<-dsÐa°ÁÙxw˜ð’¡üng½5 cêàГ­O+‹-Ç%°Ò†›iä˜'œœ~ýû’åif¬¡cÅVæy h>™±KDÁ6Ùïb‡¯‰T²q‘•«%ø6çÌhyd¿’“µúéƒätB¡}Á'¹?·½í¦º‡ÙOµAóÚcv-˜ù˜:|+µ ØŸE:ùÝWa'A®zs'ô[y;ñ‡ãçÛ%J¤CpË?ªÎºR_Î׬ƒvcecˆvv8j¨í„ÿ-!–l¢Ã™êÈ£u×Ùê‹ü˜†v”Œ2gT'Heól=ƒ€m‹Dº”_\QøG”¤Ô)]`uјÕW™`»$Jbºe žkWÓõd›´?Morbid Malory (A click integration test key) ˆ¸"SÑ  € ³‹šÁ¶z ‡æÿI×~_w ƒNûµV“ýû¸ò\&·ÁþX‚ýûpÛ)ý‚á0ÀËŸÚºÒ•Œ{¯{˜_޾¯Íè½iµ”uoNûÔ/©!55øæ:>¶TNÈ“úE͵êæêÞæN$vPšö[ŸÒY²I+ËŒ¹HwÓ*éoƒ~°ØSÑůʺ*)Z¥RëEËOð ú³þÆ #a¼¨Hrb쵯^uÖ'ˆ.ÜOÔ4#S„iá×*DÐ×r’±o¢L3Íý;<–²Ž„!wJÈ œ]ƒâíN*/ÊIœÄ†þ]›pÿ"t.ª¶jüGFaà&ÑF3´ig„ܬCß&„MO*ý•ñ!+ð0k¶¹õ*¥´ír­¨ÙT¸  P"‡™ò²*Ù4çRþ@ò'–Ÿ¡>‹;-±è jt§9»Ö³¡é§v¦üZC¼;r6düÁ·ü/°±‡|)‚”b_“¹!<ºQ¿fîjž¤¾wÞ‡)°R°dr"Vž½DG ÊãÞO!ÉÉ3ýáxëúëLþÿà‚ž‡Ï±M0è°cÙˆ…qmà´¹F! ‹D²yì Ü¡ÁÞöPÉnê왃ÿ¬4ËpåáúÌÄŽ—4‘è¬ãT¯ÐuÎèûô|̃™¨R€ÑÖVôg8Òq™.cÄ IôÒˆ‰¬0Y[Þ3”Zá˜o<çÿZ+!qÎP\5¸rr¨xjö ³ïîWLËÂ4¨Ù’Pi‹T /2ðsî®fGvÞÜ$ш³†ŠÛ{Ô#µæŸ¨HˆŸ SÑ ³‹šÁ¶z Ñ*´-•QÉLJ±Ž+ÃmDœÆ ë8ÿÒe¡a#»íèËJe¸É‹¯/9AúЩ’ú-Ås¬Þy§<ùS+– ï SñçŽG÷DæâÃ? ¢V{•“?Tâ¿O=‰Ç!FÖÀ,T »‚C%SMÁÈ¡VÞ"‘c-¸”wÇ•ÖÝÁ°click-0.5.0/click_package/tests/integration/data/evil-keyring/trustdb.gpg000066400000000000000000000024001402441472600265320ustar00rootroot00000000000000gpgSÑZ  Âè[îÎÊÕXd³‹šÁ¶z  ŽÈÙ¤Qéa Ü*ˆiÞ2}æ?)click-0.5.0/click_package/tests/integration/data/origin-keyring/000077500000000000000000000000001402441472600247005ustar00rootroot00000000000000click-0.5.0/click_package/tests/integration/data/origin-keyring/pubring.gpg000066400000000000000000000023511402441472600270460ustar00rootroot00000000000000™ SÆLÃÕmzŠ"×r®íp ¢ÒÔâNE]~\n »LâêWq‰,¸dYö‰¸—ž—…'GŽÔÁ„84òš†ýû°Á 0|‰7!SÆL   € ƒTÈ ŸÑ¹ÚÚ¤ÿ{MˆD^ ‰¶£É«FêœÃ/Òêµû—´µ0™ˆ@Ïæ4³™au€jŽBíV¹8ªÓÅBÙ±5!e^éRÛeÿ8cœ ñJ²É“UÍ1§Ñ¼KaÝÀÅð¿&qà´½n_Ðuë£[½‘)ɱP]pýÚ}€Ò `2 ’Äå°x­ð½¶rvÈ쬇Ã߈,«¾5òŽEÎÇùa^É‹åêzÏü”ÈÉ(ê~EY8ïQéh–I´9tÚ¹[a)Òµï8"}iž³!²_žÝp¶Vô‚è`×{éxÝ“õc)c€¢ÏñB•šET…äæŠ2d! ðGȧÜà ô:°¹ SÆL´¬õvž©ªÉW´Y €)E¸4cäAô#2p¤V/¥ ;ww%ëØ‘ãèjs,ْɃ¾u4Gvqp5 ´\Nsý…õå`¨¿L'p6)ï{—ÖšPﻇ”;" ÎÃgˆ\ÿ€… Ï{ =MPxbxá¨G:©¤u{”ÞóX÷uq¬=é¯Gl­ ñ²°~£/s%$íJœNEnûÜ¥$Sƒ$ÿðP½{ ·‚ƒ<ˆ¾sVŒPyÉgä °°æ«ÈnH#RDrò|Ç‚ééHZcèó'—#1I’XO'ÞXù„i?¶ˆü¶Ðù0ë€Å7$~Ì!¯èÏÓ>>$£C“ÛÚNg^¢ìeo\È:¥‰ SÆL ƒTÈ ŸÑ¹Úçƒÿz€˜®Ã•Éý“9õ¤¼â1Y|2$&…JE ŽêúTFŒR'9Û}-xiY`]gÔJU (!NbêÏì^éfDl2JŸ*è )–È& »Ll–ùÎ7‹ì7hòM—+ÀQ²ì”( 7Ýév!œC=çz¥QC÷0÷ì7$I‰ÅRpÍÈ•t_Ú $‰ªÍç·àK`@œ3ÕÝ5¼R‚ˆau”n¼XÙ2Â…q^›:Vu9À‹k®çðyWÒ­Â]&[ZøxÚ¬,¢xËxIqÏ`&£[¬|ÃÁYé8 Ñ.‘4UÈŒMB}(+0ˆZÚ¼TëýQã;ç°click-0.5.0/click_package/tests/integration/data/origin-keyring/secring.gpg000066400000000000000000000047771402441472600270500ustar00rootroot00000000000000•˜SÆLÃÕmzŠ"×r®íp ¢ÒÔâNE]~\n »LâêWq‰,¸dYö‰¸—ž—…'GŽÔÁ„84òš†ýû°Á 0|¢%ÝèÌiìÚ^« /xÒ$àÓàŽy5óâóÔ=ØÈåËK¡TÕÃ_ê9ÛZÓÀ×w¼X¿ðl°o¯É7÷6¬4ô:ËX€1Ô HVNRÁ2:”òüõŽÍ¦SͰÀ„ö7õU ¹‹§‰ÐÔÓOê$p²‚f}æu”­ºŸiÎVÂÝ Ðp¿#9ótà0ÌCß{ö$bVÏj±Çá®rðFRÝHÐì•1Š  ¿˜±5þîîÌš37ΆžÝxÑÖ@χ]>€íï¦y¯4 ò6‹DâíS¨…]‹¤ûÊlž©Ç2ËäÄ(ôe\îý×–@ý,ØÆŒ‰7!SÆL   € ƒTÈ ŸÑ¹ÚÚ¤ÿ{MˆD^ ‰¶£É«FêœÃ/Òêµû—´µ0™ˆ@Ïæ4³™au€jŽBíV¹8ªÓÅBÙ±5!e^éRÛeÿ8cœ ñJ²É“UÍ1§Ñ¼KaÝÀÅð¿&qà´½n_Ðuë£[½‘)ɱP]pýÚ}€Ò `2 ’Äå°x­ð½¶rvÈ쬇Ã߈,«¾5òŽEÎÇùa^É‹åêzÏü”ÈÉ(ê~EY8ïQéh–I´9tÚ¹[a)Òµï8"}iž³!²_žÝp¶Vô‚è`×{éxÝ“õc)c€¢ÏñB•šET…äæŠ2d! ðGȧÜà ô:°˜SÆL´¬õvž©ªÉW´Y €)E¸4cäAô#2p¤V/¥ ;ww%ëØ‘ãèjs,ْɃ¾u4Gvqp5 ´\Nsý…õå`¨¿L'p6)ï{—ÖšPﻇ”;" ÎÃgˆ\ÿ€… Ï{ =MPxbxá¨G:©¤u{”ÞóX÷uq¬=é¯Gl­ ñ²°~£/s%$íJœNEnûÜ¥$Sƒ$ÿðP½{ ·‚ƒ<ˆ¾sVŒPyÉgä °°æ«ÈnH#RDrò|Ç‚ééHZcèó'—#1I’XO'ÞXù„i?¶ˆü¶Ðù0ë€Å7$~Ì!¯èÏÓ>>$£C“ÛÚNg^¢ìeo\È:¥ü ¡h¹…¦ûÊþˆó”¯ øµp¤‘²"5¯û½°ˆ¹µÉ_(ùÔrkz¨À ú[F«Dq©Å{×üëU ³Æù%3h(“dÐiàížxÃf_Q ƒÕÌoïûføÚ]¼/EÄɈ͞Ï>6 I#¯ó{ ²zBÎ"Ö3ÓÏ>Þ6ŠÔä.zÅ bµNÜi7Œ$µ²œSêi sžÈÑÙÆéÔ”ohÿÊ Xr[j eøX6¼EWE=ähAš.¾d`å›&Q%I(pªXÄm›b õ| ,C­ðØmâÌB6§–÷·ëCÚXx“È=6rΪ ƒÕÀuþ­WÀ¾Î”¯© ¬«ƒ$È ¸'„³\Bæ#^ô¥„¸)¬ÌõÙß‚M%’{t´Ý sw•f=Ë"œYM ˜ ]oŽ _4#[‰S„"Yçëç]\ ±½cD[h4sŠØð×ÄddlkÆ@ׯl«$Ÿ?{ÁÿËhĉþ'ØbãÓß‹‘µüŸ]‰Ê}|´èrÙõö¾P`±Gµž7(å¯j>}Ÿ•¬'»^¦† ÒOÜ#‚aOÇ9µÑ=nÔÅŠ£-V° vÄ@eÂ|ä ®¦v¨Z+|ú.b«Qb»†©~.í’lW*%?ô¬ t[&%“¦çóAK@‹Ølh Äê4™Éâ!S)Aܧvél ¡Zº¨¹»KY–]F¿p ¬ Ñï?Ùèçq'Õƒî<ñžhœ-,)Zéþ„YùË*¡B[U °zîú™g8Ç;#Ó̱( ˆm³Çnócçþ+ÖÈ[Œ*ÐQ‹¦ ŸŸÙ 5?‰ SÆL ƒTÈ ŸÑ¹Úçƒÿz€˜®Ã•Éý“9õ¤¼â1Y|2$&…JE ŽêúTFŒR'9Û}-xiY`]gÔJU (!NbêÏì^éfDl2JŸ*è )–È& »Ll–ùÎ7‹ì7hòM—+ÀQ²ì”( 7Ýév!œC=çz¥QC÷0÷ì7$I‰ÅRpÍÈ•t_Ú $‰ªÍç·àK`@œ3ÕÝ5¼R‚ˆau”n¼XÙ2Â…q^›:Vu9À‹k®çðyWÒ­Â]&[ZøxÚ¬,¢xËxIqÏ`&£[¬|ÃÁYé8 Ñ.‘4UÈŒMB}(+0ˆZÚ¼TëýQã;ç°click-0.5.0/click_package/tests/integration/data/origin-keyring/trustdb.gpg000066400000000000000000000022601402441472600270660ustar00rootroot00000000000000gpgSÃŽ click-0.5.0/click_package/tests/integration/helpers.py000066400000000000000000000104221402441472600230450ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests helper for the click CLI interface.""" import contextlib import glob import json import os import random import shutil import string import subprocess import tempfile import unittest def require_root(): if os.getuid() != 0: raise unittest.SkipTest("This test needs to run as root") def require_network(): try: if subprocess.call(["ping", "-c1", "archive.ubuntu.com"]) != 0: raise unittest.SkipTest("Need network") except Exception: pass def require_overlay(): try: subprocess.check_call(["/sbin/modprobe", "overlay"]) except subprocess.CalledProcessError: raise unittest.SkipTest("Requires overlay fs support") @contextlib.contextmanager def chdir(target): curdir = os.getcwd() os.chdir(target) try: yield finally: os.chdir(curdir) def cmdline_for_user(username): """Helper to get the click commandline for the given username""" if username == "@all": user = "--all-users" else: user = "--user=%s" % username return user class ClickTestCase(unittest.TestCase): @classmethod def setUpClass(cls): if "TEST_INTEGRATION" not in os.environ: raise unittest.SkipTest("Skipping integration tests") cls.click_binary = os.environ.get("CLICK_BINARY", "/usr/bin/click") def setUp(self): super(ClickTestCase, self).setUp() self.temp_dir = tempfile.mkdtemp() def tearDown(self): super(ClickTestCase, self).tearDown() # we force the cleanup before removing the tempdir so that stuff # in temp_dir is still available self.doCleanups() shutil.rmtree(self.temp_dir) def click_install(self, path_to_click, click_name, username, allow_unauthenticated=True): cmd = [self.click_binary, "install", cmdline_for_user(username)] if allow_unauthenticated: cmd.append("--allow-unauthenticated") cmd.append(path_to_click) subprocess.check_call(cmd) self.addCleanup(self.click_unregister, click_name, username) def click_unregister(self, click_name, username): subprocess.check_call( [self.click_binary, "unregister", cmdline_for_user(username), click_name]) def _create_manifest(self, target, name, version, framework, hooks={}): with open(target, "w") as f: json.dump({ 'name': name, 'version': str(version), 'maintainer': 'Foo Bar ', 'title': 'test title', 'framework': framework, 'hooks': hooks, }, f) def _make_click(self, name=None, version=1.0, framework="ubuntu-sdk-13.10", hooks={}): if name is None: name = "com.example.%s" % "".join( random.choice(string.ascii_lowercase) for i in range(10)) tmpdir = tempfile.mkdtemp() self.addCleanup(lambda: shutil.rmtree(tmpdir)) clickdir = os.path.join(tmpdir, name) os.makedirs(clickdir) self._create_manifest(os.path.join(clickdir, "manifest.json"), name, version, framework, hooks) with open(os.path.join(clickdir, "README"), "w") as f: f.write("hello world!") with chdir(tmpdir), open(os.devnull, "w") as devnull: subprocess.call( [self.click_binary, "build", clickdir], stdout=devnull) generated_clicks = glob.glob(os.path.join(tmpdir, "*.click")) self.assertEqual(len(generated_clicks), 1) return generated_clicks[0] click-0.5.0/click_package/tests/integration/test_build.py000066400000000000000000000016471402441472600235520ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click CLI build command.""" import os from .helpers import ClickTestCase class TestBuild(ClickTestCase): def test_build(self): path_to_click = self._make_click() self.assertTrue(os.path.exists(path_to_click)) click-0.5.0/click_package/tests/integration/test_buildsource.py000066400000000000000000000034721402441472600247710ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click CLI buildsource command.""" import os import shutil import subprocess import tarfile import tempfile from .helpers import ( chdir, ClickTestCase, ) class TestBuildSource(ClickTestCase): def test_buildsource(self): temp_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, temp_dir) with chdir(temp_dir): with open(os.path.join(temp_dir, "README"), "w") as f: f.write("I'm a source package") os.mkdir(os.path.join(temp_dir, ".git")) os.mkdir(os.path.join(temp_dir, ".normal")) self._create_manifest(os.path.join(temp_dir, "manifest.json"), "srcfoo", "1.2", "ubuntu-sdk-13.10") subprocess.check_call( [self.click_binary, "buildsource", temp_dir], universal_newlines=True) # ensure we have the content we expect source_file = "srcfoo_1.2.tar.gz" tar = tarfile.open(source_file) self.assertEqual( sorted(tar.getnames()), sorted([".", "./.normal", "./manifest.json", "./README"])) click-0.5.0/click_package/tests/integration/test_chroot.py000066400000000000000000000066101402441472600237440ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click chroot feature.""" import subprocess import unittest from .helpers import ( require_network, require_overlay, require_root, ClickTestCase, ) # architectures present in 14.04 (current default framework) ALLOW_ARCHITECTURES = [ "amd64", "arm64", "armhf", "i386", "powerpc", "ppc64el"] def skipUnlessAllowedArchitecture(): system_arch = subprocess.check_output( ["dpkg", "--print-architecture"], universal_newlines=True).strip() if system_arch in ALLOW_ARCHITECTURES: return lambda func: func else: return unittest.skip("%s does not exist in 14.04") @skipUnlessAllowedArchitecture() class TestChroot(ClickTestCase): @classmethod def command(cls, arch, *args): return [cls.click_binary, "chroot", "-a", arch] + list(args) @classmethod def setUpClass(cls): super(TestChroot, cls).setUpClass() require_root() require_network() require_overlay() cls.arch = subprocess.check_output( ["dpkg", "--print-architecture"], universal_newlines=True).strip() subprocess.check_call(cls.command(cls.arch, "create")) @classmethod def tearDownClass(cls): subprocess.check_call(cls.command(cls.arch, "destroy")) def test_upgrade(self): subprocess.check_call(self.command(self.arch, "upgrade")) def test_install(self): subprocess.check_call(self.command(self.arch, "install", "apt-utils")) def test_run(self): output = subprocess.check_output( self.command(self.arch, "run", "echo", "hello world"), universal_newlines=True) self.assertEqual(output, "hello world\n") def test_maint(self): output = subprocess.check_output( self.command(self.arch, "maint", "id"), universal_newlines=True) self.assertEqual(output, "uid=0(root) gid=0(root) groups=0(root)\n") def test_exists_ok(self): subprocess.check_call(self.command(self.arch, "exists")) def test_exists_no(self): with self.assertRaises(subprocess.CalledProcessError): subprocess.check_call( self.command("arch-that-does-not-exist", "exists")) class TestChrootName(TestChroot): """Run the chroot tests again with a different --name.""" @classmethod def command(cls, arch, *args): return super(TestChrootName, cls).command( arch, "-n", "testname", *args) def test_exists_different_name_fails(self): # "click chroot exists" fails for a non-existent name. with self.assertRaises(subprocess.CalledProcessError): subprocess.check_call(super(TestChrootName, self).command( self.arch, "-n", "testname2", "exists")) click-0.5.0/click_package/tests/integration/test_contents.py000066400000000000000000000023041402441472600242770ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click CLI contents command.""" import re import subprocess from .helpers import ClickTestCase class TestContents(ClickTestCase): def test_contents(self): name = "com.example.contents" path_to_click = self._make_click(name) output = subprocess.check_output([ self.click_binary, "contents", path_to_click], universal_newlines=True) self.assertTrue(re.search( r'-rw-r[-w]-r-- root/root\s+[0-9]+\s+[0-9-]+ [0-9:]+ ./README', output)) click-0.5.0/click_package/tests/integration/test_frameworks.py000066400000000000000000000024231402441472600246240ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click CLI frameworks command.""" import os import subprocess from .helpers import ClickTestCase class TestFrameworks(ClickTestCase): def setUp(self): super(TestFrameworks, self).setUp() if (not os.path.exists("/usr/share/click/frameworks") or not os.listdir("/usr/share/click/frameworks")): self.skipTest("Please install ubuntu-sdk-libs") def test_framework_list(self): output = subprocess.check_output([ self.click_binary, "framework", "list"], universal_newlines=True) self.assertTrue("ubuntu-sdk-" in output) click-0.5.0/click_package/tests/integration/test_hook.py000066400000000000000000000056121402441472600234070ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click hook feature.""" import os import subprocess from textwrap import dedent from .helpers import ( ClickTestCase, require_root, ) class TestHook(ClickTestCase): @classmethod def setUpClass(cls): super(TestHook, cls).setUpClass() require_root() def _make_hook(self, name): hook_fname = "/usr/share/click/hooks/%s.hook" % name canary_fname = os.path.join(self.temp_dir, "canary.sh") canary_log = os.path.join(self.temp_dir, "canary.log") with open(hook_fname, "w") as f: f.write(dedent("""\ Pattern: ${home}/${id}.test-hook User-Level: yes Exec: %s Hook-Name: %s """ % (canary_fname, name))) with open(canary_fname, "w") as f: f.write(dedent("""\ #!/bin/sh echo "i-hook-you-up" >> %s """ % canary_log)) os.chmod(canary_fname, 0o755) return hook_fname, canary_log def test_hook_install_user(self): # build/install the hook hook_name = "clicktesthook" hook_file, hook_log = self._make_hook(hook_name) self.addCleanup(os.unlink, hook_file) subprocess.check_call( [self.click_binary, "hook", "install", hook_name]) self.addCleanup( subprocess.check_call, [self.click_binary, "hook", "remove", hook_name]) # make click that uses the hook hooks = {'app1': {hook_name: 'README'}} click_pkg_name = "com.example.hook-1" click_pkg = self._make_click( click_pkg_name, framework="", hooks=hooks) user = os.environ.get("USER", "root") self.click_install(click_pkg, click_pkg_name, user) # ensure we have the hook generated_hook_file = os.path.expanduser( "~/com.example.hook-1_app1_1.0.test-hook") self.assertTrue(os.path.exists(generated_hook_file)) self.assertEqual( os.path.realpath(generated_hook_file), "/opt/click.ubuntu.com/com.example.hook-1/1.0/README") with open(hook_log) as f: hook_log_content = f.read().strip() self.assertEqual("i-hook-you-up", hook_log_content) click-0.5.0/click_package/tests/integration/test_info.py000066400000000000000000000050571402441472600234050ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click CLI info command.""" import json import os import subprocess from .helpers import ClickTestCase class TestInfo(ClickTestCase): def test_info_from_path(self): name = "com.example.foo" path_to_click = self._make_click(name) output = subprocess.check_output([ self.click_binary, "info", path_to_click], universal_newlines=True) self.assertEqual(name, json.loads(output)["name"]) def test_info_installed_click(self): name = "com.example.foo" user = os.environ.get("USER", "root") path_to_click = self._make_click(name, framework="") self.click_install(path_to_click, name, user) output = subprocess.check_output([ self.click_binary, "info", name], universal_newlines=True) self.assertEqual(json.loads(output)["name"], name) def test_info_file_in_package(self): name = "org.example.info" version = "1.0" click_pkg = self._make_click(name=name, version=version, framework="") subprocess.check_call( [self.click_binary, "install", "--allow-unauthenticated", "--all-users", click_pkg]) self.addCleanup( subprocess.check_call, [self.click_binary, "unregister", "--all-users", name]) output = subprocess.check_output( [self.click_binary, "info", "/opt/click.ubuntu.com/%s/%s/README" % (name, version)], universal_newlines=True) self.assertEqual(name, json.loads(output)["name"]) def test_info_different_extension(self): name = "org.example.info" raw_path = self._make_click(name) path = "%s.extra" % raw_path os.rename(raw_path, path) output = subprocess.check_output([ self.click_binary, "info", path], universal_newlines=True) self.assertEqual(name, json.loads(output)["name"]) click-0.5.0/click_package/tests/integration/test_install.py000066400000000000000000000105461402441472600241170ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click install feature.""" import subprocess from .helpers import ( require_root, ClickTestCase, ) def add_user(name): subprocess.check_call(["useradd", name]) return name def del_user(name): subprocess.check_call(["userdel", "-r", name]) class TestClickInstall(ClickTestCase): @classmethod def setUpClass(cls): super(TestClickInstall, cls).setUpClass() require_root() cls.USER_1 = add_user("click-test-user-1") cls.USER_2 = add_user("click-test-user-2") @classmethod def tearDownClass(cls): super(TestClickInstall, cls).tearDownClass() del_user(cls.USER_1) del_user(cls.USER_2) def test_install_for_single_user(self): name = "foo-1" click_pkg = self._make_click(name=name, framework="") # install it self.click_install(click_pkg, name, self.USER_1) # ensure that user-1 has it output = subprocess.check_output([ "sudo", "-u", self.USER_1, self.click_binary, "list"], universal_newlines=True) self.assertEqual(output, "%s\t1.0\n" % name) # but not user-2 output = subprocess.check_output([ "sudo", "-u", self.USER_2, self.click_binary, "list"], universal_newlines=True) self.assertEqual(output, "") # and that we can see it with the --user option output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.USER_1], universal_newlines=True) self.assertEqual(output, "%s\t1.0\n" % name) def test_install_for_single_user_and_register(self): name = "foo-1" click_pkg = self._make_click(name=name, framework="") self.click_install(click_pkg, name, self.USER_1) # not available for user2 output = subprocess.check_output([ "sudo", "-u", self.USER_2, self.click_binary, "list"], universal_newlines=True) self.assertEqual(output, "") # register it subprocess.check_call( [self.click_binary, "register", "--user=%s" % self.USER_2, name, "1.0", ]) self.addCleanup(self.click_unregister, name, self.USER_2) # and ensure its available for user2 output = subprocess.check_output([ "sudo", "-u", self.USER_2, self.click_binary, "list"], universal_newlines=True) self.assertEqual(output, "%s\t1.0\n" % name) def test_install_for_all_users(self): name = "foo-2" click_pkg = self._make_click(name=name, framework="") self.click_install(click_pkg, name, "@all") # ensure all users see it for user in (self.USER_1, self.USER_2): output = subprocess.check_output( ["sudo", "-u", user, self.click_binary, "list"], universal_newlines=True) self.assertEqual(output, "%s\t1.0\n" % name) def test_pkgdir_after_install(self): name = "foo-3" click_pkg = self._make_click(name=name, version="1.2", framework="") self.click_install(click_pkg, name, "@all") # from the path output = subprocess.check_output( [self.click_binary, "pkgdir", "/opt/click.ubuntu.com/%s/1.2/README" % name], universal_newlines=True).strip() self.assertEqual(output, "/opt/click.ubuntu.com/%s/1.2" % name) # now test from the click package name output = subprocess.check_output( [self.click_binary, "pkgdir", name], universal_newlines=True).strip() # note that this is different from above self.assertEqual( output, "/opt/click.ubuntu.com/.click/users/@all/%s" % name) click-0.5.0/click_package/tests/integration/test_list.py000066400000000000000000000023221402441472600234150ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click CLI list command.""" import os import subprocess from .helpers import ClickTestCase class TestList(ClickTestCase): def test_list_simple(self): name = "com.ubuntu.verify-ok" path_to_click = self._make_click(name, framework="") user = os.environ.get("USER", "root") self.click_install(path_to_click, name, user) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % user], universal_newlines=True) self.assertIn(name, output) click-0.5.0/click_package/tests/integration/test_signatures.py000066400000000000000000000400231402441472600246260ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click signature checking.""" import copy import os import shutil import subprocess import tarfile from textwrap import dedent import apt from click_package import osextras from .helpers import ( require_root, ClickTestCase, ) def makedirs(path): try: os.makedirs(path) except OSError: pass def get_keyid_from_gpghome(gpg_home): """Return the public keyid of a given gpg home dir""" output = subprocess.check_output( ["gpg", "--home", gpg_home, "--list-keys", "--with-colons"], universal_newlines=True) for line in output.splitlines(): if not line.startswith("pub:"): continue return line.split(":")[4] raise ValueError("Cannot find public key in output: '%s'" % output) class Debsigs: """Tiny wrapper around the debsigs CLI""" def __init__(self, gpghome, keyid): self.keyid = keyid self.gpghome = gpghome self.policy = "/etc/debsig/policies/%s/generic.pol" % self.keyid def sign(self, filepath, signature_type="origin"): """Sign the click at filepath""" env = copy.copy(os.environ) env["GNUPGHOME"] = os.path.abspath(self.gpghome) try: subprocess.check_call( ["debsigs", "--sign=%s" % signature_type, "--default-key=%s" % self.keyid, filepath], env=env) finally: if osextras.find_on_path("gpgconf"): subprocess.call(["gpgconf", "--kill", "gpg-agent"]) def install_signature_policy(self): """Install/update the system-wide signature policy""" if apt.Cache()["debsig-verify"].installed >= "0.15": debsig_xmlns = "https://www.debian.org/debsig/1.0/" else: debsig_xmlns = "http://www.debian.org/debsig/1.0/" xmls = dedent("""\ """.format( debsig_xmlns=debsig_xmlns, keyid=self.keyid, filename="origin.pub")) makedirs(os.path.dirname(self.policy)) with open(self.policy, "w") as f: f.write(xmls) self.pubkey_path = ( "/usr/share/debsig/keyrings/%s/origin.pub" % self.keyid) makedirs(os.path.dirname(self.pubkey_path)) shutil.copy( os.path.join(self.gpghome, "pubring.gpg"), self.pubkey_path) def uninstall_signature_policy(self): # FIXME: update debsig-verify so that it can work from a different # root than "/" so that the tests do not have to use the # system root os.remove(self.policy) os.remove(self.pubkey_path) class ClickSignaturesTestCase(ClickTestCase): @classmethod def setUpClass(cls): super(ClickSignaturesTestCase, cls).setUpClass() require_root() def assertClickNoSignatureError(self, cmd_args): with self.assertRaises(subprocess.CalledProcessError) as cm: output = subprocess.check_output( [self.click_binary] + cmd_args, stderr=subprocess.STDOUT, universal_newlines=True) output = cm.exception.output expected_error_message = ("debsig: Origin Signature check failed. " "This deb might not be signed.") self.assertIn(expected_error_message, output) def assertClickInvalidSignatureError(self, cmd_args): with self.assertRaises(subprocess.CalledProcessError) as cm: output = subprocess.check_output( [self.click_binary] + cmd_args, stderr=subprocess.STDOUT, universal_newlines=True) print(output) output = cm.exception.output expected_error_message = "Signature verification error: " self.assertIn(expected_error_message, output) class TestSignatureVerificationNoSignature(ClickSignaturesTestCase): @classmethod def setUpClass(cls): super(TestSignatureVerificationNoSignature, cls).setUpClass() require_root() def test_debsig_verify_no_sig(self): name = "org.example.debsig-no-sig" path_to_click = self._make_click(name, framework="") self.assertClickNoSignatureError(["verify", path_to_click]) def test_debsig_install_no_sig(self): name = "org.example.debsig-no-sig" path_to_click = self._make_click(name, framework="") self.assertClickNoSignatureError(["install", path_to_click]) def test_debsig_install_can_install_with_sig_override(self): name = "org.example.debsig-no-sig" path_to_click = self._make_click(name, framework="") user = os.environ.get("USER", "root") subprocess.check_call( [self.click_binary, "install", "--allow-unauthenticated", "--user=%s" % user, path_to_click]) self.addCleanup( subprocess.call, [self.click_binary, "unregister", "--user=%s" % user, name]) class TestSignatureVerification(ClickSignaturesTestCase): @classmethod def setUpClass(cls): super(TestSignatureVerification, cls).setUpClass() require_root() def setUp(self): super(TestSignatureVerification, self).setUp() self.user = os.environ.get("USER", "root") # the valid origin keyring self.datadir = os.path.join(os.path.dirname(__file__), "data") origin_keyring_dir = os.path.abspath( os.path.join(self.datadir, "origin-keyring")) gpghome = self.make_gpghome(origin_keyring_dir) keyid = get_keyid_from_gpghome(gpghome) self.debsigs = Debsigs(gpghome, keyid) self.debsigs.install_signature_policy() def tearDown(self): self.debsigs.uninstall_signature_policy() def make_gpghome(self, source): gpghome = os.path.join(self.temp_dir, "gnupg") if os.path.exists(gpghome): shutil.rmtree(gpghome) shutil.copytree(source, gpghome) os.chmod(gpghome, 0o700) return gpghome def test_debsig_install_valid_signature(self): name = "org.example.debsig-valid-sig" path_to_click = self._make_click(name, framework="") self.debsigs.sign(path_to_click) subprocess.call(["cp", path_to_click, os.path.join("/home/cjwatson/src/ubuntu/click/click", os.path.basename(path_to_click))]) subprocess.check_call( [self.click_binary, "install", "--user=%s" % self.user, path_to_click]) self.addCleanup( subprocess.call, [self.click_binary, "unregister", "--user=%s" % self.user, name]) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.user], universal_newlines=True) self.assertIn(name, output) def test_debsig_install_signature_not_in_keyring(self): name = "org.example.debsig-no-keyring-sig" path_to_click = self._make_click(name, framework="") evil_keyring_dir = os.path.join(self.datadir, "evil-keyring") gpghome = self.make_gpghome(evil_keyring_dir) keyid = get_keyid_from_gpghome(gpghome) debsig_bad = Debsigs(gpghome, keyid) debsig_bad.sign(path_to_click) # and ensure its really not there self.assertClickInvalidSignatureError(["install", path_to_click]) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.user], universal_newlines=True) self.assertNotIn(name, output) def test_debsig_install_not_a_signature(self): name = "org.example.debsig-invalid-sig" path_to_click = self._make_click(name, framework="") invalid_sig = os.path.join(self.temp_dir, "_gpgorigin") with open(invalid_sig, "w") as f: f.write("no-valid-signature") # add a invalid sig subprocess.check_call(["ar", "-r", path_to_click, invalid_sig]) self.assertClickInvalidSignatureError(["install", path_to_click]) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.user], universal_newlines=True) self.assertNotIn(name, output) def test_debsig_install_signature_altered_click(self): def modify_ar_member(member): subprocess.check_call( ["ar", "-x", path_to_click, "control.tar.gz"], cwd=self.temp_dir) altered_member = os.path.join(self.temp_dir, member) with open(altered_member, "ba") as f: f.write(b"\0") subprocess.check_call(["ar", "-r", path_to_click, altered_member]) # ensure that all members we care about are checked by debsig-verify for member in ["control.tar.gz", "data.tar.gz", "debian-binary"]: name = "org.example.debsig-altered-click" path_to_click = self._make_click(name, framework="") self.debsigs.sign(path_to_click) modify_ar_member(member) self.assertClickInvalidSignatureError(["install", path_to_click]) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.user], universal_newlines=True) self.assertNotIn(name, output) def make_nasty_data_tar(self, compression): new_data_tar = os.path.join(self.temp_dir, "data.tar." + compression) evilfile = os.path.join(self.temp_dir, "README.evil") with open(evilfile, "w") as f: f.write("I am a nasty README") with tarfile.open(new_data_tar, "w:"+compression) as tar: tar.add(evilfile) return new_data_tar def test_debsig_install_signature_injected_data_tar(self): name = "org.example.debsig-injected-data-click" path_to_click = self._make_click(name, framework="") self.debsigs.sign(path_to_click) new_data = self.make_nasty_data_tar("bz2") # insert before the real data.tar.gz and ensure this is caught # NOTE: that right now this will not be caught by debsig-verify # but later in audit() by debian.debfile.DebFile() subprocess.check_call(["ar", "-r", "-b", "data.tar.gz", path_to_click, new_data]) output = subprocess.check_output( ["ar", "-t", path_to_click], universal_newlines=True) self.assertEqual(output.splitlines(), ["debian-binary", "_click-binary", "control.tar.gz", "data.tar.bz2", "data.tar.gz", "_gpgorigin"]) with self.assertRaises(subprocess.CalledProcessError): output = subprocess.check_output( [self.click_binary, "install", path_to_click], stderr=subprocess.STDOUT, universal_newlines=True) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.user], universal_newlines=True) self.assertNotIn(name, output) def test_debsig_install_signature_replaced_data_tar(self): name = "org.example.debsig-replaced-data-click" path_to_click = self._make_click(name, framework="") self.debsigs.sign(path_to_click) new_data = self.make_nasty_data_tar("bz2") # replace data.tar.gz with data.tar.bz2 and ensure this is caught subprocess.check_call(["ar", "-d", path_to_click, "data.tar.gz", ]) subprocess.check_call(["ar", "-r", path_to_click, new_data]) output = subprocess.check_output( ["ar", "-t", path_to_click], universal_newlines=True) self.assertEqual(output.splitlines(), ["debian-binary", "_click-binary", "control.tar.gz", "_gpgorigin", "data.tar.bz2", ]) with self.assertRaises(subprocess.CalledProcessError) as cm: output = subprocess.check_output( [self.click_binary, "install", path_to_click], stderr=subprocess.STDOUT, universal_newlines=True) self.assertIn("Signature verification error", cm.exception.output) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.user], universal_newlines=True) self.assertNotIn(name, output) def test_debsig_install_signature_prepend_sig(self): # this test is probably not really needed, it tries to trick # the system by prepending a valid signature that is not # in the keyring. But given that debsig-verify only reads # the first packet of any given _gpg$foo signature it's # equivalent to test_debsig_install_signature_not_in_keyring test name = "org.example.debsig-replaced-data-prepend-sig-click" path_to_click = self._make_click(name, framework="") self.debsigs.sign(path_to_click) new_data = self.make_nasty_data_tar("gz") # replace data.tar.gz subprocess.check_call(["ar", "-r", path_to_click, new_data, ]) # get previous good _gpgorigin for the old data subprocess.check_call( ["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir) with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f: good_gpg_origin = f.read() # and append a valid signature from a non-keyring key evil_keyring_dir = os.path.join(self.datadir, "evil-keyring") gpghome = self.make_gpghome(evil_keyring_dir) debsig_bad = Debsigs(gpghome, "18B38B9AC1B67A0D") debsig_bad.sign(path_to_click) subprocess.check_call( ["ar", "-x", path_to_click, "_gpgorigin"], cwd=self.temp_dir) with open(os.path.join(self.temp_dir, "_gpgorigin"), "br") as f: evil_gpg_origin = f.read() with open(os.path.join(self.temp_dir, "_gpgorigin"), "wb") as f: f.write(evil_gpg_origin) f.write(good_gpg_origin) subprocess.check_call( ["ar", "-r", path_to_click, "_gpgorigin"], cwd=self.temp_dir) # now ensure that the verification fails as well with self.assertRaises(subprocess.CalledProcessError) as cm: output = subprocess.check_output( [self.click_binary, "install", path_to_click], stderr=subprocess.STDOUT, universal_newlines=True) self.assertIn("Signature verification error", cm.exception.output) output = subprocess.check_output( [self.click_binary, "list", "--user=%s" % self.user], universal_newlines=True) self.assertNotIn(name, output) click-0.5.0/click_package/tests/integration/test_verify.py000066400000000000000000000061021402441472600237460ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Integration tests for the click CLI verify command.""" import os import subprocess from .helpers import ClickTestCase class TestVerify(ClickTestCase): def test_verify_force_missing_framework_ok(self): name = "com.example.verify-missing-framework" path_to_click = self._make_click(name) output = subprocess.check_output([ self.click_binary, "verify", "--force-missing-framework", "--allow-unauthenticated", path_to_click], universal_newlines=True) self.assertEqual(output, "") def test_verify_force_ok(self): name = "com.example.verify-ok" path_to_click = self._make_click(name, framework="") output = subprocess.check_output([ self.click_binary, "verify", "--allow-unauthenticated", path_to_click], universal_newlines=True) self.assertEqual(output, "") def test_verify_missing_framework(self): name = "com.example.verify-really-missing-framework" path_to_click = self._make_click(name, framework="missing") with self.assertRaises(subprocess.CalledProcessError) as cm: subprocess.check_output( [self.click_binary, "verify", "--allow-unauthenticated", path_to_click], universal_newlines=True, stderr=subprocess.STDOUT) expected_error = ( 'click_package.framework.ClickFrameworkInvalid: Framework ' '"missing" not present on system (use ' '--force-missing-framework option to override)') self.assertIn(expected_error, cm.exception.output) def test_verify_no_click_but_invalid(self): name = "com.example.verify-no-click" path_to_click = os.path.join(self.temp_dir, name+".click") with open(path_to_click, "w") as f: f.write("something-that-is-not-a-click") with self.assertRaises(subprocess.CalledProcessError) as cm: subprocess.check_output( [self.click_binary, "verify", "--allow-unauthenticated", path_to_click], universal_newlines=True, stderr=subprocess.STDOUT) expected_error = ( 'click_package.install.DebsigVerifyError: ' 'Signature verification error: ' 'debsig: %s does not appear to be a deb format package' ) % path_to_click self.assertIn(expected_error, cm.exception.output) click-0.5.0/click_package/tests/preload.h000066400000000000000000000054611402441472600203140ustar00rootroot00000000000000#include #include #include #include "click.h" /** * chown: (attributes headers=unistd.h) */ extern int chown (const char *file, uid_t owner, gid_t group); /** * geteuid: (attributes headers=sys/types.h,unistd.h) */ extern uid_t geteuid (void); /* Workaround for g-ir-scanner not picking up the type properly: mode_t is * uint32_t on all glibc platforms. */ /** * mkdir: (attributes headers=sys/stat.h,sys/types.h) * @mode: (type guint32) */ extern int mkdir (const char *pathname, mode_t mode); /** * getpwnam: (attributes headers=sys/types.h,pwd.h) * * Returns: (transfer none): */ extern struct passwd *getpwnam (const char *name); /** * under_under_xstat: (attributes headers=sys/types.h,sys/stat.h,unistd.h) */ extern int under_under_xstat (int ver, const char *pathname, struct stat *buf); /** * under_under_xstat64: (Attributes headers=sys/types.h,sys/stat.h,unistd.h) */ extern int under_under_xstat64 (int ver, const char *pathname, struct stat64 *buf); const gchar *g_get_user_name (void); /** * g_spawn_sync: (attributes headers=glib.h) * @argv: (array zero-terminated=1): * @envp: (array zero-terminated=1): * @flags: (type gint) * @child_setup: (type gpointer) * @standard_output: (out) (array zero-terminated=1) (element-type guint8): * @standard_error: (out) (array zero-terminated=1) (element-type guint8): * @exit_status: (out): */ gboolean g_spawn_sync (const gchar *working_directory, gchar **argv, gchar **envp, GSpawnFlags flags, GSpawnChildSetupFunc child_setup, gpointer user_data, gchar **standard_output, gchar **standard_error, gint *exit_status, GError **error); /** * click_find_on_path: (attributes headers=glib.h) */ gboolean click_find_on_path (const gchar *command); /** * click_get_db_dir: (attributes headers=glib.h) */ gchar *click_get_db_dir (void); /** * click_get_frameworks_dir: (attributes headers=glib.h) */ gchar *click_get_frameworks_dir (void); /** * click_get_hooks_dir: (attributes headers=glib.h) */ gchar *click_get_hooks_dir (void); /** * click_get_user_home: (attributes headers=glib.h) */ gchar *click_get_user_home (const gchar *user_name); /** * click_package_install_hooks: (attributes headers=glib.h,click.h) * @db: (type gpointer) */ void click_package_install_hooks (ClickDB *db, const gchar *package, const gchar *old_version, const gchar *new_version, const gchar *user_name, GError **error); click-0.5.0/click_package/tests/test_arfile.py000066400000000000000000000063641402441472600213730ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.arfile.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestArFile', ] import os import subprocess from click_package.arfile import ArFile from click_package.tests.helpers import TestCase, touch class TestArFile(TestCase): def setUp(self): super(TestArFile, self).setUp() self.use_temp_dir() def test_init_rejects_mode_r(self): self.assertRaises(ValueError, ArFile, mode="r") def test_init_name(self): path = os.path.join(self.temp_dir, "foo.a") with ArFile(name=path, mode="w") as arfile: self.assertEqual("w", arfile.mode) self.assertEqual("wb", arfile.real_mode) self.assertEqual(path, arfile.name) self.assertEqual(path, arfile.fileobj.name) self.assertTrue(arfile.opened_fileobj) self.assertFalse(arfile.closed) def test_init_rejects_readonly_fileobj(self): path = os.path.join(self.temp_dir, "foo.a") touch(path) with open(path, "rb") as fileobj: self.assertRaises(ValueError, ArFile, fileobj=fileobj) def test_init_fileobj(self): path = os.path.join(self.temp_dir, "foo.a") with open(path, "wb") as fileobj: arfile = ArFile(fileobj=fileobj) self.assertEqual("w", arfile.mode) self.assertEqual("wb", arfile.real_mode) self.assertEqual(path, arfile.name) self.assertEqual(fileobj, arfile.fileobj) self.assertFalse(arfile.opened_fileobj) self.assertFalse(arfile.closed) def test_writes_valid_ar_file(self): member_path = os.path.join(self.temp_dir, "member") with open(member_path, "wb") as member: member.write(b"\x00\x01\x02\x03\x04\x05\x06\x07") path = os.path.join(self.temp_dir, "foo.a") with ArFile(name=path, mode="w") as arfile: arfile.add_magic() arfile.add_data("data-member", b"some data") arfile.add_file("file-member", member_path) extract_path = os.path.join(self.temp_dir, "extract") os.mkdir(extract_path) subprocess.call(["ar", "x", path], cwd=extract_path) self.assertCountEqual( ["data-member", "file-member"], os.listdir(extract_path)) with open(os.path.join(extract_path, "data-member"), "rb") as member: self.assertEqual(b"some data", member.read()) with open(os.path.join(extract_path, "file-member"), "rb") as member: self.assertEqual( b"\x00\x01\x02\x03\x04\x05\x06\x07", member.read()) click-0.5.0/click_package/tests/test_build.py000066400000000000000000000340261402441472600212240ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.build.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickBuilder', 'TestClickSourceBuilder', ] import json import os import stat import subprocess import tarfile from textwrap import dedent from click_package.build import ClickBuildError, ClickBuilder, ClickSourceBuilder from click_package.preinst import static_preinst from click_package.tests.helpers import ( disable_logging, mkfile, TestCase, touch, ) # BAW 2013-04-15: Some tests require umask 022. Use this decorator to # temporarily tweak the process's umask. The test -- or system -- should # probably be made more robust instead. def umask(force_umask): def decorator(func): def wrapper(*args, **kws): old_umask = os.umask(force_umask) try: return func(*args, **kws) finally: os.umask(old_umask) return wrapper return decorator class TestClickBuilderBaseMixin: def test_read_manifest(self): self.use_temp_dir() manifest_path = os.path.join(self.temp_dir, "manifest.json") with mkfile(manifest_path) as manifest: print(dedent("""\ { "name": "com.example.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "framework": "ubuntu-sdk-13.10" }"""), file=manifest) self.builder.read_manifest(manifest_path) self.assertEqual("com.example.test", self.builder.name) self.assertEqual("1.0", self.builder.version) self.assertEqual("Foo Bar ", self.builder.maintainer) self.assertEqual("test title", self.builder.title) self.assertEqual("all", self.builder.architecture) def test_add_file(self): self.builder.add_file("/nonexistent", "target") self.assertEqual({"/nonexistent": "target"}, self.builder.file_map) def test_epochless_version(self): self.use_temp_dir() manifest_path = os.path.join(self.temp_dir, "manifest.json") for version, epochless_version in ( ("1.0", "1.0"), ("1:1.2.3", "1.2.3"), ): with mkfile(manifest_path) as manifest: print(dedent("""\ { "name": "com.example.test", "version": "%s", "maintainer": "Foo Bar ", "title": "test title", "framework": "ubuntu-sdk-13.10" }""") % version, file=manifest) self.builder.read_manifest(manifest_path) self.assertEqual(epochless_version, self.builder.epochless_version) def test_manifest_syntax_error(self): self.use_temp_dir() manifest_path = os.path.join(self.temp_dir, "manifest.json") with mkfile(manifest_path) as manifest: # The comma after the "name" entry is intentionally missing. print(dedent("""\ { "name": "com.example.test" "version": "1.0" }"""), file=manifest) self.assertRaises( ClickBuildError, self.builder.read_manifest, manifest_path) class TestClickBuilder(TestCase, TestClickBuilderBaseMixin): def setUp(self): super(TestClickBuilder, self).setUp() self.builder = ClickBuilder() def extract_field(self, path, name): return subprocess.check_output( ["dpkg-deb", "-f", path, name], universal_newlines=True).rstrip("\n") @disable_logging @umask(0o22) def test_build(self): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") with mkfile(os.path.join(scratch, "bin", "foo")) as f: f.write("test /bin/foo\n") os.symlink("foo", os.path.join(scratch, "bin", "bar")) touch(os.path.join(scratch, ".git", "config")) with mkfile(os.path.join(scratch, "toplevel")) as f: f.write("test /toplevel\n") os.symlink( "file-does-not-exist", os.path.join(scratch, "broken-symlink")) with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump({ "name": "com.example.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": "all", "framework": "ubuntu-sdk-13.10", }, f) # build() overrides this back to 0o644 os.fchmod(f.fileno(), 0o600) self.builder.add_file(scratch, "/") path = os.path.join(self.temp_dir, "com.example.test_1.0_all.click") self.assertEqual(path, self.builder.build(self.temp_dir)) self.assertTrue(os.path.exists(path)) for key, value in ( ("Package", "com.example.test"), ("Version", "1.0"), ("Click-Version", "0.4"), ("Architecture", "all"), ("Maintainer", "Foo Bar "), ("Description", "test title"), ): self.assertEqual(value, self.extract_field(path, key)) self.assertNotEqual( "", self.extract_field(path, "Installed-Size")) control_path = os.path.join(self.temp_dir, "control") subprocess.check_call(["dpkg-deb", "-e", path, control_path]) manifest_path = os.path.join(control_path, "manifest") self.assertEqual(0o644, stat.S_IMODE(os.stat(manifest_path).st_mode)) with open(os.path.join(scratch, "manifest.json")) as source, \ open(manifest_path) as target: source_json = json.load(source) target_json = json.load(target) self.assertNotEqual("", target_json["installed-size"]) del target_json["installed-size"] self.assertEqual(source_json, target_json) with open(os.path.join(control_path, "md5sums")) as md5sums: self.assertRegex( md5sums.read(), r"^" r"eb774c3ead632b397d6450d1df25e001 bin/bar\n" r"eb774c3ead632b397d6450d1df25e001 bin/foo\n" r"49327ce6306df8a87522456b14a179e0 toplevel\n" r"$") with open(os.path.join(control_path, "preinst")) as preinst: self.assertEqual(static_preinst, preinst.read()) contents = subprocess.check_output( ["dpkg-deb", "-c", path], universal_newlines=True) self.assertRegex(contents, r"^drwxr-xr-x root/root 0 .* \./\n") self.assertRegex( contents, "\nlrwxrwxrwx root/root 0 .* \./bin/bar -> foo\n") self.assertRegex( contents, "\n-rw-r--r-- root/root 14 .* \./bin/foo\n") self.assertRegex( contents, "\n-rw-r--r-- root/root 15 .* \./toplevel\n") extract_path = os.path.join(self.temp_dir, "extract") subprocess.check_call(["dpkg-deb", "-x", path, extract_path]) for rel_path in ( os.path.join("bin", "foo"), "toplevel", ): with open(os.path.join(scratch, rel_path)) as source, \ open(os.path.join(extract_path, rel_path)) as target: self.assertEqual(source.read(), target.read()) self.assertTrue( os.path.islink(os.path.join(extract_path, "bin", "bar"))) self.assertEqual( "foo", os.readlink(os.path.join(extract_path, "bin", "bar"))) def _make_scratch_dir(self, manifest_override={}): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") manifest = { "name": "com.example.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": "all", "framework": "ubuntu-sdk-13.10", } manifest.update(manifest_override) with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump(manifest, f) self.builder.add_file(scratch, "/") return scratch @disable_logging def test_build_excludes_dot_click(self): scratch = self._make_scratch_dir() touch(os.path.join(scratch, ".click", "evil-file")) path = self.builder.build(self.temp_dir) extract_path = os.path.join(self.temp_dir, "extract") subprocess.check_call(["dpkg-deb", "-x", path, extract_path]) self.assertEqual([], os.listdir(extract_path)) def test_build_ignore_pattern(self): scratch = self._make_scratch_dir() touch(os.path.join(scratch, "build", "foo.o")) self.builder.add_file(scratch, "/") self.builder.add_ignore_pattern("build") path = self.builder.build(self.temp_dir) extract_path = os.path.join(self.temp_dir, "extract") subprocess.check_call(["dpkg-deb", "-x", path, extract_path]) self.assertEqual([], os.listdir(extract_path)) @disable_logging def test_build_multiple_architectures(self): scratch = self._make_scratch_dir(manifest_override={ "architecture": ["armhf", "i386"], }) path = os.path.join(self.temp_dir, "com.example.test_1.0_multi.click") self.assertEqual(path, self.builder.build(self.temp_dir)) self.assertTrue(os.path.exists(path)) self.assertEqual("multi", self.extract_field(path, "Architecture")) control_path = os.path.join(self.temp_dir, "control") subprocess.check_call(["dpkg-deb", "-e", path, control_path]) manifest_path = os.path.join(control_path, "manifest") with open(os.path.join(scratch, "manifest.json")) as source, \ open(manifest_path) as target: source_json = json.load(source) target_json = json.load(target) del target_json["installed-size"] self.assertEqual(source_json, target_json) @disable_logging def test_build_multiple_frameworks(self): scratch = self._make_scratch_dir(manifest_override={ "framework": "ubuntu-sdk-14.04-basic, ubuntu-sdk-14.04-webapps", }) path = self.builder.build(self.temp_dir) control_path = os.path.join(self.temp_dir, "control") subprocess.check_call(["dpkg-deb", "-e", path, control_path]) manifest_path = os.path.join(control_path, "manifest") with open(os.path.join(scratch, "manifest.json")) as source, \ open(manifest_path) as target: source_json = json.load(source) target_json = json.load(target) del target_json["installed-size"] self.assertEqual(source_json, target_json) class TestClickFrameworkValidation(TestCase): def setUp(self): super(TestClickFrameworkValidation, self).setUp() self.builder = ClickBuilder() for framework_name in ("ubuntu-sdk-13.10", "ubuntu-sdk-14.04-papi", "ubuntu-sdk-14.04-html", "docker-sdk-1.3"): self._create_mock_framework_file(framework_name) def test_validate_framework_good(self): valid_framework_values = ( "ubuntu-sdk-13.10", "ubuntu-sdk-14.04-papi, ubuntu-sdk-14.04-html", "ubuntu-sdk-13.10, docker-sdk-1.3", ) for framework in valid_framework_values: self.builder._validate_framework(framework) def test_validate_framework_bad(self): invalid_framework_values = ( "ubuntu-sdk-13.10, ubuntu-sdk-14.04-papi", "ubuntu-sdk-13.10 (>= 13.10)", "ubuntu-sdk-13.10 | ubuntu-sdk-14.04", ) for framework in invalid_framework_values: with self.assertRaises(ClickBuildError): self.builder._validate_framework(framework) class TestClickSourceBuilder(TestCase, TestClickBuilderBaseMixin): def setUp(self): super(TestClickSourceBuilder, self).setUp() self.builder = ClickSourceBuilder() @umask(0o22) def test_build(self): self.use_temp_dir() scratch = os.path.join(self.temp_dir, "scratch") touch(os.path.join(scratch, "bin", "foo")) touch(os.path.join(scratch, ".git", "config")) touch(os.path.join(scratch, "foo.so")) touch(os.path.join(scratch, "build", "meep.goah")) with mkfile(os.path.join(scratch, "manifest.json")) as f: json.dump({ "name": "com.example.test", "version": "1.0", "maintainer": "Foo Bar ", "title": "test title", "architecture": "all", "framework": "ubuntu-sdk-13.10", }, f) # build() overrides this back to 0o644 os.fchmod(f.fileno(), 0o600) self.builder.add_file(scratch, "./") self.builder.add_ignore_pattern("build") path = os.path.join(self.temp_dir, "com.example.test_1.0.tar.gz") self.assertEqual(path, self.builder.build(self.temp_dir)) self.assertTrue(os.path.exists(path)) with tarfile.open(path, mode="r:gz") as tar: self.assertCountEqual( [".", "./bin", "./bin/foo", "./manifest.json"], tar.getnames()) self.assertTrue(tar.getmember("./bin/foo").isfile()) click-0.5.0/click_package/tests/test_chroot.py000066400000000000000000000367401402441472600214300ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Michael Vogt # 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; version 3 of the License. # # 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 . """Unit tests for click_package.chroot.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickChroot', ] import os import re from textwrap import dedent from click_package.chroot import ( ClickChroot, generate_sources, strip_dev_series_from_framework, ) from click_package.tests.helpers import TestCase, mock class FakeClickChroot(ClickChroot): def __init__(self, *args, **kwargs): self.temp_dir = kwargs.pop("temp_dir") super(FakeClickChroot, self).__init__(*args, **kwargs) self._exists = False def exists(self): return self._exists def maint(self, *args, **kwargs): self._maint_args = args self._maint_kwargs = kwargs return 0 def _debootstrap(self, components, mount, archive_mirror, ports_mirror): os.makedirs(os.path.join(mount, "etc", "apt")) os.makedirs(os.path.join(mount, "usr", "sbin")) os.makedirs(os.path.join(mount, "sbin")) with open(os.path.join(mount, "sbin", "initctl"), "w"): pass self._exists = True @property def chroot_config(self): p = self.temp_dir + super(FakeClickChroot, self).chroot_config if not os.path.exists(os.path.dirname(p)): os.makedirs(os.path.dirname(p)) return p class TestClickChroot(TestCase): def set_dpkg_native_architecture(self, arch): """Fool dpkg-architecture into selecting a given native arch.""" self.use_temp_dir() dpkg_script_path = os.path.join(self.temp_dir, "dpkg") with open(dpkg_script_path, "w") as dpkg_script: print(dedent("""\ #! /bin/sh echo %s """) % arch, file=dpkg_script) os.chmod(dpkg_script_path, 0o755) os.environ["PATH"] = "%s:%s" % (self.temp_dir, os.environ["PATH"]) def test_get_native_arch_amd64_to_amd64(self): chroot = ClickChroot("amd64", "ubuntu-sdk-14.04", series="trusty") self.assertEqual("amd64", chroot._get_native_arch("amd64", "amd64")) def test_get_native_arch_amd64_to_armhf(self): chroot = ClickChroot("armhf", "ubuntu-sdk-14.04", series="trusty") self.assertEqual("amd64", chroot._get_native_arch("amd64", "armhf")) def test_get_native_arch_amd64_to_i386(self): chroot = ClickChroot("i386", "ubuntu-sdk-14.04", series="trusty") self.assertEqual("i386", chroot._get_native_arch("amd64", "i386")) def test_dpkg_architecture_amd64_to_armhf(self): self.set_dpkg_native_architecture("amd64") chroot = ClickChroot("armhf", "ubuntu-sdk-14.04", series="trusty") self.assertEqual("amd64", chroot.dpkg_architecture["DEB_BUILD_ARCH"]) self.assertEqual("armhf", chroot.dpkg_architecture["DEB_HOST_ARCH"]) def test_dpkg_architecture_i386_to_armhf(self): self.set_dpkg_native_architecture("i386") chroot = ClickChroot("armhf", "ubuntu-sdk-14.04", series="trusty") self.assertEqual("i386", chroot.dpkg_architecture["DEB_BUILD_ARCH"]) self.assertEqual("armhf", chroot.dpkg_architecture["DEB_HOST_ARCH"]) def test_dpkg_architecture_amd64_to_i386(self): self.set_dpkg_native_architecture("amd64") chroot = ClickChroot("i386", "ubuntu-sdk-14.04", series="trusty") self.assertEqual("i386", chroot.dpkg_architecture["DEB_BUILD_ARCH"]) self.assertEqual("i386", chroot.dpkg_architecture["DEB_HOST_ARCH"]) def test_gen_sources_archive_only(self): chroot = ClickChroot("amd64", "ubuntu-sdk-13.10", series="trusty") chroot.native_arch = "i386" sources = generate_sources( chroot.series, chroot.native_arch, chroot.target_arch, "http://archive.ubuntu.com/ubuntu", "http://ports.ubuntu.com/ubuntu-ports", "main") self.assertEqual([ 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu trusty main', 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu trusty-security main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty-security main', 'deb-src http://archive.ubuntu.com/ubuntu trusty main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-security main', ], sources) def test_gen_sources_mixed_archive_ports(self): chroot = ClickChroot("armhf", "ubuntu-sdk-13.10", series="trusty") chroot.native_arch = "i386" sources = generate_sources( chroot.series, chroot.native_arch, chroot.target_arch, "http://archive.ubuntu.com/ubuntu", "http://ports.ubuntu.com/ubuntu-ports", "main") self.assertEqual([ 'deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty main', 'deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-updates main', 'deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-security main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty-security main', 'deb-src http://archive.ubuntu.com/ubuntu trusty main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-security main', ], sources) def test_gen_sources_ports_only(self): chroot = ClickChroot("armhf", "ubuntu-sdk-13.10", series="trusty") chroot.native_arch = "armel" sources = generate_sources( chroot.series, chroot.native_arch, chroot.target_arch, "http://archive.ubuntu.com/ubuntu", "http://ports.ubuntu.com/ubuntu-ports", "main") self.assertEqual([ 'deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty main', 'deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-updates main', 'deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports trusty-security main', 'deb [arch=armel] http://ports.ubuntu.com/ubuntu-ports trusty main', 'deb [arch=armel] http://ports.ubuntu.com/ubuntu-ports trusty-updates main', 'deb [arch=armel] http://ports.ubuntu.com/ubuntu-ports trusty-security main', 'deb-src http://archive.ubuntu.com/ubuntu trusty main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-security main', ], sources) def test_gen_sources_native(self): chroot = ClickChroot("i386", "ubuntu-sdk-14.04", series="trusty") chroot.native_arch = "i386" sources = generate_sources( chroot.series, chroot.native_arch, chroot.target_arch, "http://archive.ubuntu.com/ubuntu", "http://ports.ubuntu.com/ubuntu-ports", "main") self.assertEqual([ 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb [arch=i386] http://archive.ubuntu.com/ubuntu trusty-security main', 'deb-src http://archive.ubuntu.com/ubuntu trusty main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-updates main', 'deb-src http://archive.ubuntu.com/ubuntu trusty-security main', ], sources) def test_make_cross_package_native(self): chroot = ClickChroot("amd64", "ubuntu-sdk-14.04", series="trusty") chroot.native_arch = "amd64" self.assertEqual("g++", chroot._make_cross_package("g++")) def test_make_cross_package_cross(self): chroot = ClickChroot("armhf", "ubuntu-sdk-14.04", series="trusty") chroot.native_arch = "amd64" self.assertEqual( "g++-arm-linux-gnueabihf", chroot._make_cross_package("g++")) def test_framework_base_base(self): chroot = ClickChroot("i386", "ubuntu-sdk-14.04-papi") self.assertEqual(chroot.framework_base, "ubuntu-sdk-14.04") def test_framework_base_series(self): chroot = ClickChroot("i386", "ubuntu-sdk-14.04") self.assertEqual(chroot.framework_base, "ubuntu-sdk-14.04") def test_chroot_series(self): chroot = ClickChroot("i386", "ubuntu-sdk-14.04") self.assertEqual(chroot.series, "trusty") def test_chroot_full_name(self): chroot = ClickChroot("i386", "ubuntu-sdk-14.04") self.assertEqual(chroot.full_name, "click-ubuntu-sdk-14.04-i386") def test_chroot_generate_daemon_config(self): self.use_temp_dir() chroot = ClickChroot("i386", "ubuntu-sdk-14.04") os.makedirs(os.path.join(self.temp_dir, "usr", "sbin")) daemon_policy = chroot._generate_daemon_policy(self.temp_dir) with open(daemon_policy) as f: self.assertEqual(f.read(), chroot.DAEMON_POLICY) def test_chroot_generate_finish_script(self): self.use_temp_dir() chroot = ClickChroot("i386", "ubuntu-sdk-14.04") finish_script = chroot._generate_finish_script( self.temp_dir, ["build-pkg-1", "build-pkg-2"]) with open(finish_script) as f: self.assertEqual(f.read(), dedent("""\ #!/bin/bash set -e # Configure target arch dpkg --add-architecture i386 # Reload package lists apt-get update || true # Pull down signature requirements apt-get -y --force-yes install gnupg ubuntu-keyring # Reload package lists apt-get update || true # Disable debconf questions # so that automated builds won't prompt echo set debconf/frontend Noninteractive | debconf-communicate echo set debconf/priority critical | debconf-communicate apt-get -y --force-yes dist-upgrade # Install basic build tool set to match buildd apt-get -y --force-yes install build-pkg-1 build-pkg-2 # Set up expected /dev entries if [ ! -r /dev/stdin ]; then ln -s /proc/self/fd/0 /dev/stdin fi if [ ! -r /dev/stdout ]; then ln -s /proc/self/fd/1 /dev/stdout fi if [ ! -r /dev/stderr ]; then ln -s /proc/self/fd/2 /dev/stderr fi # Clean up rm /finish.sh apt-get clean """)) def test_chroot_generate_apt_conf_d_empty(self): self.use_temp_dir() chroot = ClickChroot("i386", "ubuntu-sdk-14.04") apt_conf_f = chroot._generate_apt_proxy_file(self.temp_dir, "") self.assertFalse(os.path.exists(apt_conf_f)) def test_chroot_generate_apt_conf_d(self): self.use_temp_dir() chroot = ClickChroot("i386", "ubuntu-sdk-14.04") apt_conf_f = chroot._generate_apt_proxy_file( self.temp_dir, "http://proxy.example.com") with open(apt_conf_f) as f: self.assertEqual( re.sub(r'\s+', ' ', f.read()), '// proxy settings copied by click chroot ' 'Acquire { HTTP { Proxy "http://proxy.example.com"; }; }; ') def test_chroot_generate_chroot_config(self): self.use_temp_dir() chroot = FakeClickChroot( "i386", "ubuntu-sdk-14.04", temp_dir=self.temp_dir) with mock.patch.object(chroot, "user", new="meep"): chroot._generate_chroot_config(self.temp_dir) with open(chroot.chroot_config) as f: content = f.read() self.assertEqual( content, dedent("""\ [click-ubuntu-sdk-14.04-i386] description=Build chroot for click packages on i386 users=root,{user} root-users=root,{user} source-root-users=root,{user} type=directory profile=default setup.fstab=click/fstab # Not protocols or services see # debian bug 557730 setup.nssdatabases=sbuild/nssdatabases union-type={overlayfs} directory={temp_dir} """).format(user="meep", temp_dir=self.temp_dir, overlayfs=chroot._get_overlayfs_name())) def test_chroot_create_mocked(self): self.use_temp_dir() os.environ["http_proxy"] = "http://proxy.example.com/" target = "ubuntu-sdk-14.04" chroot = FakeClickChroot( "i386", target, chroots_dir=self.temp_dir, temp_dir=self.temp_dir) with mock.patch.object(chroot, "maint") as mock_maint: mock_maint.return_value = 0 chroot.create() mock_maint.assert_called_with("/finish.sh") # ensure the following files where created inside the chroot for in_chroot in ["etc/localtime", "etc/timezone", "etc/apt/sources.list", "usr/sbin/policy-rc.d"]: full_path = os.path.join( self.temp_dir, chroot.full_name, in_chroot) self.assertTrue(os.path.exists(full_path)) # ensure the schroot/chroot.d file was created and looks valid schroot_d = os.path.join( self.temp_dir, "etc", "schroot", "chroot.d", chroot.full_name) self.assertTrue(os.path.exists(schroot_d)) def test_chroot_maint(self): chroot = ClickChroot("i386", "ubuntu-sdk-14.04") with mock.patch("subprocess.call") as mock_call: mock_call.return_value = 0 chroot.maint("foo", "bar") mock_call.assert_called_with([ "schroot", "-u", "root", "-c", "source:"+chroot.full_name, "--", "foo", "bar"]) def test_chroot_destroy(self): self.use_temp_dir() chroot = FakeClickChroot( "i386", "ubuntu-sdk-14.04", chroots_dir=self.temp_dir, temp_dir=self.temp_dir) chroot.create() chroot_path = os.path.join(self.temp_dir, chroot.full_name) self.assertTrue(os.path.exists(chroot_path)) chroot.destroy() self.assertFalse(os.path.exists(chroot_path)) def test_strip_dev_series_from_framework(self): for have, want in ( ("ubuntu-sdk-14.10-html-dev1", "ubuntu-sdk-14.10-html"), ("ubuntu-sdk-14.10-html", "ubuntu-sdk-14.10-html"), ("ubuntu-sdk-14.04-dev99", "ubuntu-sdk-14.04"), ): self.assertEqual(strip_dev_series_from_framework(have), want) click-0.5.0/click_package/tests/test_database.py000066400000000000000000001232671402441472600216770ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.database.""" from __future__ import print_function __metaclass__ = type __all__ = [ "TestClickDB", "TestClickInstalledPackage", "TestClickSingleDB", ] from functools import partial from itertools import takewhile import json import os import unittest from unittest import skip from gi.repository import Click, GLib from six import integer_types from click_package.json_helpers import json_array_to_python, json_object_to_python from click_package.tests.gimock_types import Passwd from click_package.tests.helpers import TestCase, mkfile, touch class TestClickInstalledPackage(TestCase): def setUp(self): super(TestClickInstalledPackage, self).setUp() self.foo = Click.InstalledPackage.new( "foo", "1.0", "/path/to/foo/1.0", False) self.foo_clone = Click.InstalledPackage.new( "foo", "1.0", "/path/to/foo/1.0", False) self.foo_different_version = Click.InstalledPackage.new( "foo", "2.0", "/path/to/foo/1.0", False) self.foo_different_path = Click.InstalledPackage.new( "foo", "1.0", "/path/to/foo/2.0", False) self.foo_different_writeable = Click.InstalledPackage.new( "foo", "1.0", "/path/to/foo/1.0", True) self.bar = Click.InstalledPackage.new( "bar", "1.0", "/path/to/foo/1.0", False) def test_hash(self): self.assertIsInstance(self.foo.hash(), integer_types) self.assertEqual(self.foo.hash(), self.foo_clone.hash()) self.assertNotEqual(self.foo.hash(), self.foo_different_version.hash()) self.assertNotEqual(self.foo.hash(), self.foo_different_path.hash()) self.assertNotEqual( self.foo.hash(), self.foo_different_writeable.hash()) self.assertNotEqual(self.foo.hash(), self.bar.hash()) # GLib doesn't allow passing an InstalledPackage as an argument here. @unittest.expectedFailure def test_equal_to(self): self.assertTrue(self.foo.equal_to(self.foo_clone)) self.assertFalse(self.foo.equal_to(self.foo_different_version)) self.assertFalse(self.foo.equal_to(self.foo_different_path)) self.assertFalse(self.foo.equal_to(self.foo_different_writeable)) self.assertFalse(self.foo.equal_to(self.bar)) class TestClickSingleDB(TestCase): def setUp(self): super(TestClickSingleDB, self).setUp() self.use_temp_dir() self.master_db = Click.DB() self.master_db.add(self.temp_dir) self.db = self.master_db.get(self.master_db.props.size - 1) self.spawn_calls = [] def g_spawn_sync_side_effect(self, status_map, working_directory, argv, envp, flags, child_setup, user_data, standard_output, standard_error, exit_status, error): self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv))) if argv[0] in status_map: exit_status[0] = status_map[argv[0]] else: self.delegate_to_original("g_spawn_sync") return 0 def _installed_packages_tuplify(self, ip): return [(p.props.package, p.props.version, p.props.path) for p in ip] def test_path(self): path = os.path.join(self.temp_dir, "a", "1.0") os.makedirs(path) self.assertEqual(path, self.db.get_path("a", "1.0")) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, self.db.get_path, "a", "1.1") def test_has_package_version(self): os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) self.assertTrue(self.db.has_package_version("a", "1.0")) self.assertFalse(self.db.has_package_version("a", "1.1")) def test_packages_current(self): os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) os.makedirs(os.path.join(self.temp_dir, "a", "1.1")) a_current = os.path.join(self.temp_dir, "a", "current") os.symlink("1.1", a_current) os.makedirs(os.path.join(self.temp_dir, "b", "0.1")) b_current = os.path.join(self.temp_dir, "b", "current") os.symlink("0.1", b_current) os.makedirs(os.path.join(self.temp_dir, "c", "2.0")) self.assertEqual([ ("a", "1.1", a_current), ("b", "0.1", b_current), ], self._installed_packages_tuplify( self.db.get_packages(all_versions=False))) def test_packages_all(self): os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) os.makedirs(os.path.join(self.temp_dir, "a", "1.1")) os.symlink("1.1", os.path.join(self.temp_dir, "a", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "0.1")) os.symlink("0.1", os.path.join(self.temp_dir, "b", "current")) os.makedirs(os.path.join(self.temp_dir, "c", "2.0")) self.assertEqual([ ("a", "1.0", os.path.join(self.temp_dir, "a", "1.0")), ("a", "1.1", os.path.join(self.temp_dir, "a", "1.1")), ("b", "0.1", os.path.join(self.temp_dir, "b", "0.1")), ("c", "2.0", os.path.join(self.temp_dir, "c", "2.0")), ], self._installed_packages_tuplify( self.db.get_packages(all_versions=True))) def test_packages_all_ignores_non_directory(self): os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) touch(os.path.join(self.temp_dir, "file")) self.assertEqual([ ("a", "1.0", os.path.join(self.temp_dir, "a", "1.0")), ], self._installed_packages_tuplify( self.db.get_packages(all_versions=True))) def test_manifest(self): manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") manifest_obj = { "name": "a", "version": "1.0", "hooks": {"a-app": {}}, "_should_be_removed": "", } with mkfile(manifest_path) as manifest: json.dump(manifest_obj, manifest) del manifest_obj["_should_be_removed"] manifest_obj["_directory"] = os.path.join(self.temp_dir, "a", "1.0") self.assertEqual( manifest_obj, json_object_to_python(self.db.get_manifest("a", "1.0"))) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, self.db.get_manifest, "a", "1.1") self.assertEqual( manifest_obj, json.loads(self.db.get_manifest_as_string("a", "1.0"))) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, self.db.get_manifest_as_string, "a", "1.1") def test_manifest_bad(self): manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: print("{bad syntax", file=manifest) self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, self.db.get_manifest, "a", "1.0") self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, self.db.get_manifest_as_string, "a", "1.0") manifest_path = os.path.join( self.temp_dir, "a", "1.1", ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: print("[0]", file=manifest) self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, self.db.get_manifest, "a", "1.1") self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, self.db.get_manifest_as_string, "a", "1.1") def test_app_running(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() preloads["click_find_on_path"].return_value = True preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 0}) self.assertTrue(self.db.app_running("foo", "bar", "1.0")) self.assertEqual( [[b"lomiri-app-pid", b"foo_bar_1.0"]], self.spawn_calls) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 1 << 8}) self.assertFalse(self.db.app_running("foo", "bar", "1.0")) def test_any_app_running_ubuntu_app_pid(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: json.dump({"hooks": {"a-app": {}}}, manifest) preloads["click_find_on_path"].side_effect = ( lambda command: command == b"lomiri-app-pid") preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 0}) self.assertTrue(self.db.any_app_running("a", "1.0")) self.assertEqual( [[b"lomiri-app-pid", b"a_a-app_1.0"]], self.spawn_calls) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 1 << 8}) self.assertFalse(self.db.any_app_running("a", "1.0")) def test_any_app_running_no_app_pid_command(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: json.dump({"hooks": {"a-app": {}}}, manifest) preloads["click_find_on_path"].return_value = False preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 0}) self.assertFalse(self.db.any_app_running("a", "1.0")) def test_any_app_running_missing_app(self): with self.run_in_subprocess("click_find_on_path") as (enter, preloads): enter() preloads["click_find_on_path"].side_effect = ( lambda command: command == b"lomiri-app-pid") self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, self.db.any_app_running, "a", "1.0") def test_any_app_running_bad_manifest(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: print("{bad syntax", file=manifest) preloads["click_find_on_path"].side_effect = ( lambda command: command == b"lomiri-app-pid") self.assertFalse(self.db.any_app_running("a", "1.0")) self.assertFalse(preloads["g_spawn_sync"].called) def test_any_app_running_no_hooks(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: json.dump({}, manifest) preloads["click_find_on_path"].side_effect = ( lambda command: command == b"lomiri-app-pid") self.assertFalse(self.db.any_app_running("a", "1.0")) self.assertFalse(preloads["g_spawn_sync"].called) def test_maybe_remove_registered(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() version_path = os.path.join(self.temp_dir, "a", "1.0") manifest_path = os.path.join( version_path, ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: json.dump({"hooks": {"a-app": {}}}, manifest) user_path = os.path.join( self.temp_dir, ".click", "users", "test-user", "a") os.makedirs(os.path.dirname(user_path)) os.symlink(version_path, user_path) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 0}) preloads["click_find_on_path"].return_value = True self.db.maybe_remove("a", "1.0") self.assertTrue(os.path.exists(version_path)) self.assertTrue(os.path.exists(user_path)) def test_maybe_remove_running(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() version_path = os.path.join(self.temp_dir, "a", "1.0") manifest_path = os.path.join( version_path, ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: json.dump({"hooks": {"a-app": {}}}, manifest) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 0}) preloads["click_find_on_path"].return_value = True self.db.maybe_remove("a", "1.0") self.assertTrue(os.path.exists(version_path)) @skip("See https://github.com/ubports/click/issues/6") def test_maybe_remove_not_running(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", ) as (enter, preloads): enter() os.environ["TEST_QUIET"] = "1" version_path = os.path.join(self.temp_dir, "a", "1.0") manifest_path = os.path.join( version_path, ".click", "info", "a.manifest") with mkfile(manifest_path) as manifest: json.dump({"hooks": {"a-app": {}}}, manifest) current_path = os.path.join(self.temp_dir, "a", "current") os.symlink("1.0", current_path) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 1 << 8}) preloads["click_find_on_path"].return_value = True self.db.maybe_remove("a", "1.0") self.assertFalse(os.path.exists(os.path.join(self.temp_dir, "a"))) @skip("See https://github.com/ubports/click/issues/6") def test_gc(self): with self.run_in_subprocess( "click_find_on_path", "g_spawn_sync", "getpwnam" ) as (enter, preloads): enter() preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) os.environ["TEST_QUIET"] = "1" a_path = os.path.join(self.temp_dir, "a", "1.0") a_manifest_path = os.path.join( a_path, ".click", "info", "a.manifest") with mkfile(a_manifest_path) as manifest: json.dump({"hooks": {"a-app": {}}}, manifest) b_path = os.path.join(self.temp_dir, "b", "1.0") b_manifest_path = os.path.join( b_path, ".click", "info", "b.manifest") with mkfile(b_manifest_path) as manifest: json.dump({"hooks": {"b-app": {}}}, manifest) c_path = os.path.join(self.temp_dir, "c", "1.0") c_manifest_path = os.path.join( c_path, ".click", "info", "c.manifest") with mkfile(c_manifest_path) as manifest: json.dump({"hooks": {"c-app": {}}}, manifest) a_user_path = os.path.join( self.temp_dir, ".click", "users", "test-user", "a") os.makedirs(os.path.dirname(a_user_path)) os.symlink(a_path, a_user_path) b_gcinuse_path = os.path.join( self.temp_dir, ".click", "users", "@gcinuse", "b") os.makedirs(os.path.dirname(b_gcinuse_path)) os.symlink(b_path, b_gcinuse_path) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"lomiri-app-pid": 1 << 8}) preloads["click_find_on_path"].return_value = True self.db.gc() self.assertTrue(os.path.exists(a_path)) self.assertFalse(os.path.exists(b_gcinuse_path)) self.assertFalse(os.path.exists(b_path)) self.assertFalse(os.path.exists(c_path)) def test_gc_ignores_non_directory(self): with self.run_in_subprocess( "getpwnam" ) as (enter, preloads): enter() preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) a_path = os.path.join(self.temp_dir, "a", "1.0") a_manifest_path = os.path.join( a_path, ".click", "info", "a.manifest") with mkfile(a_manifest_path) as manifest: json.dump({"hooks": {"a-app": {}}}, manifest) a_user_path = os.path.join( self.temp_dir, ".click", "users", "test-user", "a") os.makedirs(os.path.dirname(a_user_path)) os.symlink(a_path, a_user_path) touch(os.path.join(self.temp_dir, "file")) self.db.gc() self.assertTrue(os.path.exists(a_path)) # Test that bug #1479001 is fixed. Uses the following scenario: # # - Two databases: db1 and db2. # - One package, "test-package": # - Versions 1 and 3 installed in db1 # - Version 2 installed in db2 # - User has a registration in db2 for version 2, where the registration # timestamp precedes the installation of version 3. # # In this case, bug #1479001 expects that the user's registration would # be updated to 3, since it was installed after the user registered for # 2, which implies that the user would like the update to 3. @skip("See https://github.com/ubports/click/issues/6") def test_gc_fixes_old_user_registrations(self): with self.run_in_subprocess("getpwnam") as (enter, preloads): enter() # Setup the system hook preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_dir=b"/foo"))) # Setup both databases db1 = os.path.join(self.temp_dir, "db1") db2 = os.path.join(self.temp_dir, "db2") db = Click.DB() db.add(db1) db.add(db2) # Prepare common manifest for the packages manifest = {"hooks": {"test-app": {"test": "foo"}}} # Setup versions 1.0 and 3.0 of package in db1 version1 = os.path.join(db1, "test-package", "1.0") with mkfile(os.path.join(version1, ".click", "info", "test-package.manifest")) as f: json.dump(manifest, f) version3 = os.path.join(db1, "test-package", "3.0") with mkfile(os.path.join(version3, ".click", "info", "test-package.manifest")) as f: json.dump(manifest, f) # Setup version 0.2 of package in db2 version2 = os.path.join(db2, "test-package", "2.0") with mkfile(os.path.join(version2, ".click", "info", "test-package.manifest")) as f: json.dump(manifest, f) # Setup the user registration for 2.0 in db2. registrationPath = os.path.join( db2, ".click", "users", "foo", "test-package") os.makedirs(os.path.dirname(registrationPath)) os.symlink(version2, registrationPath) # Run the garbage collection to update the registrations. db.gc() # Verify that the user still has a registration for the package, # and that it's now registered for version 3.0. self.assertTrue(os.path.lexists(registrationPath)) self.assertEqual(version3, os.readlink(registrationPath)) user_db = Click.User.for_user(db, "foo") try: version = user_db.get_version("test-package") self.assertEqual("3.0", version) except: self.fail("No user registration for 'test-package'") def _make_ownership_test(self): path = os.path.join(self.temp_dir, "a", "1.0") touch(os.path.join(path, ".click", "info", "a.manifest")) os.symlink("1.0", os.path.join(self.temp_dir, "a", "current")) user_path = os.path.join( self.temp_dir, ".click", "users", "test-user", "a") os.makedirs(os.path.dirname(user_path)) os.symlink(path, user_path) touch(os.path.join(self.temp_dir, ".click", "log")) def _set_stat_side_effect(self, preloads, side_effect, limit): limit = limit.encode() preloads["__xstat"].side_effect = ( lambda ver, path, buf: side_effect( "__xstat", limit, ver, path, buf)) preloads["__xstat64"].side_effect = ( lambda ver, path, buf: side_effect( "__xstat64", limit, ver, path, buf)) def test_ensure_ownership_quick_if_correct(self): def stat_side_effect(name, limit, ver, path, buf): st = self.convert_stat_pointer(name, buf) if path == limit: st.st_uid = 1 st.st_gid = 1 return 0 else: self.delegate_to_original(name) return -1 with self.run_in_subprocess( "chown", "getpwnam", "__xstat", "__xstat64", ) as (enter, preloads): enter() preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) self._set_stat_side_effect( preloads, stat_side_effect, self.db.props.root) self._make_ownership_test() self.db.ensure_ownership() self.assertFalse(preloads["chown"].called) def test_ensure_ownership(self): def stat_side_effect(name, limit, ver, path, buf): st = self.convert_stat_pointer(name, buf) if path == limit: st.st_uid = 2 st.st_gid = 2 return 0 else: self.delegate_to_original(name) return -1 with self.run_in_subprocess( "chown", "getpwnam", "__xstat", "__xstat64", ) as (enter, preloads): enter() preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) self._set_stat_side_effect( preloads, stat_side_effect, self.db.props.root) self._make_ownership_test() self.db.ensure_ownership() expected_paths = [ self.temp_dir, os.path.join(self.temp_dir, ".click"), os.path.join(self.temp_dir, ".click", "log"), os.path.join(self.temp_dir, ".click", "users"), os.path.join(self.temp_dir, "a"), os.path.join(self.temp_dir, "a", "1.0"), os.path.join(self.temp_dir, "a", "1.0", ".click"), os.path.join(self.temp_dir, "a", "1.0", ".click", "info"), os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest"), os.path.join(self.temp_dir, "a", "current"), ] self.assertCountEqual( [path.encode() for path in expected_paths], [args[0][0] for args in preloads["chown"].call_args_list]) self.assertCountEqual( [(1, 1)], set(args[0][1:] for args in preloads["chown"].call_args_list)) def test_ensure_ownership_missing_clickpkg_user(self): with self.run_in_subprocess("getpwnam") as (enter, preloads): enter() preloads["getpwnam"].return_value = None self.assertRaisesDatabaseError( Click.DatabaseError.ENSURE_OWNERSHIP, self.db.ensure_ownership) def test_ensure_ownership_failed_chown(self): def stat_side_effect(name, limit, ver, path, buf): st = self.convert_stat_pointer(name, buf) if path == limit: st.st_uid = 2 st.st_gid = 2 return 0 else: self.delegate_to_original(name) return -1 with self.run_in_subprocess( "chown", "getpwnam", "__xstat", "__xstat64", ) as (enter, preloads): enter() preloads["chown"].return_value = -1 preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) self._set_stat_side_effect( preloads, stat_side_effect, self.db.props.root) self._make_ownership_test() self.assertRaisesDatabaseError( Click.DatabaseError.ENSURE_OWNERSHIP, self.db.ensure_ownership) class TestClickDB(TestCase): def setUp(self): super(TestClickDB, self).setUp() self.use_temp_dir() def _installed_packages_tuplify(self, ip): return [ (p.props.package, p.props.version, p.props.path, p.props.writeable) for p in ip] def test_read_configuration(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = /a", file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = /b", file=b) db = Click.DB() db.read(db_dir=self.temp_dir) db.add("/c") self.assertEqual(3, db.props.size) self.assertEqual( ["/a", "/b", "/c"], [db.get(i).props.root for i in range(db.props.size)]) def test_no_read(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = /a", file=a) db = Click.DB() self.assertEqual(0, db.props.size) def test_no_db_conf_errors(self): db = Click.DB() self.assertRaisesDatabaseError( Click.DatabaseError.INVALID, db.get, 0) self.assertEqual(db.props.overlay, "") self.assertRaisesDatabaseError( Click.DatabaseError.INVALID, db.maybe_remove, "something", "1.0") self.assertRaisesDatabaseError( Click.DatabaseError.INVALID, db.gc) self.assertRaisesDatabaseError( Click.DatabaseError.INVALID, db.ensure_ownership) def test_read_nonexistent(self): db = Click.DB() db.read(db_dir=os.path.join(self.temp_dir, "nonexistent")) self.assertEqual(0, db.props.size) def test_read_not_directory(self): path = os.path.join(self.temp_dir, "file") touch(path) db = Click.DB() self.assertRaisesFileError(GLib.FileError.NOTDIR, db.read, db_dir=path) def test_add(self): db = Click.DB() self.assertEqual(0, db.props.size) db.add("/new/root") self.assertEqual(1, db.props.size) self.assertEqual("/new/root", db.get(0).props.root) def test_overlay(self): with open(os.path.join(self.temp_dir, "00_custom.conf"), "w") as f: print("[Click Database]", file=f) print("root = /custom", file=f) with open(os.path.join(self.temp_dir, "99_default.conf"), "w") as f: print("[Click Database]", file=f) print("root = /opt/click.ubuntu.com", file=f) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual("/opt/click.ubuntu.com", db.props.overlay) def test_path(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.0") os.makedirs(os.path.join(self.temp_dir, "a", "pkg", "1.0")) self.assertEqual( os.path.join(self.temp_dir, "a", "pkg", "1.0"), db.get_path("pkg", "1.0")) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_path, "pkg", "1.1") os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.0")) # The deepest copy of the same package/version is still preferred. self.assertEqual( os.path.join(self.temp_dir, "a", "pkg", "1.0"), db.get_path("pkg", "1.0")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.1")) self.assertEqual( os.path.join(self.temp_dir, "b", "pkg", "1.1"), db.get_path("pkg", "1.1")) def test_has_package_version(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertFalse(db.has_package_version("pkg", "1.0")) os.makedirs(os.path.join(self.temp_dir, "a", "pkg", "1.0")) self.assertTrue(db.has_package_version("pkg", "1.0")) self.assertFalse(db.has_package_version("pkg", "1.1")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.0")) self.assertTrue(db.has_package_version("pkg", "1.0")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg", "1.1")) self.assertTrue(db.has_package_version("pkg", "1.1")) def test_packages_current(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual([], list(db.get_packages(all_versions=False))) os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0")) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1")) pkg1_current = os.path.join(self.temp_dir, "b", "pkg1", "current") os.symlink("1.1", pkg1_current) os.makedirs(os.path.join(self.temp_dir, "b", "pkg2", "0.1")) pkg2_current = os.path.join(self.temp_dir, "b", "pkg2", "current") os.symlink("0.1", pkg2_current) self.assertEqual([ ("pkg1", "1.1", pkg1_current, True), ("pkg2", "0.1", pkg2_current, True), ], self._installed_packages_tuplify( db.get_packages(all_versions=False))) def test_packages_all(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual([], list(db.get_packages(all_versions=True))) os.makedirs(os.path.join(self.temp_dir, "a", "pkg1", "1.0")) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg1", "1.1")) os.symlink("1.1", os.path.join(self.temp_dir, "b", "pkg1", "current")) os.makedirs(os.path.join(self.temp_dir, "b", "pkg2", "0.1")) os.symlink("0.1", os.path.join(self.temp_dir, "b", "pkg2", "current")) self.assertEqual([ ("pkg1", "1.1", os.path.join(self.temp_dir, "b", "pkg1", "1.1"), True), ("pkg2", "0.1", os.path.join(self.temp_dir, "b", "pkg2", "0.1"), True), ("pkg1", "1.0", os.path.join(self.temp_dir, "a", "pkg1", "1.0"), False), ], self._installed_packages_tuplify( db.get_packages(all_versions=True))) def test_manifest(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest, "pkg", "1.0") self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest_as_string, "pkg", "1.0") a_manifest_path = os.path.join( self.temp_dir, "a", "pkg", "1.0", ".click", "info", "pkg.manifest") a_manifest_obj = {"name": "pkg", "version": "1.0"} with mkfile(a_manifest_path) as a_manifest: json.dump(a_manifest_obj, a_manifest) a_manifest_obj["_directory"] = os.path.join( self.temp_dir, "a", "pkg", "1.0") self.assertEqual( a_manifest_obj, json_object_to_python(db.get_manifest("pkg", "1.0"))) self.assertEqual( a_manifest_obj, json.loads(db.get_manifest_as_string("pkg", "1.0"))) self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest, "pkg", "1.1") self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, db.get_manifest_as_string, "pkg", "1.1") b_manifest_path = os.path.join( self.temp_dir, "b", "pkg", "1.1", ".click", "info", "pkg.manifest") b_manifest_obj = {"name": "pkg", "version": "1.1"} with mkfile(b_manifest_path) as b_manifest: json.dump(b_manifest_obj, b_manifest) b_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg", "1.1") self.assertEqual( b_manifest_obj, json_object_to_python(db.get_manifest("pkg", "1.1"))) self.assertEqual( b_manifest_obj, json.loads(db.get_manifest_as_string("pkg", "1.1"))) def test_manifest_bad(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) db = Click.DB() db.read(db_dir=self.temp_dir) manifest_path = os.path.join( self.temp_dir, "a", "pkg", "1.0", ".click", "info", "pkg.manifest") with mkfile(manifest_path) as manifest: print("{bad syntax", file=manifest) self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, db.get_manifest, "pkg", "1.0") self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, db.get_manifest_as_string, "pkg", "1.0") manifest_path = os.path.join( self.temp_dir, "a", "pkg", "1.1", ".click", "info", "pkg.manifest") with mkfile(manifest_path) as manifest: print("[0]", file=manifest) self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, db.get_manifest, "pkg", "1.0") self.assertRaisesDatabaseError( Click.DatabaseError.BAD_MANIFEST, db.get_manifest_as_string, "pkg", "1.0") def test_manifests_current(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual( [], json_array_to_python(db.get_manifests(all_versions=False))) self.assertEqual( [], json.loads(db.get_manifests_as_string(all_versions=False))) a_pkg1_manifest_path = os.path.join( self.temp_dir, "a", "pkg1", "1.0", ".click", "info", "pkg1.manifest") a_pkg1_manifest_obj = {"name": "pkg1", "version": "1.0"} with mkfile(a_pkg1_manifest_path) as a_pkg1_manifest: json.dump(a_pkg1_manifest_obj, a_pkg1_manifest) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) b_pkg1_manifest_path = os.path.join( self.temp_dir, "b", "pkg1", "1.1", ".click", "info", "pkg1.manifest") b_pkg1_manifest_obj = {"name": "pkg1", "version": "1.1"} with mkfile(b_pkg1_manifest_path) as b_pkg1_manifest: json.dump(b_pkg1_manifest_obj, b_pkg1_manifest) os.symlink("1.1", os.path.join(self.temp_dir, "b", "pkg1", "current")) b_pkg2_manifest_path = os.path.join( self.temp_dir, "b", "pkg2", "0.1", ".click", "info", "pkg2.manifest") b_pkg2_manifest_obj = {"name": "pkg2", "version": "0.1"} with mkfile(b_pkg2_manifest_path) as b_pkg2_manifest: json.dump(b_pkg2_manifest_obj, b_pkg2_manifest) os.symlink("0.1", os.path.join(self.temp_dir, "b", "pkg2", "current")) b_pkg1_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg1", "1.1") b_pkg1_manifest_obj["_removable"] = 1 b_pkg2_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg2", "0.1") b_pkg2_manifest_obj["_removable"] = 1 self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj], json_array_to_python(db.get_manifests(all_versions=False))) self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj], json.loads(db.get_manifests_as_string(all_versions=False))) def test_manifests_all(self): with open(os.path.join(self.temp_dir, "a.conf"), "w") as a: print("[Click Database]", file=a) print("root = %s" % os.path.join(self.temp_dir, "a"), file=a) with open(os.path.join(self.temp_dir, "b.conf"), "w") as b: print("[Click Database]", file=b) print("root = %s" % os.path.join(self.temp_dir, "b"), file=b) db = Click.DB() db.read(db_dir=self.temp_dir) self.assertEqual( [], json_array_to_python(db.get_manifests(all_versions=True))) self.assertEqual( [], json.loads(db.get_manifests_as_string(all_versions=True))) a_pkg1_manifest_path = os.path.join( self.temp_dir, "a", "pkg1", "1.0", ".click", "info", "pkg1.manifest") a_pkg1_manifest_obj = {"name": "pkg1", "version": "1.0"} with mkfile(a_pkg1_manifest_path) as a_pkg1_manifest: json.dump(a_pkg1_manifest_obj, a_pkg1_manifest) os.symlink("1.0", os.path.join(self.temp_dir, "a", "pkg1", "current")) b_pkg1_manifest_path = os.path.join( self.temp_dir, "b", "pkg1", "1.1", ".click", "info", "pkg1.manifest") b_pkg1_manifest_obj = {"name": "pkg1", "version": "1.1"} with mkfile(b_pkg1_manifest_path) as b_pkg1_manifest: json.dump(b_pkg1_manifest_obj, b_pkg1_manifest) os.symlink("1.1", os.path.join(self.temp_dir, "b", "pkg1", "current")) b_pkg2_manifest_path = os.path.join( self.temp_dir, "b", "pkg2", "0.1", ".click", "info", "pkg2.manifest") b_pkg2_manifest_obj = {"name": "pkg2", "version": "0.1"} with mkfile(b_pkg2_manifest_path) as b_pkg2_manifest: json.dump(b_pkg2_manifest_obj, b_pkg2_manifest) os.symlink("0.1", os.path.join(self.temp_dir, "b", "pkg2", "current")) a_pkg1_manifest_obj["_directory"] = os.path.join( self.temp_dir, "a", "pkg1", "1.0") a_pkg1_manifest_obj["_removable"] = 0 b_pkg1_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg1", "1.1") b_pkg1_manifest_obj["_removable"] = 1 b_pkg2_manifest_obj["_directory"] = os.path.join( self.temp_dir, "b", "pkg2", "0.1") b_pkg2_manifest_obj["_removable"] = 1 self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj, a_pkg1_manifest_obj], json_array_to_python(db.get_manifests(all_versions=True))) self.assertEqual( [b_pkg1_manifest_obj, b_pkg2_manifest_obj, a_pkg1_manifest_obj], json.loads(db.get_manifests_as_string(all_versions=True))) click-0.5.0/click_package/tests/test_framework.py000066400000000000000000000137121402441472600221210ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.framework.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickFramework', ] import os from gi.repository import Click from click_package.tests.helpers import TestCase, touch class TestClickFramework(TestCase): def setUp(self): super(TestClickFramework, self).setUp() self.use_temp_dir() def _setup_frameworks(self, preloads, frameworks_dir=None, frameworks={}): if frameworks_dir is None: frameworks_dir = os.path.join(self.temp_dir, "frameworks") Click.ensuredir(frameworks_dir) for framework_name in frameworks: framework_path = os.path.join( frameworks_dir, "%s.framework" % framework_name) with open(framework_path, "w") as framework: for key, value in frameworks[framework_name].items(): print("%s: %s" % (key, value), file=framework) preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(frameworks_dir)) def test_open(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks(preloads, frameworks={"framework-1": {}}) Click.Framework.open("framework-1") self.assertRaisesFrameworkError( Click.FrameworkError.NO_SUCH_FRAMEWORK, Click.Framework.open, "framework-2") def test_has_framework(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks(preloads, frameworks={"framework-1": {}}) self.assertTrue(Click.Framework.has_framework("framework-1")) self.assertFalse(Click.Framework.has_framework("framework-2")) def test_get_frameworks(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks( preloads, frameworks={"ubuntu-sdk-13.10": {}, "ubuntu-sdk-14.04": {}, "ubuntu-sdk-14.10": {}}) self.assertEqual( ["ubuntu-sdk-13.10", "ubuntu-sdk-14.04", "ubuntu-sdk-14.10"], sorted(f.props.name for f in Click.Framework.get_frameworks())) def test_get_frameworks_nonexistent(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() frameworks_dir = os.path.join(self.temp_dir, "nonexistent") preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(frameworks_dir)) self.assertEqual([], Click.Framework.get_frameworks()) def test_get_frameworks_not_directory(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = os.path.join(self.temp_dir, "file") touch(path) preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(path)) self.assertEqual([], Click.Framework.get_frameworks()) def test_get_frameworks_ignores_other_files(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() frameworks_dir = os.path.join(self.temp_dir, "frameworks") Click.ensuredir(frameworks_dir) touch(os.path.join(frameworks_dir, "file")) preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(frameworks_dir)) self.assertEqual([], Click.Framework.get_frameworks()) def test_get_frameworks_ignores_unopenable_files(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() frameworks_dir = os.path.join(self.temp_dir, "frameworks") Click.ensuredir(frameworks_dir) os.symlink( "nonexistent", os.path.join(frameworks_dir, "foo.framework")) preloads["click_get_frameworks_dir"].side_effect = ( lambda: self.make_string(frameworks_dir)) self.assertEqual([], Click.Framework.get_frameworks()) def test_fields(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() self._setup_frameworks( preloads, frameworks={ "ubuntu-sdk-14.04-qml": { "base-name": "ubuntu-sdk", "base-version": "14.04", }}) framework = Click.Framework.open("ubuntu-sdk-14.04-qml") self.assertCountEqual( ["base-name", "base-version"], framework.get_fields()) self.assertEqual("ubuntu-sdk", framework.get_field("base-name")) self.assertEqual("14.04", framework.get_field("base-version")) self.assertRaisesFrameworkError( Click.FrameworkError.MISSING_FIELD, framework.get_field, "nonexistent") self.assertEqual("ubuntu-sdk", framework.get_base_name()) self.assertEqual("14.04", framework.get_base_version()) click-0.5.0/click_package/tests/test_hooks.py000066400000000000000000001605211402441472600212500ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.hooks.""" from __future__ import print_function __metaclass__ = type __all__ = [ "TestClickHookSystemLevel", "TestClickHookUserLevel", "TestClickPatternFormatter", "TestPackageInstallHooks", "TestPackageRemoveHooks", ] from functools import partial from itertools import takewhile import json import os from textwrap import dedent from gi.repository import Click, GLib from click_package.tests.gimock_types import Passwd from click_package.tests.helpers import TestCase, mkfile, mkfile_utf8 class TestClickPatternFormatter(TestCase): def _make_variant(self, **kwargs): # pygobject's Variant creator can't handle maybe types, so we have # to do this by hand. builder = GLib.VariantBuilder.new(GLib.VariantType.new("a{sms}")) for key, value in kwargs.items(): entry = GLib.VariantBuilder.new(GLib.VariantType.new("{sms}")) entry.add_value(GLib.Variant.new_string(key)) entry.add_value(GLib.Variant.new_maybe( GLib.VariantType.new("s"), None if value is None else GLib.Variant.new_string(value))) builder.add_value(entry.end()) return builder.end() def test_expands_provided_keys(self): self.assertEqual( "foo.bar", Click.pattern_format("foo.${key}", self._make_variant(key="bar"))) self.assertEqual( "foo.barbaz", Click.pattern_format( "foo.${key1}${key2}", self._make_variant(key1="bar", key2="baz"))) def test_expands_missing_keys_to_empty_string(self): self.assertEqual( "xy", Click.pattern_format("x${key}y", self._make_variant())) def test_preserves_unmatched_dollar(self): self.assertEqual("$", Click.pattern_format("$", self._make_variant())) self.assertEqual( "$ {foo}", Click.pattern_format("$ {foo}", self._make_variant())) self.assertEqual( "x${y", Click.pattern_format("${key}${y", self._make_variant(key="x"))) def test_double_dollar(self): self.assertEqual("$", Click.pattern_format("$$", self._make_variant())) self.assertEqual( "${foo}", Click.pattern_format("$${foo}", self._make_variant())) self.assertEqual( "x$y", Click.pattern_format("x$$${key}", self._make_variant(key="y"))) def test_possible_expansion(self): self.assertEqual( {"id": "abc"}, Click.pattern_possible_expansion( "x_abc_1", "x_${id}_${num}", self._make_variant(num="1")).unpack()) self.assertIsNone( Click.pattern_possible_expansion( "x_abc_1", "x_${id}_${num}", self._make_variant(num="2"))) class TestClickHookBase(TestCase): TEST_USER = "test-user" def setUp(self): super(TestClickHookBase, self).setUp() self.use_temp_dir() self.db = Click.DB() self.db.add(self.temp_dir) self.spawn_calls = [] def _make_installed_click(self, package="test-1", version="1.0", json_data={}, make_current=True, all_users=False): with mkfile_utf8(os.path.join( self.temp_dir, package, version, ".click", "info", "%s.manifest" % package)) as f: json.dump(json_data, f, ensure_ascii=False) if make_current: os.symlink( version, os.path.join(self.temp_dir, package, "current")) if all_users: db = Click.User.for_all_users(self.db) else: db = Click.User.for_user(self.db, self.TEST_USER) db.set_version(package, version) def _make_hook_file(self, content, hookname="test"): hook_file = os.path.join(self.hooks_dir, "%s.hook" % hookname) with mkfile(hook_file) as f: print(content, file=f) def _setup_hooks_dir(self, preloads, hooks_dir=None): if hooks_dir is None: hooks_dir = self.temp_dir preloads["click_get_hooks_dir"].side_effect = ( lambda: self.make_string(hooks_dir)) self.hooks_dir = hooks_dir def g_spawn_sync_side_effect(self, status_map, working_directory, argv, envp, flags, child_setup, user_data, standard_output, standard_error, exit_status, error): self.spawn_calls.append(list(takewhile(lambda x: x is not None, argv))) if argv[0] in status_map: exit_status[0] = status_map[argv[0]] else: self.delegate_to_original("g_spawn_sync") return 0 class TestClickHookSystemLevel(TestClickHookBase): def test_open(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ Pattern: /usr/share/test/${id}.test # Comment Exec: test-update User: root """)) hook = Click.Hook.open(self.db, "test") self.assertCountEqual( ["pattern", "exec", "user"], hook.get_fields()) self.assertEqual( "/usr/share/test/${id}.test", hook.get_field("pattern")) self.assertEqual("test-update", hook.get_field("exec")) self.assertRaisesHooksError( Click.HooksError.MISSING_FIELD, hook.get_field, "nonexistent") self.assertFalse(hook.props.is_user_level) def test_open_unopenable_file(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) os.symlink("nonexistent", os.path.join(self.hooks_dir, "foo.hook")) self.assertRaisesHooksError( Click.HooksError.NO_SUCH_HOOK, Click.Hook.open, self.db, "foo") def test_hook_name_absent(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file( "Pattern: /usr/share/test/${id}.test") hook = Click.Hook.open(self.db, "test") self.assertEqual("test", hook.get_hook_name()) def test_hook_name_present(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ Pattern: /usr/share/test/${id}.test Hook-Name: other""")) hook = Click.Hook.open(self.db, "test") self.assertEqual("other", hook.get_hook_name()) def test_invalid_app_id(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ Pattern: /usr/share/test/${id}.test # Comment Exec: test-update User: root """)) hook = Click.Hook.open(self.db, "test") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_app_id, "package", "0.1", "app_name") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_app_id, "package", "0.1", "app/name") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_pattern, "package", "0.1", "app_name") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_pattern, "package", "0.1", "app/name") def test_short_id_invalid(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file( "Pattern: /usr/share/test/${short-id}.test") hook = Click.Hook.open(self.db, "test") # It would perhaps be better if unrecognised $-expansions raised # KeyError, but they don't right now. self.assertEqual( "/usr/share/test/.test", hook.get_pattern("package", "0.1", "app-name", user_name=None)) def test_short_id_valid_with_single_version(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ Pattern: /usr/share/test/${short-id}.test Single-Version: yes""")) hook = Click.Hook.open(self.db, "test") self.assertEqual( "/usr/share/test/package_app-name.test", hook.get_pattern("package", "0.1", "app-name", user_name=None)) def test_run_commands(self): with self.run_in_subprocess( "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"/bin/sh": 0}) with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("Exec: test-update", file=f) print("User: root", file=f) hook = Click.Hook.open(self.db, "test") self.assertEqual( "root", hook.get_run_commands_user(user_name=None)) hook.run_commands(user_name=None) self.assertEqual( [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls) def test_run_commands_fail(self): with self.run_in_subprocess( "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"/bin/sh": 1}) with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("Exec: test-update", file=f) print("User: root", file=f) hook = Click.Hook.open(self.db, "test") self.assertRaisesHooksError( Click.HooksError.COMMAND_FAILED, hook.run_commands, user_name=None) def test_install_package(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file( "Pattern: %s/${id}.test" % self.temp_dir) os.makedirs( os.path.join(self.temp_dir, "org.example.package", "1.0")) hook = Click.Hook.open(self.db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo/bar", user_name=None) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") target_path = os.path.join( self.temp_dir, "org.example.package", "1.0", "foo", "bar") self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(target_path, os.readlink(symlink_path)) def test_install_package_trailing_slash(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file( "Pattern: %s/${id}/" % self.temp_dir) os.makedirs( os.path.join(self.temp_dir, "org.example.package", "1.0")) hook = Click.Hook.open(self.db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo", user_name=None) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0") target_path = os.path.join( self.temp_dir, "org.example.package", "1.0", "foo") self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(target_path, os.readlink(symlink_path)) def test_install_package_uses_deepest_copy(self): # If the same version of a package is unpacked in multiple # databases, then we make sure the link points to the deepest copy, # even if it already points somewhere else. It is important to be # consistent about this since system hooks may only have a single # target for any given application ID. with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file( "Pattern: %s/${id}.test" % self.temp_dir) underlay = os.path.join(self.temp_dir, "underlay") overlay = os.path.join(self.temp_dir, "overlay") db = Click.DB() db.add(underlay) db.add(overlay) os.makedirs(os.path.join(underlay, "org.example.package", "1.0")) os.makedirs(os.path.join(overlay, "org.example.package", "1.0")) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") underlay_target_path = os.path.join( underlay, "org.example.package", "1.0", "foo") overlay_target_path = os.path.join( overlay, "org.example.package", "1.0", "foo") os.symlink(overlay_target_path, symlink_path) hook = Click.Hook.open(db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo", user_name=None) self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(underlay_target_path, os.readlink(symlink_path)) def test_upgrade(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file( "Pattern: %s/${id}.test" % self.temp_dir) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") os.symlink("old-target", symlink_path) os.makedirs( os.path.join(self.temp_dir, "org.example.package", "1.0")) hook = Click.Hook.open(self.db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo/bar", user_name=None) target_path = os.path.join( self.temp_dir, "org.example.package", "1.0", "foo", "bar") self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(target_path, os.readlink(symlink_path)) def test_remove_package(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file( "Pattern: %s/${id}.test" % self.temp_dir) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") os.symlink("old-target", symlink_path) hook = Click.Hook.open(self.db, "test") hook.remove_package( "org.example.package", "1.0", "test-app", user_name=None) self.assertFalse(os.path.exists(symlink_path)) def test_install(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir( preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) self._make_hook_file( "Pattern: %s/${id}.new" % self.temp_dir, hookname="new") self._make_installed_click("test-1", "1.0", json_data={ "maintainer": b"Unic\xc3\xb3de ".decode( "UTF-8"), "hooks": {"test1-app": {"new": "target-1"}}}) self._make_installed_click("test-2", "2.0", json_data={ "maintainer": b"Unic\xc3\xb3de ".decode( "UTF-8"), "hooks": {"test1-app": {"new": "target-2"}}, }) hook = Click.Hook.open(self.db, "new") hook.install(user_name=None) path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new") self.assertTrue(os.path.lexists(path_1)) self.assertEqual( os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), os.readlink(path_1)) path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new") self.assertTrue(os.path.lexists(path_2)) self.assertEqual( os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), os.readlink(path_2)) def test_remove(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir( preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) self._make_hook_file( "Pattern: %s/${id}.old" % self.temp_dir, hookname="old") self._make_installed_click("test-1", "1.0", json_data={ "hooks": {"test1-app": {"old": "target-1"}}}) path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old") os.symlink( os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1) self._make_installed_click("test-2", "2.0", json_data={ "hooks": {"test2-app": {"old": "target-2"}}}) path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old") os.symlink( os.path.join(self.temp_dir, "test-2", "2.0", "target-2"), path_2) hook = Click.Hook.open(self.db, "old") hook.remove(user_name=None) self.assertFalse(os.path.exists(path_1)) self.assertFalse(os.path.exists(path_2)) def test_sync(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir( preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) self._make_hook_file( "Pattern: %s/${id}.test" % self.temp_dir) self._make_installed_click("test-1", "1.0", json_data={ "hooks": {"test1-app": {"test": "target-1"}}}) self._make_installed_click( "test-2", "1.0", make_current=False, json_data={"hooks": {"test2-app": {"test": "target-2"}}}) self._make_installed_click("test-2", "1.1", json_data={ "hooks": {"test2-app": {"test": "target-2"}}}) path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test") os.symlink( os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), path_1) path_2_1_0 = os.path.join( self.temp_dir, "test-2_test2-app_1.0.test") path_2_1_1 = os.path.join( self.temp_dir, "test-2_test2-app_1.1.test") path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test") os.symlink( os.path.join(self.temp_dir, "test-3", "1.0", "target-3"), path_3) hook = Click.Hook.open(self.db, "test") hook.sync(user_name=None) self.assertTrue(os.path.lexists(path_1)) self.assertEqual( os.path.join(self.temp_dir, "test-1", "1.0", "target-1"), os.readlink(path_1)) self.assertTrue(os.path.lexists(path_2_1_0)) self.assertEqual( os.path.join(self.temp_dir, "test-2", "1.0", "target-2"), os.readlink(path_2_1_0)) self.assertTrue(os.path.lexists(path_2_1_1)) self.assertEqual( os.path.join(self.temp_dir, "test-2", "1.1", "target-2"), os.readlink(path_2_1_1)) self.assertFalse(os.path.lexists(path_3)) class TestClickHookUserLevel(TestClickHookBase): def test_open(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ User-Level: yes Pattern: ${home}/.local/share/test/${id}.test # Comment Exec: test-update """)) hook = Click.Hook.open(self.db, "test") self.assertCountEqual( ["user-level", "pattern", "exec"], hook.get_fields()) self.assertEqual( "${home}/.local/share/test/${id}.test", hook.get_field("pattern")) self.assertEqual("test-update", hook.get_field("exec")) self.assertRaisesHooksError( Click.HooksError.MISSING_FIELD, hook.get_field, "nonexistent") self.assertTrue(hook.props.is_user_level) def test_hook_name_absent(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ User-Level: yes Pattern: ${home}/.local/share/test/${id}.test""")) hook = Click.Hook.open(self.db, "test") self.assertEqual("test", hook.get_hook_name()) def test_hook_name_present(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ User-Level: yes Pattern: ${home}/.local/share/test/${id}.test Hook-Name: other""")) hook = Click.Hook.open(self.db, "test") self.assertEqual("other", hook.get_hook_name()) def test_invalid_app_id(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() self._setup_hooks_dir(preloads) self._make_hook_file(dedent("""\ User-Level: yes Pattern: ${home}/.local/share/test/${id}.test # Comment Exec: test-update""")) hook = Click.Hook.open(self.db, "test") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_app_id, "package", "0.1", "app_name") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_app_id, "package", "0.1", "app/name") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_pattern, "package", "0.1", "app_name") self.assertRaisesHooksError( Click.HooksError.BAD_APP_NAME, hook.get_pattern, "package", "0.1", "app/name") def test_short_id_valid(self): with self.run_in_subprocess( "click_get_hooks_dir", "getpwnam") as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_dir=b"/mock"))) self._make_hook_file(dedent("""\ User-Level: yes Pattern: ${home}/.local/share/test/${short-id}.test """)) hook = Click.Hook.open(self.db, "test") self.assertEqual( "/mock/.local/share/test/package_app-name.test", hook.get_pattern( "package", "0.1", "app-name", user_name="mock")) def test_run_commands(self): with self.run_in_subprocess( "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"/bin/sh": 0}) with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("User-Level: yes", file=f) print("Exec: test-update", file=f) hook = Click.Hook.open(self.db, "test") self.assertEqual( self.TEST_USER, hook.get_run_commands_user(user_name=self.TEST_USER)) hook.run_commands(user_name=self.TEST_USER) self.assertEqual( [[b"/bin/sh", b"-c", b"test-update"]], self.spawn_calls) def test_run_commands_fail(self): with self.run_in_subprocess( "click_get_hooks_dir", "g_spawn_sync") as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["g_spawn_sync"].side_effect = partial( self.g_spawn_sync_side_effect, {b"/bin/sh": 1}) with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("User-Level: yes", file=f) print("Exec: test-update", file=f) hook = Click.Hook.open(self.db, "test") self.assertRaisesHooksError( Click.HooksError.COMMAND_FAILED, hook.run_commands, user_name=self.TEST_USER) def test_install_package(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) user_db = Click.User.for_user(self.db, self.TEST_USER) user_db.set_version("org.example.package", "1.0") self._make_hook_file(dedent("""\ User-Level: yes Pattern: %s/${id}.test""") % self.temp_dir) hook = Click.Hook.open(self.db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo/bar", user_name=self.TEST_USER) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") target_path = os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "org.example.package", "foo", "bar") self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(target_path, os.readlink(symlink_path)) def test_install_package_trailing_slash(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) user_db = Click.User.for_user(self.db, self.TEST_USER) user_db.set_version("org.example.package", "1.0") self._make_hook_file(dedent("""\ User-Level: yes Pattern: %s/${id}/""") % self.temp_dir) hook = Click.Hook.open(self.db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo", user_name=self.TEST_USER) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0") target_path = os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "org.example.package", "foo") self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(target_path, os.readlink(symlink_path)) def test_install_package_removes_previous(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.1")) user_db = Click.User.for_user(self.db, self.TEST_USER) user_db.set_version("org.example.package", "1.0") self._make_hook_file(dedent("""\ User-Level: yes Pattern: %s/${id}.test""") % self.temp_dir) hook = Click.Hook.open(self.db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo/bar", user_name=self.TEST_USER) hook.install_package( "org.example.package", "1.1", "test-app", "foo/bar", user_name=self.TEST_USER) old_symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.1.test") self.assertFalse(os.path.islink(old_symlink_path)) self.assertTrue(os.path.islink(symlink_path)) target_path = os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "org.example.package", "foo", "bar") self.assertEqual(target_path, os.readlink(symlink_path)) def test_upgrade(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") os.symlink("old-target", symlink_path) os.makedirs(os.path.join( self.temp_dir, "org.example.package", "1.0")) user_db = Click.User.for_user(self.db, self.TEST_USER) user_db.set_version("org.example.package", "1.0") self._make_hook_file(dedent("""\ User-Level: yes Pattern: %s/${id}.test""") % self.temp_dir) hook = Click.Hook.open(self.db, "test") hook.install_package( "org.example.package", "1.0", "test-app", "foo/bar", user_name=self.TEST_USER) target_path = os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "org.example.package", "foo", "bar") self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(target_path, os.readlink(symlink_path)) def test_remove_package(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" self._make_hook_file(dedent("""\ User-Level: yes Pattern: %s/${id}.test""") % self.temp_dir) symlink_path = os.path.join( self.temp_dir, "org.example.package_test-app_1.0.test") os.symlink("old-target", symlink_path) hook = Click.Hook.open(self.db, "test") hook.remove_package( "org.example.package", "1.0", "test-app", user_name=self.TEST_USER) self.assertFalse(os.path.exists(symlink_path)) def test_install(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", "getpwnam" ) as (enter, preloads): enter() # Don't tell click about the hooks directory yet. self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" preloads["getpwnam"].side_effect = ( lambda name: self.make_pointer(Passwd(pw_uid=1, pw_gid=1))) with mkfile(os.path.join(self.temp_dir, "hooks", "new.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.new" % self.temp_dir, file=f) self._make_installed_click("test-1", "1.0", json_data={ "maintainer": b"Unic\xc3\xb3de ".decode( "UTF-8"), "hooks": {"test1-app": {"new": "target-1"}}, }) self._make_installed_click("test-2", "2.0", json_data={ "maintainer": b"Unic\xc3\xb3de ".decode( "UTF-8"), "hooks": {"test1-app": {"new": "target-2"}}, }) # Now tell click about the hooks directory and make sure it # catches up correctly. self._setup_hooks_dir( preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) hook = Click.Hook.open(self.db, "new") hook.install(user_name=None) path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.new") self.assertTrue(os.path.lexists(path_1)) self.assertEqual( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-1", "target-1"), os.readlink(path_1)) path_2 = os.path.join(self.temp_dir, "test-2_test1-app_2.0.new") self.assertTrue(os.path.lexists(path_2)) self.assertEqual( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-2", "target-2"), os.readlink(path_2)) os.unlink(path_1) os.unlink(path_2) hook.install(user_name="another-user") self.assertFalse(os.path.lexists(path_1)) self.assertFalse(os.path.lexists(path_2)) hook.install(user_name=self.TEST_USER) self.assertTrue(os.path.lexists(path_1)) self.assertEqual( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-1", "target-1"), os.readlink(path_1)) self.assertTrue(os.path.lexists(path_2)) self.assertEqual( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-2", "target-2"), os.readlink(path_2)) def test_remove(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() # Don't tell click about the hooks directory yet. self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" with mkfile(os.path.join(self.temp_dir, "hooks", "old.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.old" % self.temp_dir, file=f) user_db = Click.User.for_user(self.db, self.TEST_USER) self._make_installed_click("test-1", "1.0", json_data={ "hooks": {"test1-app": {"old": "target-1"}}}) path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.old") os.symlink( os.path.join(user_db.get_path("test-1"), "target-1"), path_1) self._make_installed_click("test-2", "2.0", json_data={ "hooks": {"test2-app": {"old": "target-2"}}}) path_2 = os.path.join(self.temp_dir, "test-2_test2-app_2.0.old") os.symlink( os.path.join(user_db.get_path("test-2"), "target-2"), path_2) # Now tell click about the hooks directory and make sure it # catches up correctly. self._setup_hooks_dir( preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) hook = Click.Hook.open(self.db, "old") hook.remove(user_name=None) self.assertFalse(os.path.exists(path_1)) self.assertFalse(os.path.exists(path_2)) def test_sync(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() preloads["click_get_user_home"].return_value = b"/home/test-user" self._setup_hooks_dir(preloads) with mkfile( os.path.join(self.temp_dir, "hooks", "test.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.test" % self.temp_dir, file=f) self._make_installed_click("test-1", "1.0", json_data={ "hooks": {"test1-app": {"test": "target-1"}}}) self._make_installed_click("test-2", "1.1", json_data={ "hooks": {"test2-app": {"test": "target-2"}}}) path_1 = os.path.join(self.temp_dir, "test-1_test1-app_1.0.test") os.symlink( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-1", "target-1"), path_1) path_2 = os.path.join(self.temp_dir, "test-2_test2-app_1.1.test") path_3 = os.path.join(self.temp_dir, "test-3_test3-app_1.0.test") os.symlink( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-3", "target-3"), path_3) self._setup_hooks_dir( preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) hook = Click.Hook.open(self.db, "test") hook.sync(user_name=self.TEST_USER) self.assertTrue(os.path.lexists(path_1)) self.assertEqual( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-1", "target-1"), os.readlink(path_1)) self.assertTrue(os.path.lexists(path_2)) self.assertEqual( os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-2", "target-2"), os.readlink(path_2)) self.assertFalse(os.path.lexists(path_3)) def test_sync_without_user_db(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() preloads["click_get_user_home"].return_value = b"/home/test-user" self._setup_hooks_dir(preloads) with mkfile( os.path.join(self.temp_dir, "hooks", "test.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.test" % self.temp_dir, file=f) self._make_installed_click( "test-package", "1.0", all_users=True, json_data={ "hooks": {"test-app": {"test": "target"}}}) self._setup_hooks_dir( preloads, hooks_dir=os.path.join(self.temp_dir, "hooks")) hook = Click.Hook.open(self.db, "test") hook.sync(user_name=self.TEST_USER) self.assertFalse(os.path.exists(os.path.join( self.temp_dir, ".click", "users", self.TEST_USER, "test-package"))) def test_sync_uses_deepest_copy(self): # If the same version of a package is unpacked in multiple # databases, then we make sure the user link points to the deepest # copy, even if it already points somewhere else. It is important # to be consistent about this since system hooks may only have a # single target for any given application ID, and user links must # match system hooks so that (for example) the version of an # application run by a user has a matching system AppArmor profile. with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", ) as (enter, preloads): enter() self._setup_hooks_dir(preloads) preloads["click_get_user_home"].return_value = b"/home/test-user" with mkfile(os.path.join(self.temp_dir, "test.hook")) as f: print("User-Level: yes", file=f) print("Pattern: %s/${id}.test" % self.temp_dir, file=f) underlay = os.path.join(self.temp_dir, "underlay") overlay = os.path.join(self.temp_dir, "overlay") db = Click.DB() db.add(underlay) db.add(overlay) underlay_unpacked = os.path.join(underlay, "test-package", "1.0") overlay_unpacked = os.path.join(overlay, "test-package", "1.0") os.makedirs(underlay_unpacked) os.makedirs(overlay_unpacked) manifest = {"hooks": {"test-app": {"test": "foo"}}} with mkfile(os.path.join( underlay_unpacked, ".click", "info", "test-package.manifest")) as f: json.dump(manifest, f) with mkfile(os.path.join( overlay_unpacked, ".click", "info", "test-package.manifest")) as f: json.dump(manifest, f) underlay_user_link = os.path.join( underlay, ".click", "users", "@all", "test-package") overlay_user_link = os.path.join( overlay, ".click", "users", self.TEST_USER, "test-package") Click.ensuredir(os.path.dirname(underlay_user_link)) os.symlink(underlay_unpacked, underlay_user_link) Click.ensuredir(os.path.dirname(overlay_user_link)) os.symlink(overlay_unpacked, overlay_user_link) symlink_path = os.path.join( self.temp_dir, "test-package_test-app_1.0.test") underlay_target_path = os.path.join(underlay_user_link, "foo") overlay_target_path = os.path.join(overlay_user_link, "foo") os.symlink(overlay_target_path, symlink_path) hook = Click.Hook.open(db, "test") hook.sync(user_name=self.TEST_USER) self.assertTrue(os.path.islink(underlay_user_link)) self.assertEqual( underlay_unpacked, os.readlink(underlay_user_link)) self.assertFalse(os.path.islink(overlay_user_link)) self.assertTrue(os.path.islink(symlink_path)) self.assertEqual(underlay_target_path, os.readlink(symlink_path)) class TestPackageInstallHooks(TestClickHookBase): def test_removes_old_hooks(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() hooks_dir = os.path.join(self.temp_dir, "hooks") self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) with mkfile(os.path.join(hooks_dir, "unity.hook")) as f: print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f) print("Single-Version: yes", file=f) with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f: print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f) print("Single-Version: yes", file=f) print("Hook-Name: yelp", file=f) with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f: print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f) print("Single-Version: yes", file=f) print("Hook-Name: yelp", file=f) os.mkdir(os.path.join(self.temp_dir, "unity")) unity_path = os.path.join( self.temp_dir, "unity", "test_app_1.0.scope") os.symlink("dummy", unity_path) os.mkdir(os.path.join(self.temp_dir, "yelp")) yelp_docs_path = os.path.join( self.temp_dir, "yelp", "docs-test_app_1.0.txt") os.symlink("dummy", yelp_docs_path) yelp_other_path = os.path.join( self.temp_dir, "yelp", "other-test_app_1.0.txt") os.symlink("dummy", yelp_other_path) self._make_installed_click("test", "1.0", make_current=False, json_data={ "hooks": {"app": {"yelp": "foo.txt", "unity": "foo.scope"}}}) self._make_installed_click("test", "1.1", json_data={}) Click.package_install_hooks( self.db, "test", "1.0", "1.1", user_name=None) self.assertFalse(os.path.lexists(unity_path)) self.assertFalse(os.path.lexists(yelp_docs_path)) self.assertFalse(os.path.lexists(yelp_other_path)) def test_installs_new_hooks(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() hooks_dir = os.path.join(self.temp_dir, "hooks") self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) with mkfile(os.path.join(hooks_dir, "a.hook")) as f: print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f) with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f: print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f) print("Hook-Name: b", file=f) with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f: print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f) print("Hook-Name: b", file=f) os.mkdir(os.path.join(self.temp_dir, "a")) os.mkdir(os.path.join(self.temp_dir, "b")) self._make_installed_click( "test", "1.0", make_current=False, json_data={"hooks": {}}) self._make_installed_click("test", "1.1", json_data={ "hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}) Click.package_install_hooks( self.db, "test", "1.0", "1.1", user_name=None) self.assertTrue(os.path.lexists( os.path.join(self.temp_dir, "a", "test_app_1.1.a"))) self.assertTrue(os.path.lexists( os.path.join(self.temp_dir, "b", "1-test_app_1.1.b"))) self.assertTrue(os.path.lexists( os.path.join(self.temp_dir, "b", "2-test_app_1.1.b"))) def test_upgrades_existing_hooks(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() hooks_dir = os.path.join(self.temp_dir, "hooks") self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) with mkfile(os.path.join(hooks_dir, "a.hook")) as f: print("Pattern: %s/a/${id}.a" % self.temp_dir, file=f) print("Single-Version: yes", file=f) with mkfile(os.path.join(hooks_dir, "b-1.hook")) as f: print("Pattern: %s/b/1-${id}.b" % self.temp_dir, file=f) print("Single-Version: yes", file=f) print("Hook-Name: b", file=f) with mkfile(os.path.join(hooks_dir, "b-2.hook")) as f: print("Pattern: %s/b/2-${id}.b" % self.temp_dir, file=f) print("Single-Version: yes", file=f) print("Hook-Name: b", file=f) with mkfile(os.path.join(hooks_dir, "c.hook")) as f: print("Pattern: %s/c/${id}.c" % self.temp_dir, file=f) print("Single-Version: yes", file=f) os.mkdir(os.path.join(self.temp_dir, "a")) a_path = os.path.join(self.temp_dir, "a", "test_app_1.0.a") os.symlink("dummy", a_path) os.mkdir(os.path.join(self.temp_dir, "b")) b_irrelevant_path = os.path.join( self.temp_dir, "b", "1-test_other-app_1.0.b") os.symlink("dummy", b_irrelevant_path) b_1_path = os.path.join(self.temp_dir, "b", "1-test_app_1.0.b") os.symlink("dummy", b_1_path) b_2_path = os.path.join(self.temp_dir, "b", "2-test_app_1.0.b") os.symlink("dummy", b_2_path) os.mkdir(os.path.join(self.temp_dir, "c")) package_dir = os.path.join(self.temp_dir, "test") with mkfile(os.path.join( package_dir, "1.0", ".click", "info", "test.manifest")) as f: json.dump({"hooks": {"app": {"a": "foo.a", "b": "foo.b"}}}, f) with mkfile(os.path.join( package_dir, "1.1", ".click", "info", "test.manifest")) as f: json.dump( {"hooks": { "app": {"a": "foo.a", "b": "foo.b", "c": "foo.c"}} }, f) Click.package_install_hooks( self.db, "test", "1.0", "1.1", user_name=None) self.assertFalse(os.path.lexists(a_path)) self.assertTrue(os.path.lexists(b_irrelevant_path)) self.assertFalse(os.path.lexists(b_1_path)) self.assertFalse(os.path.lexists(b_2_path)) self.assertTrue(os.path.lexists( os.path.join(self.temp_dir, "a", "test_app_1.1.a"))) self.assertTrue(os.path.lexists( os.path.join(self.temp_dir, "b", "1-test_app_1.1.b"))) self.assertTrue(os.path.lexists( os.path.join(self.temp_dir, "b", "2-test_app_1.1.b"))) self.assertTrue(os.path.lexists( os.path.join(self.temp_dir, "c", "test_app_1.1.c"))) class TestPackageRemoveHooks(TestClickHookBase): def test_removes_hooks(self): with self.run_in_subprocess( "click_get_hooks_dir") as (enter, preloads): enter() hooks_dir = os.path.join(self.temp_dir, "hooks") self._setup_hooks_dir(preloads, hooks_dir=hooks_dir) with mkfile(os.path.join(hooks_dir, "unity.hook")) as f: print("Pattern: %s/unity/${id}.scope" % self.temp_dir, file=f) with mkfile(os.path.join(hooks_dir, "yelp-docs.hook")) as f: print("Pattern: %s/yelp/docs-${id}.txt" % self.temp_dir, file=f) print("Hook-Name: yelp", file=f) with mkfile(os.path.join(hooks_dir, "yelp-other.hook")) as f: print("Pattern: %s/yelp/other-${id}.txt" % self.temp_dir, file=f) print("Hook-Name: yelp", file=f) os.mkdir(os.path.join(self.temp_dir, "unity")) unity_path = os.path.join( self.temp_dir, "unity", "test_app_1.0.scope") os.symlink("dummy", unity_path) os.mkdir(os.path.join(self.temp_dir, "yelp")) yelp_docs_path = os.path.join( self.temp_dir, "yelp", "docs-test_app_1.0.txt") os.symlink("dummy", yelp_docs_path) yelp_other_path = os.path.join( self.temp_dir, "yelp", "other-test_app_1.0.txt") os.symlink("dummy", yelp_other_path) package_dir = os.path.join(self.temp_dir, "test") with mkfile(os.path.join( package_dir, "1.0", ".click", "info", "test.manifest")) as f: json.dump( {"hooks": { "app": {"yelp": "foo.txt", "unity": "foo.scope"}} }, f) Click.package_remove_hooks(self.db, "test", "1.0", user_name=None) self.assertFalse(os.path.lexists(unity_path)) self.assertFalse(os.path.lexists(yelp_docs_path)) self.assertFalse(os.path.lexists(yelp_other_path)) class TestPackageHooksValidateFramework(TestClickHookBase): def _setup_test_env(self, preloads): preloads["click_get_user_home"].return_value = b"/home/test-user" self._setup_hooks_dir( preloads, os.path.join(self.temp_dir, "hooks")) self._make_hook_file(dedent("""\ User-Level: yes Pattern: %s/${id}.test """) % self.temp_dir) self.hook_symlink_path = os.path.join( self.temp_dir, "test-1_test1-app_1.0.test") def test_links_are_kept_on_validate_framework(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", "click_get_frameworks_dir", ) as (enter, preloads): enter() self._setup_frameworks( preloads, frameworks=["ubuntu-sdk-13.10"]) self._setup_test_env(preloads) self._make_installed_click(json_data={ "framework": "ubuntu-sdk-13.10", "hooks": { "test1-app": {"test": "target-1"} }, }) self.assertTrue(os.path.lexists(self.hook_symlink_path)) # run the hooks Click.run_user_hooks(self.db, user_name=self.TEST_USER) self.assertTrue(os.path.lexists(self.hook_symlink_path)) def test_links_are_kept_multiple_frameworks(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", "click_get_frameworks_dir", ) as (enter, preloads): enter() self._setup_frameworks( preloads, frameworks=["ubuntu-sdk-14.04", "ubuntu-sdk-13.10"]) self._setup_test_env(preloads) self._make_installed_click(json_data={ "framework": "ubuntu-sdk-13.10", "hooks": { "test1-app": {"test": "target-1"} }, }) self.assertTrue(os.path.lexists(self.hook_symlink_path)) # run the hooks Click.run_user_hooks(self.db, user_name=self.TEST_USER) self.assertTrue(os.path.lexists(self.hook_symlink_path)) def test_links_are_removed_on_missing_framework(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", "click_get_frameworks_dir", ) as (enter, preloads): enter() self._setup_frameworks(preloads, frameworks=["missing"]) self._setup_test_env(preloads) self._make_installed_click(json_data={ "framework": "ubuntu-sdk-13.10", "hooks": { "test1-app": {"test": "target-1"} }, }) self.assertTrue(os.path.lexists(self.hook_symlink_path)) # run the hooks Click.run_user_hooks(self.db, user_name=self.TEST_USER) self.assertFalse(os.path.lexists(self.hook_symlink_path)) def test_links_are_removed_on_missing_multiple_framework(self): with self.run_in_subprocess( "click_get_hooks_dir", "click_get_user_home", "click_get_frameworks_dir", ) as (enter, preloads): enter() self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self._setup_test_env(preloads) self._make_installed_click(json_data={ "framework": "ubuntu-sdk-13.10, ubuntu-sdk-13.10-html", "hooks": { "test1-app": {"test": "target-1"} }, }) self.assertTrue(os.path.lexists(self.hook_symlink_path)) # run the hooks Click.run_user_hooks(self.db, user_name=self.TEST_USER) self.assertFalse(os.path.lexists(self.hook_symlink_path)) click-0.5.0/click_package/tests/test_install.py000066400000000000000000001013541402441472600215720ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.install.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickInstaller', ] from contextlib import ( closing, contextmanager, ) import hashlib import json import os import shutil import stat import subprocess import tarfile from unittest import skip, skipUnless from debian.deb822 import Deb822 from gi.repository import Click from click_package.arfile import ArFile from click_package.build import ClickBuilder from click_package.install import ( ClickInstaller, ClickInstallerAuditError, ClickInstallerPermissionDenied, ) from click_package.preinst import static_preinst from click_package.tests.helpers import ( disable_logging, mkfile, mock, TestCase, touch, ) from click_package.versions import spec_version @contextmanager def mock_quiet_subprocess_call(): original_call = subprocess.call def side_effect(*args, **kwargs): if "TEST_VERBOSE" in os.environ: return original_call(*args, **kwargs) else: with open("/dev/null", "w") as devnull: return original_call( *args, stdout=devnull, stderr=devnull, **kwargs) with mock.patch("subprocess.call") as mock_call: mock_call.side_effect = side_effect yield mock_call @skip('Tests failing on 20.04 due to preload hack not working.') class TestClickInstaller(TestCase): def setUp(self): super(TestClickInstaller, self).setUp() self.use_temp_dir() self.db = Click.DB() self.db.add(self.temp_dir) # mock signature checks during the tests self.debsig_patcher = mock.patch("click_package.install.DebsigVerify") self.debsig_patcher.start() def tearDown(self): self.debsig_patcher.stop() def make_fake_package(self, control_fields=None, manifest=None, control_scripts=None, data_files=None): """Build a fake package with given contents.""" control_fields = {} if control_fields is None else control_fields control_scripts = {} if control_scripts is None else control_scripts data_files = {} if data_files is None else data_files data_dir = os.path.join(self.temp_dir, "fake-package") control_dir = os.path.join(self.temp_dir, "DEBIAN") with mkfile(os.path.join(control_dir, "control")) as control: for key, value in control_fields.items(): print('%s: %s' % (key.title(), value), file=control) print(file=control) if manifest is not None: with mkfile(os.path.join(control_dir, "manifest")) as f: json.dump(manifest, f) print(file=f) for name, contents in control_scripts.items(): with mkfile(os.path.join(control_dir, name)) as script: script.write(contents) Click.ensuredir(data_dir) for name, path in data_files.items(): Click.ensuredir(os.path.dirname(os.path.join(data_dir, name))) if path is None: touch(os.path.join(data_dir, name)) elif os.path.isdir(path): shutil.copytree(path, os.path.join(data_dir, name)) else: shutil.copy2(path, os.path.join(data_dir, name)) package_path = '%s.click' % data_dir ClickBuilder()._pack( self.temp_dir, control_dir, data_dir, package_path) return package_path def test_audit_no_click_version(self): path = self.make_fake_package() self.assertRaisesRegex( ClickInstallerAuditError, "No Click-Version field", ClickInstaller(self.db).audit, path) def test_audit_bad_click_version(self): path = self.make_fake_package(control_fields={"Click-Version": "|"}) self.assertRaises(ValueError, ClickInstaller(self.db).audit, path) def test_audit_new_click_version(self): path = self.make_fake_package(control_fields={"Click-Version": "999"}) self.assertRaisesRegex( ClickInstallerAuditError, "Click-Version: 999 newer than maximum supported version .*", ClickInstaller(self.db).audit, path) def test_audit_forbids_depends(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={ "Click-Version": "0.2", "Depends": "libc6", }) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaisesRegex( ClickInstallerAuditError, "Depends field is forbidden in Click packages", ClickInstaller(self.db).audit, path) def test_audit_forbids_maintscript(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, control_scripts={ "preinst": "#! /bin/sh\n", "postinst": "#! /bin/sh\n", }) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaisesRegex( ClickInstallerAuditError, r"Maintainer scripts are forbidden in Click packages " r"\(found: postinst preinst\)", ClickInstaller(self.db).audit, path) def test_audit_requires_manifest(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, control_scripts={"preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaisesRegex( ClickInstallerAuditError, "Package has no manifest", ClickInstaller(self.db).audit, path) def test_audit_invalid_manifest_json(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, control_scripts={"manifest": "{", "preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) self.assertRaises(ValueError, ClickInstaller(self.db).audit, path) def test_audit_no_name(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={}) self.assertRaisesRegex( ClickInstallerAuditError, 'No "name" entry in manifest', ClickInstaller(self.db).audit, path) def test_audit_name_bad_character(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={"name": "../evil"}) self.assertRaisesRegex( ClickInstallerAuditError, 'Invalid character "/" in "name" entry: ../evil', ClickInstaller(self.db).audit, path) def test_audit_no_version(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={"name": "test-package"}) self.assertRaisesRegex( ClickInstallerAuditError, 'No "version" entry in manifest', ClickInstaller(self.db).audit, path) def test_audit_no_framework(self): path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={"name": "test-package", "version": "1.0"}, control_scripts={"preinst": static_preinst}) self.assertRaisesRegex( ClickInstallerAuditError, 'No "framework" entry in manifest', ClickInstaller(self.db).audit, path) def test_audit_missing_framework(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "missing", }, control_scripts={"preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["present"]) self.assertRaisesRegex( ClickInstallerAuditError, 'Framework "missing" not present on system.*', ClickInstaller(self.db).audit, path) # FIXME: we really want a unit test with a valid signature too def test_audit_no_signature(self): if not Click.find_on_path("debsig-verify"): self.skipTest("this test needs debsig-verify") path = self.make_fake_package( control_fields={"Click-Version": "0.4"}, manifest={ "name": "test-package", "version": "1.0", "framework": "", }) self.debsig_patcher.stop() self.assertRaisesRegex( ClickInstallerAuditError, "Signature verification error", ClickInstaller(self.db).audit, path) self.debsig_patcher.start() @disable_logging def test_audit_missing_framework_force(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "missing", }) self._setup_frameworks(preloads, frameworks=["present"]) ClickInstaller(self.db, True).audit(path) def test_audit_passes_correct_package(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) installer = ClickInstaller(self.db) self.assertEqual(("test-package", "1.0"), installer.audit(path)) def test_audit_multiple_frameworks(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.4"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-14.04-basic, ubuntu-sdk-14.04-webapps", }, control_scripts={"preinst": static_preinst}) installer = ClickInstaller(self.db) self._setup_frameworks(preloads, frameworks=["dummy"]) self.assertRaisesRegex( ClickInstallerAuditError, 'Frameworks "ubuntu-sdk-14.04-basic", ' '"ubuntu-sdk-14.04-webapps" not present on system.*', installer.audit, path) self._setup_frameworks( preloads, frameworks=["dummy", "ubuntu-sdk-14.04-basic"]) self.assertRaisesRegex( ClickInstallerAuditError, 'Framework "ubuntu-sdk-14.04-webapps" not present on ' 'system.*', installer.audit, path) self._setup_frameworks( preloads, frameworks=[ "dummy", "ubuntu-sdk-14.04-basic", "ubuntu-sdk-14.04-webapps", ]) self.assertEqual(("test-package", "1.0"), installer.audit(path)) def test_audit_missing_dot_slash(self): # Manually construct a package with data paths that do not start # with "./", which could be used to bypass path filtering. with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={".click/tmp.ci/manifest": None}) # Repack without the leading "./". data_dir = os.path.join(self.temp_dir, "fake-package") data_tar_path = os.path.join(self.temp_dir, "data.tar.gz") control_tar_path = os.path.join(self.temp_dir, "control.tar.gz") package_path = '%s.click' % data_dir with closing(tarfile.TarFile.open( name=data_tar_path, mode="w:gz", format=tarfile.GNU_FORMAT )) as data_tar: data_tar.add( os.path.join(data_dir, ".click"), arcname=".click") with ArFile(name=package_path, mode="w") as package: package.add_magic() package.add_data("debian-binary", b"2.0\n") package.add_data( "_click-binary", ("%s\n" % spec_version).encode("UTF-8")) package.add_file("control.tar.gz", control_tar_path) package.add_file("data.tar.gz", data_tar_path) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer = ClickInstaller(self.db) self.assertRaisesRegex( ClickInstallerAuditError, 'File name ".click" in package does not start with "./"', installer.audit, path) def test_audit_broken_md5sums(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={ "preinst": static_preinst, "md5sums": "%s foo" % ("0" * 32), }, data_files={"foo": None}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer = ClickInstaller(self.db) self.assertRaises( subprocess.CalledProcessError, installer.audit, path, slow=True) def test_audit_matching_md5sums(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() data_path = os.path.join(self.temp_dir, "foo") with mkfile(data_path) as data: print("test", file=data) with open(data_path, "rb") as data: data_md5sum = hashlib.md5(data.read()).hexdigest() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={ "preinst": static_preinst, "md5sums": "%s foo" % data_md5sum, }, data_files={"foo": data_path}) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer = ClickInstaller(self.db) self.assertEqual( ("test-package", "1.0"), installer.audit(path, slow=True)) def test_no_write_permission(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={"Click-Version": "0.2"}, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}) write_mask = ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) installer = ClickInstaller(self.db) temp_dir_mode = os.stat(self.temp_dir).st_mode try: os.chmod(self.temp_dir, temp_dir_mode & write_mask) self.assertRaises( ClickInstallerPermissionDenied, installer.install, path) finally: os.chmod(self.temp_dir, temp_dir_mode) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") def test_install(self, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.0", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertCountEqual([".click", "test-package"], os.listdir(root)) package_dir = os.path.join(root, "test-package") self.assertCountEqual(["1.0", "current"], os.listdir(package_dir)) inst_dir = os.path.join(package_dir, "current") self.assertTrue(os.path.islink(inst_dir)) self.assertEqual("1.0", os.readlink(inst_dir)) self.assertCountEqual([".click", "foo"], os.listdir(inst_dir)) status_path = os.path.join(inst_dir, ".click", "status") with open(status_path) as status_file: # .readlines() avoids the need for a python-apt backport to # Ubuntu 12.04 LTS. status = list(Deb822.iter_paragraphs(status_file.readlines())) self.assertEqual(1, len(status)) self.assertEqual({ "Package": "test-package", "Status": "install ok installed", "Version": "1.0", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, status[0]) mock_package_install_hooks.assert_called_once_with( db, "test-package", None, "1.0", user_name=None) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") def test_sandbox(self): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() original_call = subprocess.check_output def call_side_effect(*args, **kwargs): return original_call( ["touch", os.path.join(self.temp_dir, "sentinel")], **kwargs) path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.0", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.0", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock.patch("subprocess.check_output") as mock_call: mock_call.side_effect = call_side_effect self.assertRaises( subprocess.CalledProcessError, installer.install, path) self.assertFalse( os.path.exists(os.path.join(self.temp_dir, "sentinel"))) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") def test_upgrade(self, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() os.environ["TEST_QUIET"] = "1" path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={"foo": None}) root = os.path.join(self.temp_dir, "root") package_dir = os.path.join(root, "test-package") inst_dir = os.path.join(package_dir, "current") os.makedirs(os.path.join(package_dir, "1.0")) os.symlink("1.0", inst_dir) db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertCountEqual([".click", "test-package"], os.listdir(root)) self.assertCountEqual(["1.1", "current"], os.listdir(package_dir)) self.assertTrue(os.path.islink(inst_dir)) self.assertEqual("1.1", os.readlink(inst_dir)) self.assertCountEqual([".click", "foo"], os.listdir(inst_dir)) status_path = os.path.join(inst_dir, ".click", "status") with open(status_path) as status_file: # .readlines() avoids the need for a python-apt backport to # Ubuntu 12.04 LTS. status = list(Deb822.iter_paragraphs(status_file.readlines())) self.assertEqual(1, len(status)) self.assertEqual({ "Package": "test-package", "Status": "install ok installed", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, status[0]) mock_package_install_hooks.assert_called_once_with( db, "test-package", "1.0", "1.1", user_name=None) def _get_mode(self, path): return stat.S_IMODE(os.stat(path).st_mode) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") def test_world_readable(self, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() owner_only_file = os.path.join(self.temp_dir, "owner-only-file") touch(owner_only_file) os.chmod(owner_only_file, stat.S_IRUSR | stat.S_IWUSR) owner_only_dir = os.path.join(self.temp_dir, "owner-only-dir") os.mkdir(owner_only_dir, stat.S_IRWXU) path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}, data_files={ "world-readable-file": owner_only_file, "world-readable-dir": owner_only_dir, }) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) inst_dir = os.path.join(root, "test-package", "current") self.assertEqual( stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH, self._get_mode(os.path.join(inst_dir, "world-readable-file"))) self.assertEqual( stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH, self._get_mode(os.path.join(inst_dir, "world-readable-dir"))) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") @mock.patch("click_package.install.ClickInstaller._dpkg_architecture") def test_single_architecture(self, mock_dpkg_architecture, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() mock_dpkg_architecture.return_value = "armhf" path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "armhf", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", "architecture": "armhf", }, control_scripts={"preinst": static_preinst}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertTrue( os.path.exists(os.path.join(root, "test-package", "current"))) @skipUnless( os.path.exists(ClickInstaller(None)._preload_path()), "preload bits not built; installing packages will fail") @mock.patch("gi.repository.Click.package_install_hooks") @mock.patch("click_package.install.ClickInstaller._dpkg_architecture") def test_multiple_architectures(self, mock_dpkg_architecture, mock_package_install_hooks): with self.run_in_subprocess( "click_get_frameworks_dir") as (enter, preloads): enter() mock_dpkg_architecture.return_value = "armhf" path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "multi", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.2", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", "architecture": ["armhf", "i386"], }, control_scripts={"preinst": static_preinst}) root = os.path.join(self.temp_dir, "root") db = Click.DB() db.add(root) installer = ClickInstaller(db) self._setup_frameworks(preloads, frameworks=["ubuntu-sdk-13.10"]) with mock_quiet_subprocess_call(): installer.install(path) self.assertTrue( os.path.exists(os.path.join(root, "test-package", "current"))) @disable_logging def test_reinstall_preinstalled(self): # Attempting to reinstall a preinstalled version shouldn't actually # reinstall it in an overlay database (which would cause # irreconcilable confusion about the correct target for system hook # symlinks), but should instead simply update the user registration. path = self.make_fake_package( control_fields={ "Package": "test-package", "Version": "1.1", "Architecture": "all", "Maintainer": "Foo Bar ", "Description": "test", "Click-Version": "0.4", }, manifest={ "name": "test-package", "version": "1.1", "framework": "ubuntu-sdk-13.10", }, control_scripts={"preinst": static_preinst}) underlay = os.path.join(self.temp_dir, "underlay") overlay = os.path.join(self.temp_dir, "overlay") db = Click.DB() db.add(underlay) installer = ClickInstaller(db, True) with mock_quiet_subprocess_call(): installer.install(path, all_users=True) underlay_unpacked = os.path.join(underlay, "test-package", "1.1") self.assertTrue(os.path.exists(underlay_unpacked)) all_link = os.path.join( underlay, ".click", "users", "@all", "test-package") self.assertTrue(os.path.islink(all_link)) self.assertEqual(underlay_unpacked, os.readlink(all_link)) db.add(overlay) registry = Click.User.for_user(db, "test-user") registry.remove("test-package") user_link = os.path.join( overlay, ".click", "users", "test-user", "test-package") self.assertTrue(os.path.islink(user_link)) self.assertEqual("@hidden", os.readlink(user_link)) installer = ClickInstaller(db, True) with mock_quiet_subprocess_call(): installer.install(path, user="test-user") overlay_unpacked = os.path.join(overlay, "test-package", "1.1") self.assertFalse(os.path.exists(overlay_unpacked)) self.assertEqual("1.1", registry.get_version("test-package")) click-0.5.0/click_package/tests/test_osextras.py000066400000000000000000000153021402441472600217710ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.osextras.""" from __future__ import print_function __all__ = [ 'TestOSExtrasNative', 'TestOSExtrasPython', ] import os from gi.repository import Click, GLib from click_package import osextras from click_package.tests.helpers import TestCase, mock, touch class TestOSExtrasBaseMixin: def test_ensuredir_previously_missing(self): new_dir = os.path.join(self.temp_dir, "dir") self.mod.ensuredir(new_dir) self.assertTrue(os.path.isdir(new_dir)) def test_ensuredir_previously_present(self): new_dir = os.path.join(self.temp_dir, "dir") os.mkdir(new_dir) self.mod.ensuredir(new_dir) self.assertTrue(os.path.isdir(new_dir)) def test_find_on_path_missing_environment(self): os.environ.pop("PATH", None) self.assertFalse(self.mod.find_on_path("ls")) def test_find_on_path_present_executable(self): bin_dir = os.path.join(self.temp_dir, "bin") program = os.path.join(bin_dir, "program") touch(program) os.chmod(program, 0o755) os.environ["PATH"] = bin_dir self.assertTrue(self.mod.find_on_path("program")) def test_find_on_path_present_not_executable(self): bin_dir = os.path.join(self.temp_dir, "bin") touch(os.path.join(bin_dir, "program")) os.environ["PATH"] = bin_dir self.assertFalse(self.mod.find_on_path("program")) def test_find_on_path_requires_regular_file(self): bin_dir = os.path.join(self.temp_dir, "bin") self.mod.ensuredir(os.path.join(bin_dir, "subdir")) os.environ["PATH"] = bin_dir self.assertFalse(self.mod.find_on_path("subdir")) def test_unlink_file_present(self): path = os.path.join(self.temp_dir, "file") touch(path) self.mod.unlink_force(path) self.assertFalse(os.path.exists(path)) def test_unlink_file_missing(self): path = os.path.join(self.temp_dir, "file") self.mod.unlink_force(path) self.assertFalse(os.path.exists(path)) def test_symlink_file_present(self): path = os.path.join(self.temp_dir, "link") touch(path) self.mod.symlink_force("source", path) self.assertTrue(os.path.islink(path)) self.assertEqual("source", os.readlink(path)) def test_symlink_link_present(self): path = os.path.join(self.temp_dir, "link") os.symlink("old", path) self.mod.symlink_force("source", path) self.assertTrue(os.path.islink(path)) self.assertEqual("source", os.readlink(path)) def test_symlink_missing(self): path = os.path.join(self.temp_dir, "link") self.mod.symlink_force("source", path) self.assertTrue(os.path.islink(path)) self.assertEqual("source", os.readlink(path)) def test_umask(self): old_mask = os.umask(0o040) try: self.assertEqual(0o040, self.mod.get_umask()) os.umask(0o002) self.assertEqual(0o002, self.mod.get_umask()) finally: os.umask(old_mask) class TestOSExtrasNative(TestCase, TestOSExtrasBaseMixin): def setUp(self): super(TestOSExtrasNative, self).setUp() self.use_temp_dir() self.mod = Click def test_ensuredir_error(self): path = os.path.join(self.temp_dir, "file") touch(path) self.assertRaisesFileError(mock.ANY, self.mod.ensuredir, path) def test_dir_read_name_directory_present(self): new_dir = os.path.join(self.temp_dir, "dir") touch(os.path.join(new_dir, "file")) d = Click.Dir.open(new_dir, 0) self.assertEqual("file", d.read_name()) self.assertIsNone(d.read_name()) def test_dir_read_name_directory_missing(self): new_dir = os.path.join(self.temp_dir, "dir") d = Click.Dir.open(new_dir, 0) self.assertIsNone(d.read_name()) def test_dir_open_error(self): not_dir = os.path.join(self.temp_dir, "file") touch(not_dir) self.assertRaisesFileError( GLib.FileError.NOTDIR, Click.Dir.open, not_dir, 0) def test_unlink_error(self): path = os.path.join(self.temp_dir, "dir") os.mkdir(path) self.assertRaisesFileError(mock.ANY, self.mod.unlink_force, path) def test_symlink_unlink_error(self): path = os.path.join(self.temp_dir, "dir") os.mkdir(path) self.assertRaisesFileError( mock.ANY, self.mod.symlink_force, "source", path) def test_symlink_error(self): path = os.path.join(self.temp_dir, "dir", "file") self.assertRaisesFileError( mock.ANY, self.mod.symlink_force, "source", path) class TestOSExtrasPython(TestCase, TestOSExtrasBaseMixin): def setUp(self): super(TestOSExtrasPython, self).setUp() self.use_temp_dir() self.mod = osextras def test_ensuredir_oserror(self): path = os.path.join(self.temp_dir, "file") touch(path) self.assertRaises(OSError, self.mod.ensuredir, path) def test_listdir_directory_present(self): new_dir = os.path.join(self.temp_dir, "dir") touch(os.path.join(new_dir, "file")) self.assertEqual(["file"], osextras.listdir_force(new_dir)) def test_listdir_directory_missing(self): new_dir = os.path.join(self.temp_dir, "dir") self.assertEqual([], osextras.listdir_force(new_dir)) def test_listdir_oserror(self): not_dir = os.path.join(self.temp_dir, "file") touch(not_dir) self.assertRaises(OSError, osextras.listdir_force, not_dir) def test_unlink_oserror(self): path = os.path.join(self.temp_dir, "dir") os.mkdir(path) self.assertRaises(OSError, self.mod.unlink_force, path) def test_symlink_unlink_oserror(self): path = os.path.join(self.temp_dir, "dir") os.mkdir(path) self.assertRaises(OSError, self.mod.symlink_force, "source", path) def test_symlink_oserror(self): path = os.path.join(self.temp_dir, "dir", "file") self.assertRaises(OSError, self.mod.symlink_force, "source", path) click-0.5.0/click_package/tests/test_paths.py.in000066400000000000000000000024451402441472600216510ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click.paths. This is mostly just to reduce noise in the coverage report. """ from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickPaths', ] from gi.repository import Click from click_package.tests.helpers import TestCase class TestClickPaths(TestCase): def test_get_hooks_dir(self): self.assertEqual("@pkgdatadir@/hooks", Click.get_hooks_dir()) def test_get_db_dir(self): self.assertEqual("@sysconfdir@/click/databases", Click.get_db_dir()) def test_get_frameworks_dir(self): self.assertEqual("@pkgdatadir@/frameworks", Click.get_frameworks_dir()) click-0.5.0/click_package/tests/test_query.py000066400000000000000000000032601402441472600212660ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.query.""" from __future__ import print_function __all__ = [ 'TestQuery', ] import os from gi.repository import Click from click_package.tests.helpers import TestCase, touch class TestQuery(TestCase): def setUp(self): super(TestQuery, self).setUp() self.use_temp_dir() def test_find_package_directory_missing(self): path = os.path.join(self.temp_dir, "nonexistent") self.assertRaisesQueryError( Click.QueryError.PATH, Click.find_package_directory, path) def test_find_package_directory(self): info = os.path.join(self.temp_dir, ".click", "info") path = os.path.join(self.temp_dir, "file") Click.ensuredir(info) touch(path) pkgdir = Click.find_package_directory(path) self.assertEqual(self.temp_dir, pkgdir) def test_find_package_directory_outside(self): self.assertRaisesQueryError( Click.QueryError.NO_PACKAGE_DIR, Click.find_package_directory, "/bin") click-0.5.0/click_package/tests/test_scripts.py000066400000000000000000000033451402441472600216140ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Test that all top-level scripts work.""" __metaclass__ = type __all__ = [ 'TestScripts', ] import os import subprocess from unittest import skipIf from click_package.tests.helpers import TestCase class TestScripts(TestCase): @skipIf('SKIP_SLOW_TESTS' in os.environ, 'Skipping slow tests') def test_scripts(self): self.longMessage = True paths = [] for dirpath, _, filenames in os.walk("bin"): filenames = [ n for n in filenames if not n.startswith(".") and not n.endswith("~")] for filename in filenames: paths.append(os.path.join(dirpath, filename)) for path in paths: subp = subprocess.Popen( [path, "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) err = subp.communicate()[1] self.assertEqual("", err, "%s --help produced error output" % path) self.assertEqual( 0, subp.returncode, "%s --help exited non-zero" % path) click-0.5.0/click_package/tests/test_static.py000066400000000000000000000052421402441472600214120ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Test compliance with various static analysis tools.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestStatic', ] import os import sys from unittest import skipIf from pkg_resources import resource_filename try: import pep8 except ImportError: pep8 = None try: import pyflakes import pyflakes.api import pyflakes.reporter except ImportError: pyflakes = None from click_package.tests.helpers import TestCase class TestStatic(TestCase): def all_paths(self): paths = [] start_dir = os.path.dirname(resource_filename('click_package', '__init__.py')) for dirpath, dirnames, filenames in os.walk(start_dir): for ignore in ('doc', ".git", "__pycache__"): if ignore in dirnames: dirnames.remove(ignore) filenames = [ n for n in filenames if not n.startswith(".") and not n.endswith("~")] if dirpath.split(os.sep)[-1] == "bin": for filename in filenames: paths.append(os.path.join(dirpath, filename)) else: for filename in filenames: if filename.endswith(".py"): paths.append(os.path.join(dirpath, filename)) return paths @skipIf('SKIP_SLOW_TESTS' in os.environ, 'Skipping slow tests') @skipIf(pep8 is None, 'No pep8 package available') def test_pep8_clean(self): # https://github.com/jcrocholl/pep8/issues/103 pep8_style = pep8.StyleGuide(ignore='E123') result = pep8_style.check_files(self.all_paths()) self.assertEqual(result.total_errors, 0) @skipIf('SKIP_SLOW_TESTS' in os.environ, 'Skipping slow tests') @skipIf(pyflakes is None, 'No pyflakes package available') def test_pyflakes_clean(self): reporter = pyflakes.reporter.Reporter(sys.stdout, sys.stderr) warnings = pyflakes.api.checkRecursive(self.all_paths(), reporter) self.assertEqual(0, warnings) click-0.5.0/click_package/tests/test_user.py000066400000000000000000000703331402441472600211040ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Unit tests for click_package.user.""" from __future__ import print_function __metaclass__ = type __all__ = [ 'TestClickUser', ] import json import os import shutil import tempfile from textwrap import dedent from unittest import skip # Important: # # glib reads the environment only once and caches it then # so we need to create them before importing. # # ensure that the xdg dirs point to a different place as # registry.remove() will remove stuff here xdg_tempdir = tempfile.mkdtemp() for xdgdir in ("XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME"): os.environ[xdgdir] = os.path.join(xdg_tempdir, xdgdir) # ensure this is imported after the environment manipulation from gi.repository import Click, GLib from click_package.json_helpers import ( json_array_to_python, json_object_to_python, ) from click_package.tests.gimock_types import Passwd from click_package.tests.helpers import ( TestCase, make_installed_click, mkfile, make_file_with_content, touch, ) @skip('Tests failing on 20.04 due to preload hack not working.') class TestClickUser(TestCase): def setUp(self): super(TestClickUser, self).setUp() self.use_temp_dir() self.db = Click.DB() self.db.add(self.temp_dir) def _setUpMultiDB(self): self.multi_db = Click.DB() self.multi_db.add(os.path.join(self.temp_dir, "custom")) self.multi_db.add(os.path.join(self.temp_dir, "click")) user_dbs = [ os.path.join( self.multi_db.get(i).props.root, ".click", "users", "user") for i in range(self.multi_db.props.size) ] a_1_0 = os.path.join(self.temp_dir, "custom", "a", "1.0") os.makedirs(a_1_0) with mkfile(os.path.join(a_1_0, ".click", "info", "a.manifest")) as m: json.dump({"name": "a", "version": "1.0", "hooks": {"a-app": {}}}, m) b_2_0 = os.path.join(self.temp_dir, "custom", "b", "2.0") os.makedirs(b_2_0) with mkfile(os.path.join(b_2_0, ".click", "info", "b.manifest")) as m: json.dump({"name": "b", "version": "2.0"}, m) a_1_1 = os.path.join(self.temp_dir, "click", "a", "1.1") os.makedirs(a_1_1) with mkfile(os.path.join(a_1_1, ".click", "info", "a.manifest")) as m: json.dump({"name": "a", "version": "1.1"}, m) c_0_1 = os.path.join(self.temp_dir, "click", "c", "0.1") os.makedirs(c_0_1) with mkfile(os.path.join(c_0_1, ".click", "info", "c.manifest")) as m: json.dump({"name": "c", "version": "0.1"}, m) os.makedirs(user_dbs[0]) os.symlink(a_1_0, os.path.join(user_dbs[0], "a")) os.symlink(b_2_0, os.path.join(user_dbs[0], "b")) os.makedirs(user_dbs[1]) os.symlink(a_1_1, os.path.join(user_dbs[1], "a")) os.symlink(c_0_1, os.path.join(user_dbs[1], "c")) return user_dbs, Click.User.for_user(self.multi_db, "user") def test_new_no_db(self): with self.run_in_subprocess( "click_get_db_dir", "g_get_user_name") as (enter, preloads): enter() preloads["click_get_db_dir"].side_effect = ( lambda: self.make_string(self.temp_dir)) preloads["g_get_user_name"].side_effect = ( lambda: self.make_string("test-user")) db_root = os.path.join(self.temp_dir, "db") os.makedirs(db_root) with open(os.path.join(self.temp_dir, "db.conf"), "w") as f: print("[Click Database]", file=f) print("root = %s" % db_root, file=f) registry = Click.User.for_user(None, None) self.assertEqual( os.path.join(db_root, ".click", "users", "test-user"), registry.get_overlay_db()) def test_new_db_not_directory(self): with self.run_in_subprocess( "click_get_db_dir", "g_get_user_name") as (enter, preloads): enter() path = os.path.join(self.temp_dir, "file") touch(path) preloads["click_get_db_dir"].side_effect = ( lambda: self.make_string(path)) self.assertRaisesFileError( GLib.FileError.NOTDIR, Click.User.for_user, None, None) def test_get_overlay_db(self): self.assertEqual( os.path.join(self.temp_dir, ".click", "users", "user"), Click.User.for_user(self.db, "user").get_overlay_db()) def test_ensure_db_ownership(self): # getpwnam results are cached properly, in a way that doesn't fail # due to confusion with getpwnam returning a pointer to a static # buffer. with self.run_in_subprocess( "chown", "geteuid", "getpwnam") as (enter, preloads): enter() preloads["geteuid"].return_value = 0 getpwnam_result = Passwd() def getpwnam_side_effect(name): if name == b"clickpkg": getpwnam_result.pw_uid = 1 getpwnam_result.pw_gid = 1 else: getpwnam_result.pw_uid = 2 getpwnam_result.pw_gid = 2 return self.make_pointer(getpwnam_result) preloads["getpwnam"].side_effect = getpwnam_side_effect registry = Click.User.for_user(self.db, "user") os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) click_dir = os.path.join(self.temp_dir, ".click") registry.set_version("a", "1.0") self.assertEqual(3, preloads["chown"].call_count) preloads["chown"].assert_any_call(click_dir.encode(), 1, 1) preloads["chown"].assert_any_call( os.path.join(click_dir, "users").encode(), 1, 1) preloads["chown"].assert_any_call( os.path.join(click_dir, "users", "user").encode(), 2, 2) # Try again, now that both password file entries should be # cached. shutil.rmtree(os.path.join(self.temp_dir, ".click")) preloads["chown"].reset_mock() registry.set_version("a", "1.0") self.assertEqual(3, preloads["chown"].call_count) preloads["chown"].assert_any_call(click_dir.encode(), 1, 1) preloads["chown"].assert_any_call( os.path.join(click_dir, "users").encode(), 1, 1) preloads["chown"].assert_any_call( os.path.join(click_dir, "users", "user").encode(), 2, 2) def test_ensure_db_mkdir_fails(self): with self.run_in_subprocess("mkdir") as (enter, preloads): enter() preloads["mkdir"].return_value = -1 registry = Click.User.for_user(self.db, "user") self.assertRaisesUserError( Click.UserError.CREATE_DB, registry.set_version, "a", "1.0") def test_ensure_db_chown_fails(self): with self.run_in_subprocess( "chown", "geteuid", "getpwnam") as (enter, preloads): enter() preloads["geteuid"].return_value = 0 getpwnam_result = Passwd() def getpwnam_side_effect(name): if name == b"clickpkg": getpwnam_result.pw_uid = 1 getpwnam_result.pw_gid = 1 else: getpwnam_result.pw_uid = 2 getpwnam_result.pw_gid = 2 return self.make_pointer(getpwnam_result) preloads["getpwnam"].side_effect = getpwnam_side_effect preloads["chown"].return_value = -1 registry = Click.User.for_user(self.db, "user") self.assertRaisesUserError( Click.UserError.CHOWN_DB, registry.set_version, "a", "1.0") def test_ensure_db_getpwnam_fails(self): with self.run_in_subprocess( "geteuid", "getpwnam") as (enter, preloads): enter() preloads["geteuid"].return_value = 0 preloads["getpwnam"].return_value = None registry = Click.User.for_user(self.db, "user") self.assertRaisesUserError( Click.UserError.GETPWNAM, registry.set_version, "a", "1.0") def test_get_package_names_missing(self): db = Click.DB() db.add(os.path.join(self.temp_dir, "nonexistent")) registry = Click.User.for_user(db, None) self.assertEqual([], list(registry.get_package_names())) def test_get_package_names(self): registry = Click.User.for_user(self.db, "user") os.makedirs(registry.get_overlay_db()) os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a")) os.symlink("/1.1", os.path.join(registry.get_overlay_db(), "b")) self.assertCountEqual(["a", "b"], list(registry.get_package_names())) def test_get_package_names_multiple_root(self): _, registry = self._setUpMultiDB() self.assertCountEqual( ["a", "b", "c"], list(registry.get_package_names())) def test_get_version_missing(self): registry = Click.User.for_user(self.db, "user") self.assertRaisesUserError( Click.UserError.NO_SUCH_PACKAGE, registry.get_version, "a") self.assertFalse(registry.has_package_name("a")) def test_get_version(self): registry = Click.User.for_user(self.db, "user") os.makedirs(registry.get_overlay_db()) os.symlink("/1.0", os.path.join(registry.get_overlay_db(), "a")) self.assertEqual("1.0", registry.get_version("a")) self.assertTrue(registry.has_package_name("a")) def test_get_version_multiple_root(self): _, registry = self._setUpMultiDB() self.assertEqual("1.1", registry.get_version("a")) self.assertEqual("2.0", registry.get_version("b")) self.assertEqual("0.1", registry.get_version("c")) self.assertTrue(registry.has_package_name("a")) self.assertTrue(registry.has_package_name("b")) self.assertTrue(registry.has_package_name("c")) def test_set_version_missing_target(self): registry = Click.User.for_user(self.db, "user") self.assertRaisesDatabaseError( Click.DatabaseError.DOES_NOT_EXIST, registry.set_version, "a", "1.0") def test_set_version_missing(self): registry = Click.User.for_user(self.db, "user") os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) registry.set_version("a", "1.0") path = os.path.join(registry.get_overlay_db(), "a") self.assertTrue(os.path.islink(path)) self.assertEqual( os.path.join(self.temp_dir, "a", "1.0"), os.readlink(path)) def test_set_version_changed(self): registry = Click.User.for_user(self.db, "user") os.makedirs(registry.get_overlay_db()) path = os.path.join(registry.get_overlay_db(), "a") os.symlink("/1.0", path) os.makedirs(os.path.join(self.temp_dir, "a", "1.1")) registry.set_version("a", "1.1") self.assertTrue(os.path.islink(path)) self.assertEqual( os.path.join(self.temp_dir, "a", "1.1"), os.readlink(path)) def test_set_version_multiple_root(self): user_dbs, registry = self._setUpMultiDB() os.makedirs(os.path.join(self.multi_db.get(1).props.root, "a", "1.2")) registry.set_version("a", "1.2") a_underlay = os.path.join(user_dbs[0], "a") a_overlay = os.path.join(user_dbs[1], "a") self.assertTrue(os.path.islink(a_underlay)) self.assertEqual( os.path.join(self.multi_db.get(0).props.root, "a", "1.0"), os.readlink(a_underlay)) self.assertTrue(os.path.islink(a_overlay)) self.assertEqual( os.path.join(self.multi_db.get(1).props.root, "a", "1.2"), os.readlink(a_overlay)) os.makedirs(os.path.join(self.multi_db.get(1).props.root, "b", "2.1")) registry.set_version("b", "2.1") b_underlay = os.path.join(user_dbs[0], "b") b_overlay = os.path.join(user_dbs[1], "b") self.assertTrue(os.path.islink(b_underlay)) self.assertEqual( os.path.join(self.multi_db.get(0).props.root, "b", "2.0"), os.readlink(b_underlay)) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual( os.path.join(self.multi_db.get(1).props.root, "b", "2.1"), os.readlink(b_overlay)) os.makedirs(os.path.join(self.multi_db.get(1).props.root, "c", "0.2")) registry.set_version("c", "0.2") c_underlay = os.path.join(user_dbs[0], "c") c_overlay = os.path.join(user_dbs[1], "c") self.assertFalse(os.path.islink(c_underlay)) self.assertTrue(os.path.islink(c_overlay)) self.assertEqual( os.path.join(self.multi_db.get(1).props.root, "c", "0.2"), os.readlink(c_overlay)) os.makedirs(os.path.join(self.multi_db.get(1).props.root, "d", "3.0")) registry.set_version("d", "3.0") d_underlay = os.path.join(user_dbs[0], "d") d_overlay = os.path.join(user_dbs[1], "d") self.assertFalse(os.path.islink(d_underlay)) self.assertTrue(os.path.islink(d_overlay)) self.assertEqual( os.path.join(self.multi_db.get(1).props.root, "d", "3.0"), os.readlink(d_overlay)) def test_set_version_restore_to_underlay(self): user_dbs, registry = self._setUpMultiDB() a_underlay = os.path.join(user_dbs[0], "a") a_overlay = os.path.join(user_dbs[1], "a") # Initial state: 1.0 in underlay, 1.1 in overlay. self.assertTrue(os.path.islink(a_underlay)) self.assertEqual( os.path.join(self.multi_db.get(0).props.root, "a", "1.0"), os.readlink(a_underlay)) self.assertTrue(os.path.islink(a_overlay)) self.assertEqual( os.path.join(self.multi_db.get(1).props.root, "a", "1.1"), os.readlink(a_overlay)) # Setting to 1.0 (version in underlay) removes overlay link. registry.set_version("a", "1.0") self.assertTrue(os.path.islink(a_underlay)) self.assertEqual( os.path.join(self.multi_db.get(0).props.root, "a", "1.0"), os.readlink(a_underlay)) self.assertFalse(os.path.islink(a_overlay)) def test_remove_missing(self): registry = Click.User.for_user(self.db, "user") self.assertRaisesUserError( Click.UserError.NO_SUCH_PACKAGE, registry.remove, "a") def test_remove(self): registry = Click.User.for_user(self.db, "user") os.makedirs(registry.get_overlay_db()) path = os.path.join(registry.get_overlay_db(), "a") os.symlink("/1.0", path) registry.remove("a") self.assertFalse(os.path.exists(path)) def test_remove_multiple_root(self): user_dbs, registry = self._setUpMultiDB() registry.remove("a") self.assertFalse(os.path.exists(os.path.join(user_dbs[1], "a"))) # Exposed underlay. self.assertEqual("1.0", registry.get_version("a")) registry.remove("b") # Hidden. self.assertEqual( "@hidden", os.readlink(os.path.join(user_dbs[1], "b"))) self.assertFalse(registry.has_package_name("b")) registry.remove("c") self.assertFalse(os.path.exists(os.path.join(user_dbs[1], "c"))) self.assertFalse(registry.has_package_name("c")) self.assertRaisesUserError( Click.UserError.NO_SUCH_PACKAGE, registry.remove, "d") def test_remove_multiple_root_creates_overlay_directory(self): multi_db = Click.DB() multi_db.add(os.path.join(self.temp_dir, "preinstalled")) multi_db.add(os.path.join(self.temp_dir, "click")) user_dbs = [ os.path.join(multi_db.get(i).props.root, ".click", "users", "user") for i in range(multi_db.props.size) ] a_1_0 = os.path.join(self.temp_dir, "preinstalled", "a", "1.0") os.makedirs(a_1_0) os.makedirs(user_dbs[0]) os.symlink(a_1_0, os.path.join(user_dbs[0], "a")) self.assertFalse(os.path.exists(user_dbs[1])) registry = Click.User.for_user(multi_db, "user") self.assertEqual("1.0", registry.get_version("a")) registry.remove("a") self.assertFalse(registry.has_package_name("a")) self.assertEqual( "@hidden", os.readlink(os.path.join(user_dbs[1], "a"))) def test_get_path(self): registry = Click.User.for_user(self.db, "user") os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) registry.set_version("a", "1.0") self.assertEqual( os.path.join(registry.get_overlay_db(), "a"), registry.get_path("a")) def test_get_path_multiple_root(self): user_dbs, registry = self._setUpMultiDB() self.assertEqual( os.path.join(user_dbs[1], "a"), registry.get_path("a")) self.assertEqual( os.path.join(user_dbs[0], "b"), registry.get_path("b")) self.assertEqual( os.path.join(user_dbs[1], "c"), registry.get_path("c")) self.assertRaisesUserError( Click.UserError.NO_SUCH_PACKAGE, registry.get_path, "d") def test_get_manifest(self): registry = Click.User.for_user(self.db, "user") manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") manifest_obj = {"name": "a", "version": "1.0"} with mkfile(manifest_path) as manifest: json.dump(manifest_obj, manifest) manifest_obj["_directory"] = os.path.join( registry.get_overlay_db(), "a") manifest_obj["_removable"] = 1 registry.set_version("a", "1.0") self.assertEqual( manifest_obj, json_object_to_python(registry.get_manifest("a"))) self.assertEqual( manifest_obj, json.loads(registry.get_manifest_as_string("a"))) def test_get_manifest_multiple_root(self): user_dbs, registry = self._setUpMultiDB() expected_a = { "name": "a", "version": "1.1", "_directory": os.path.join(user_dbs[1], "a"), "_removable": 1, } self.assertEqual( expected_a, json_object_to_python(registry.get_manifest("a"))) self.assertEqual( expected_a, json.loads(registry.get_manifest_as_string("a"))) expected_b = { "name": "b", "version": "2.0", "_directory": os.path.join(user_dbs[0], "b"), "_removable": 1, } self.assertEqual( expected_b, json_object_to_python(registry.get_manifest("b"))) self.assertEqual( expected_b, json.loads(registry.get_manifest_as_string("b"))) expected_c = { "name": "c", "version": "0.1", "_directory": os.path.join(user_dbs[1], "c"), "_removable": 1, } self.assertEqual( expected_c, json_object_to_python(registry.get_manifest("c"))) self.assertEqual( expected_c, json.loads(registry.get_manifest_as_string("c"))) self.assertRaisesUserError( Click.UserError.NO_SUCH_PACKAGE, registry.get_path, "d") def test_get_manifests(self): registry = Click.User.for_user(self.db, "user") a_manifest_path = os.path.join( self.temp_dir, "a", "1.0", ".click", "info", "a.manifest") a_manifest_obj = {"name": "a", "version": "1.0"} with mkfile(a_manifest_path) as a_manifest: json.dump(a_manifest_obj, a_manifest) registry.set_version("a", "1.0") b_manifest_path = os.path.join( self.temp_dir, "b", "2.0", ".click", "info", "b.manifest") b_manifest_obj = {"name": "b", "version": "2.0"} with mkfile(b_manifest_path) as b_manifest: json.dump(b_manifest_obj, b_manifest) registry.set_version("b", "2.0") a_manifest_obj["_directory"] = os.path.join( registry.get_overlay_db(), "a") a_manifest_obj["_removable"] = 1 b_manifest_obj["_directory"] = os.path.join( registry.get_overlay_db(), "b") b_manifest_obj["_removable"] = 1 self.assertEqual( [a_manifest_obj, b_manifest_obj], json_array_to_python(registry.get_manifests())) self.assertEqual( [a_manifest_obj, b_manifest_obj], json.loads(registry.get_manifests_as_string())) def test_get_manifests_multiple_root(self): user_dbs, registry = self._setUpMultiDB() a_manifest_obj = { "name": "a", "version": "1.1", "_directory": os.path.join(user_dbs[1], "a"), "_removable": 1, } b_manifest_obj = { "name": "b", "version": "2.0", "_directory": os.path.join(user_dbs[0], "b"), "_removable": 1, } c_manifest_obj = { "name": "c", "version": "0.1", "_directory": os.path.join(user_dbs[1], "c"), "_removable": 1, } self.assertEqual( [a_manifest_obj, c_manifest_obj, b_manifest_obj], json_array_to_python(registry.get_manifests())) self.assertEqual( [a_manifest_obj, c_manifest_obj, b_manifest_obj], json.loads(registry.get_manifests_as_string())) registry.remove("b") self.assertEqual( "@hidden", os.readlink(os.path.join(user_dbs[1], "b"))) self.assertEqual( [a_manifest_obj, c_manifest_obj], json_array_to_python(registry.get_manifests())) self.assertEqual( [a_manifest_obj, c_manifest_obj], json.loads(registry.get_manifests_as_string())) def test_is_removable(self): registry = Click.User.for_user(self.db, "user") os.makedirs(os.path.join(self.temp_dir, "a", "1.0")) registry.set_version("a", "1.0") self.assertTrue(registry.is_removable("a")) def test_is_removable_multiple_root(self): user_dbs, registry = self._setUpMultiDB() self.assertTrue(registry.is_removable("a")) self.assertTrue(registry.is_removable("b")) self.assertTrue(registry.is_removable("c")) self.assertFalse(registry.is_removable("d")) def test_hidden(self): user_dbs, registry = self._setUpMultiDB() b_overlay = os.path.join(user_dbs[1], "b") registry.remove("b") self.assertFalse(registry.has_package_name("b")) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual("@hidden", os.readlink(b_overlay)) self.assertRaisesUserError( Click.UserError.HIDDEN_PACKAGE, registry.get_version, "b") self.assertRaisesUserError( Click.UserError.HIDDEN_PACKAGE, registry.get_path, "b") self.assertFalse(registry.is_removable("b")) registry.set_version("b", "2.0") self.assertTrue(registry.has_package_name("b")) self.assertTrue(os.path.islink(b_overlay)) self.assertEqual( os.path.join(self.multi_db.get(0).props.root, "b", "2.0"), os.readlink(b_overlay)) self.assertEqual("2.0", registry.get_version("b")) self.assertEqual(b_overlay, registry.get_path("b")) self.assertTrue(registry.is_removable("b")) @skip('Tests failing on 20.04 due to preload hack not working.') class StopAppTestCase(TestCase): def setUp(self): super(StopAppTestCase, self).setUp() self.use_temp_dir() self.db = Click.DB() self.db.add(self.temp_dir) # setup fake app_stop fake_app_stop = os.path.join(self.temp_dir, "bin", "lomiri-app-stop") self.fake_app_stop_output = os.path.join( self.temp_dir, "fake-app-stop.out") fake_app_stop_content = dedent("""\ #!/bin/sh echo "$@" >> %s """ % self.fake_app_stop_output) make_file_with_content(fake_app_stop, fake_app_stop_content, 0o755) # its ok to modify env here, click.helpers.TestCase will take care # of it os.environ["PATH"] = "%s:%s" % ( os.path.dirname(fake_app_stop), os.environ["PATH"]) def test_app_stops_on_remove(self): make_installed_click(self.db, self.temp_dir, "meep", "2.0", {"hooks": {"a-app": {}}}) registry = Click.User.for_user(self.db, "user") registry.remove("meep") # ensure that stop was called with the right app with open(self.fake_app_stop_output) as f: self.assertEqual("meep_a-app_2.0", f.read().strip()) @skip('Tests failing on 20.04 due to preload hack not working.') class UserDataRemovalTestCase(TestCase): @classmethod def setUpClass(cls): cls.class_temp_dir = xdg_tempdir def tearDown(self): super(UserDataRemovalTestCase, self).tearDown() for entry in os.listdir(self.class_temp_dir): shutil.rmtree(os.path.join(self.class_temp_dir, entry)) def setUp(self): super(UserDataRemovalTestCase, self).setUp() self.use_temp_dir() self.db = Click.DB() self.db.add(self.temp_dir) # add app self.appname = "some-app" self.registry = Click.User.for_user(self.db, "user") os.makedirs(self.registry.get_overlay_db()) path = os.path.join(self.registry.get_overlay_db(), self.appname) os.symlink("/1.0", path) def test_remove_removes_cache_only(self): """ Ensure that app cache for user is removed if a click is removed """ fake_config_dir = os.path.join( os.environ["XDG_CONFIG_HOME"], self.appname) fake_cache_dir = os.path.join( os.environ["XDG_CACHE_HOME"], self.appname) fake_data_dir = os.path.join( os.environ["XDG_DATA_HOME"], self.appname) os.makedirs(fake_config_dir) os.makedirs(fake_cache_dir) os.makedirs(fake_data_dir) self.registry.remove(self.appname) self.assertTrue(os.path.exists(fake_config_dir)) self.assertFalse(os.path.exists(fake_cache_dir)) self.assertTrue(os.path.exists(fake_data_dir)) def test_remove_does_not_follow_symlinks_inside_dir(self): """ Ensure that symlinks that are inside a user-data dir get removed, but that the symlink targets are not touched """ fake_cache_dir = os.path.join( os.environ["XDG_CACHE_HOME"], self.appname) os.makedirs(fake_cache_dir) # setup symlink inside the config dir that that points # to a file that is outside of the config dir canary = os.path.join(self.temp_dir, "canary") touch(canary) os.symlink(canary, os.path.join(fake_cache_dir, "canary")) self.assertTrue(os.path.islink(os.path.join(fake_cache_dir, "canary"))) self.registry.remove(self.appname) # ensure that the cache dir is gone self.assertFalse(os.path.exists(fake_cache_dir)) # ... but the canary file is still there self.assertTrue(os.path.exists(canary)) def test_remove_does_not_follow_if_xdg_dir_is_symlink(self): """ Ensure that if the user-cach dir is a symlink that symlink is removed but that the symlink target is not touched """ fake_cache_dir = os.path.join( os.environ["XDG_CACHE_HOME"], self.appname) os.makedirs(os.environ["XDG_CACHE_HOME"]) # setup the config dir as a symlink to a other dir canary = os.path.join(self.temp_dir, "canary") os.mkdir(canary) os.symlink(canary, fake_cache_dir) self.assertTrue(os.path.islink(fake_cache_dir)) self.registry.remove(self.appname) # ensure that the config dir is gone self.assertFalse(os.path.exists(fake_cache_dir)) # ... but the canary dir is still there self.assertTrue(os.path.exists(canary)) click-0.5.0/click_package/versions.py000066400000000000000000000013231402441472600175660ustar00rootroot00000000000000# Copyright (C) 2013 Canonical Ltd. # Author: Colin Watson # 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; version 3 of the License. # # 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 . """Click package versioning.""" spec_version = "0.4" click-0.5.0/conf/000077500000000000000000000000001402441472600135325ustar00rootroot00000000000000click-0.5.0/conf/Makefile.am000066400000000000000000000000241402441472600155620ustar00rootroot00000000000000SUBDIRS = databases click-0.5.0/conf/databases/000077500000000000000000000000001402441472600154615ustar00rootroot00000000000000click-0.5.0/conf/databases/10_core.conf000066400000000000000000000001371402441472600175610ustar00rootroot00000000000000[Click Database] # Preinstalled Ubuntu core applications. root = /usr/share/click/preinstalled click-0.5.0/conf/databases/20_custom.conf000066400000000000000000000001111402441472600201340ustar00rootroot00000000000000[Click Database] # OEM/carrier/etc. customisations. root = /custom/click click-0.5.0/conf/databases/99_default.conf.in000066400000000000000000000001061402441472600206770ustar00rootroot00000000000000[Click Database] # User-installed applications. root = @DEFAULT_ROOT@ click-0.5.0/conf/databases/Makefile.am000066400000000000000000000001641402441472600175160ustar00rootroot00000000000000databasesdir = $(sysconfdir)/click/databases databases_DATA = \ 10_core.conf \ 20_custom.conf \ 99_default.conf click-0.5.0/configure.ac000066400000000000000000000102341402441472600150730ustar00rootroot00000000000000AC_INIT([click],m4_esyscmd([./get-version]),[https://launchpad.net/click]) AC_CONFIG_SRCDIR([preload/clickpreload.c]) AC_CONFIG_AUX_DIR([build-aux]) AC_CONFIG_MACRO_DIR([m4]) AM_INIT_AUTOMAKE([foreign]) AM_CONFIG_HEADER([config.h]) AC_USE_SYSTEM_EXTENSIONS LT_INIT([disable-static]) AC_SUBST([GETTEXT_PACKAGE], [click]) IT_PROG_INTLTOOL AC_PROG_CC CFLAGS="$CFLAGS -Wall" AC_ARG_WITH([python-interpreters], [AS_HELP_STRING([--with-python-interpreters], [install for these Python interpreters (space-separated, default: python3)])], [PYTHON_INTERPRETERS="$withval"], [PYTHON_INTERPRETERS=python3]) AC_SUBST([PYTHON_INTERPRETERS]) AC_ARG_WITH([default-root], [AS_HELP_STRING([--with-default-root], [set default root path for installed packages (default: /opt/click.ubuntu.com)])], [DEFAULT_ROOT="$withval"], [DEFAULT_ROOT=/opt/click.ubuntu.com]) AC_SUBST([DEFAULT_ROOT]) click_save_LIBS="$LIBS" AC_SEARCH_LIBS([dlopen], [dl]) AC_SUBST([PRELOAD_LIBS], ["$LIBS"]) LIBS="$click_save_LIBS" AC_CACHE_CHECK([for Perl vendor library directory], [click_cv_perl_vendorlib], [click_cv_perl_vendorlib=`perl -MConfig -e 'print $Config{vendorlib}'`]) AC_SUBST([perl_vendorlib], ["$click_cv_perl_vendorlib"]) AM_PROG_VALAC PKG_CHECK_MODULES([LIBCLICK], [ glib-2.0 >= 2.34 gobject-2.0 >= 2.34 json-glib-1.0 >= 0.10 gee-0.8 ]) AC_SUBST([LIBCLICK_CFLAGS]) AC_SUBST([LIBCLICK_LIBS]) # Structure characteristics needed for the Python/C integration in the test # suite. AC_COMPUTE_INT([STAT_OFFSET_UID], [offsetof(struct stat, st_uid)], [ AC_INCLUDES_DEFAULT #include ]) AC_SUBST([STAT_OFFSET_UID]) AC_COMPUTE_INT([STAT_OFFSET_GID], [offsetof(struct stat, st_gid)], [ AC_INCLUDES_DEFAULT #include ]) AC_SUBST([STAT_OFFSET_GID]) AC_COMPUTE_INT([STAT64_OFFSET_UID], [offsetof(struct stat64, st_uid)], [ AC_INCLUDES_DEFAULT #include ]) AC_SUBST([STAT64_OFFSET_UID]) AC_COMPUTE_INT([STAT64_OFFSET_GID], [offsetof(struct stat64, st_gid)], [ AC_INCLUDES_DEFAULT #include ]) AC_SUBST([STAT64_OFFSET_GID]) GOBJECT_INTROSPECTION_REQUIRE([0.6.7]) VAPIGEN_VAPIDIR=`$PKG_CONFIG --variable=vapidir vapigen` AC_SUBST([VAPIGEN_VAPIDIR]) AC_ARG_ENABLE([systemd], AS_HELP_STRING([--disable-systemd], [Disable systemd integration])) AM_CONDITIONAL([INSTALL_SYSTEMD], [test "x$enable_systemd" != xno]) AC_ARG_WITH([systemdsystemunitdir], AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd system unit files])) if test "x$enable_systemd" != xno && test "x$with_systemdsystemunitdir" = x; then AC_MSG_CHECKING([for systemd system unit directory]) with_systemdsystemunitdir="$($PKG_CONFIG --variable=systemdsystemunitdir systemd)" if test "x$with_systemdsystemunitdir" = x; then AC_MSG_ERROR([no systemd system unit directory found]) fi AC_MSG_RESULT([$with_systemdsystemunitdir]) fi AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir]) AC_ARG_WITH([systemduserunitdir], AS_HELP_STRING([--with-systemduserunitdir=DIR], [Directory for systemd user unit files])) if test "x$enable_systemd" != xno && test "x$with_systemduserunitdir" = x; then AC_MSG_CHECKING([for systemd user unit directory]) with_systemduserunitdir="$($PKG_CONFIG --variable=systemduserunitdir systemd)" if test "x$with_systemduserunitdir" = x; then AC_MSG_ERROR([no systemd user unit directory found]) fi AC_MSG_RESULT([$with_systemduserunitdir]) fi AC_SUBST([systemduserunitdir], [$with_systemduserunitdir]) m4_include([m4/gcov.m4]) AC_TDD_GCOV AM_CONDITIONAL([HAVE_GCOV], [test "x$ac_cv_check_gcov" = xyes]) AM_CONDITIONAL([HAVE_LCOV], [test "x$ac_cv_check_lcov" = xyes]) AM_CONDITIONAL([HAVE_GCOVR], [test "x$ac_cv_check_gcovr" = xyes]) AC_SUBST(COVERAGE_CFLAGS) AC_SUBST(COVERAGE_LDFLAGS) AC_CONFIG_FILES([ Makefile click_package/Makefile click_package/tests/Makefile click_package/tests/config.py conf/Makefile conf/databases/Makefile conf/databases/99_default.conf debhelper/Makefile init/Makefile init/systemd/Makefile lib/Makefile lib/click/Makefile lib/click/click-0.4.pc preload/Makefile schroot/Makefile ]) AC_CONFIG_FILES([lib/click/valac-wrapper], [chmod +x lib/click/valac-wrapper]) AC_CONFIG_FILES([setup.py], [chmod +x setup.py]) AC_OUTPUT click-0.5.0/debhelper/000077500000000000000000000000001402441472600145375ustar00rootroot00000000000000click-0.5.0/debhelper/Makefile.am000066400000000000000000000004721402441472600165760ustar00rootroot00000000000000perllibdir = $(perl_vendorlib)/Debian/Debhelper/Sequence debhelperdir = $(datadir)/debhelper/autoscripts bin_SCRIPTS = dh_click man1_MANS = dh_click.1 dist_perllib_DATA = click.pm dist_debhelper_DATA = postinst-click prerm-click CLEANFILES = $(man1_MANS) %.1: % pod2man -c Debhelper -r $(PACKAGE_VERSION) $< $@ click-0.5.0/debhelper/click.pm000066400000000000000000000002341402441472600161610ustar00rootroot00000000000000#! /usr/bin/perl # debhelper sequence file for click use warnings; use strict; use Debian::Debhelper::Dh_Lib; insert_after("dh_install", "dh_click"); 1; click-0.5.0/debhelper/dh_click000077500000000000000000000044701402441472600162320ustar00rootroot00000000000000#! /usr/bin/perl -w =head1 NAME dh_click - install system hooks for click =cut use strict; use Debian::Debhelper::Dh_Lib; =head1 SYNOPSIS B [S>] =head1 DESCRIPTION dh_click is a debhelper program that is responsible for installing system hooks for B. It also automatically generates the F and F commands needed to interface with the Ubuntu B package. These commands are inserted into the maintainer scripts by L. =head1 FILES =over 4 =item debian/I.click-hook Click package hook files, installed into usr/share/click/hooks/I.hook in the package build directory. See F for their format. =back =head1 OPTIONS =over 4 =item B<-n>, B<--noscripts> Do not modify F/F scripts. =item B<--name=>I Install the hook using the filename I instead of the default filename, which is the package name. When this parameter is used, B looks for and installs files named F, instead of the usual F. =back =head1 EXAMPLES dh_click is usually called indirectly in a rules file via the dh command. %: dh $@ --with click You must build-depend on at least debhelper (>= 7.0.8) to use this form, and in any case you must build-depend on click-dev to use this program at all. It can also be called directly at any time before C, usually in a binary-arch or binary-indep rule. =cut init(); # PROMISE: DH NOOP WITHOUT click-hook foreach my $package (@{$dh{DOPACKAGES}}) { my $tmp=tmpdir($package); my $click_hook=pkgfile($package,"click-hook"); my $hookname=$package; if (defined $dh{NAME}) { $hookname=$dh{NAME}; } if ($click_hook ne '') { if (! -d "$tmp/usr/share/click/hooks") { doit("install","-d","$tmp/usr/share/click/hooks"); } doit("install","-p","-m644",$click_hook,"$tmp/usr/share/click/hooks/$hookname.hook"); if (! $dh{NOSCRIPTS}) { autoscript($package,"postinst","postinst-click","s/#HOOK#/$hookname/"); autoscript($package,"prerm","prerm-click","s/#HOOK#/$hookname/"); } } } =head1 SEE ALSO L This program is a part of click. =head1 AUTHOR Colin Watson Copyright (C) 2013 Canonical Ltd., licensed under the GNU GPL v3. =cut click-0.5.0/debhelper/postinst-click000066400000000000000000000001351402441472600174270ustar00rootroot00000000000000if [ "$1" = "configure" ] && which click >/dev/null 2>&1; then click hook install #HOOK# fi click-0.5.0/debhelper/prerm-click000066400000000000000000000001021402441472600166630ustar00rootroot00000000000000if which click >/dev/null 2>&1; then click hook remove #HOOK# fi click-0.5.0/debian/000077500000000000000000000000001402441472600140275ustar00rootroot00000000000000click-0.5.0/debian/changelog000066400000000000000000001366741402441472600157220ustar00rootroot00000000000000click (0.5.0) UNRELEASED; urgency=medium * Release 0.5.0. -- Rodney Dawes Wed, 17 Mar 2021 11:00:51 -0400 click (0.4.47+ubports) xenial; urgency=medium * Imported to UBports -- UBports auto importer Mon, 08 Jan 2018 16:50:20 +0000 click (0.4.46+17.10.20170607.3-0ubuntu1) artful; urgency=medium * Rename the python package this installs from click to click_package. (LP: #1693226) * Use the correct overlayfs module name for the chroot configuration. (LP: #1696402) -- Sergio Schvezov Wed, 07 Jun 2017 18:53:16 +0000 click (0.4.45.1+16.10.20160916-0ubuntu1) yakkety; urgency=medium * Kill gpg-agent (if possible) after running debsigs in integration tests. * Copy integration test GPG keyring to a temporary directory so that the GPG agent socket name doesn't end up being too long. -- Colin Watson Fri, 16 Sep 2016 17:46:25 +0000 click (0.4.45+16.10.20160828.1-0ubuntu1) yakkety; urgency=medium * Emit more debugging information when libclickpreload/libgimockpreload fail. * Preload __xmknod rather than mknod (LP: #1615757). * Stop the test suite leaving spurious empty /tmp/gimock* directories around. * Remove stray blank line from click_package_install_hooks test preload annotations. * Link libgimockpreload properly against libclick and several other libraries, and use -Wl,--no-as-needed to stop the linker discarding references to libraries that will be used via dlsym. * Initialise real_* pointers in libgimockpreload more reliably. * Export click_get_user_home for the benefit of the test suite. * Fix click_get_user_home mocks to return bytes rather than text, avoiding obscure crashes in the test suite. * Fix builds on xenial and earlier: build-depending on libpackagekit-glib2-dev (...) | base-files isn't resolved properly by apt, but we can just build-depend on libpackagekit-glib2-dev and let the rest of the build machinery disable use of PackageKit if it's 1.0.0 or newer. -- Colin Watson Sun, 28 Aug 2016 21:00:53 +0000 click (0.4.44+16.10.20160811.1-0ubuntu1) yakkety; urgency=medium [ Colin Watson ] * Handle new policy URL in debsig-verify 0.15. (LP: #1608281) -- Timo Jyrinki Thu, 11 Aug 2016 10:50:58 +0000 click (0.4.44+16.10.20160810.1-0ubuntu1) yakkety; urgency=medium * Add --disable-packagekit to the autopkgtests. -- Timo Jyrinki Wed, 10 Aug 2016 12:36:49 +0000 click (0.4.44+16.10.20160810-0ubuntu1) yakkety; urgency=medium * Don't use @ in debian/tests/control since packagekit plugin is still in debian/control. -- Timo Jyrinki Wed, 10 Aug 2016 08:21:34 +0000 click (0.4.44+16.10.20160809.1-0ubuntu1) yakkety; urgency=medium [ Colin Watson ] * Chase redirection for click.readthedocs.org -> click.readthedocs.io. [ Antti Kaijanmäki ] * As PK 1.0 does not support plugins anymore, drop the click plugin. (LP: #1470655, #1496292) -- Timo Jyrinki Tue, 09 Aug 2016 11:22:15 +0000 click (0.4.43+16.04.20160203-0ubuntu2) xenial; urgency=medium * Fix python3-click-package -> python3-click Replaces version (LP: #1544776). -- Colin Watson Fri, 12 Feb 2016 11:10:43 +0000 click (0.4.43+16.04.20160203-0ubuntu1) xenial; urgency=medium [ Colin Watson ] * Fix test skipping in build_core_apps integration tests. * Skip chroot integration tests on architectures that were not present in 14.04. * Fix TestChroot.test_exists_no integration test to be more meaningful rather than just testing a command syntax error. * Rename python3-click back to its original name of python3-click-package to get it out of the way of PyPI's "click" package, conflicting with python3-click and python3-click-cli; this is unfortunate and non-compliant with the Python policy, but from that point of view the package that owns the name in the upstream packaging system ought to win. [ CI Train Bot ] * No-change rebuild. -- Colin Watson Wed, 03 Feb 2016 11:17:48 +0000 click (0.4.42+16.04.20151229-0ubuntu1) xenial; urgency=medium [ Colin Watson ] * chroot: Point debootstrap at ports.ubuntu.com for non-primary architectures. * Skip build_core_apps integration tests on architectures that lack support for native- or cross-compiling for armhf. [ CI Train Bot ] * No-change rebuild. -- Colin Watson Tue, 29 Dec 2015 01:36:51 +0000 click (0.4.41+16.04.20151211-0ubuntu1) xenial; urgency=medium [ Colin Watson ] * Fix spurious test_sync_without_user_db test failure. * Fix test failures under Python 2. * Forbid installing packages with data tarball members whose names do not start with "./" (LP: #1506467). * Take evasive action in case the conflicting "click" package has been installed locally from PyPI (LP: #1486841). * Drop use of apt_pkg from click.install, since it's no longer needed there (LP: #1510015). [ Dimitri John Ledkov ] * Require specific Click version, to avoid gi warnings that fail test-suite (LP: #1522608). * Set Vcs-* fields to the actual development branch. [ CI Train Bot ] * No-change rebuild. -- Colin Watson Fri, 11 Dec 2015 01:31:29 +0000 click (0.4.40+15.10.20151006-0ubuntu1.1) wily; urgency=medium * SECURITY UPDATE: fix privilege escalation via crafted data.tar.gz that can be used to install alternate security policy than what is defined - click/install.py: Forbid installing packages with data tarball members whose names do not start with "./". Patch thanks to Colin Watson. - CVE-2015-XXXX - LP: #1506467 -- Jamie Strandboge Thu, 15 Oct 2015 11:13:36 -0500 click (0.4.40+15.10.20151006-0ubuntu1) wily; urgency=medium [ Michael Vogt ] * Don't follow symlinks when stat()ing files. (LP: 1496976) [ Zoltán Balogh ] * Add and pin up the overlay PPA for vivid. [ Kyle Fazzari ] * Garbage collect old user registrations. (LP: #1479001) [ CI Train Bot ] * No-change rebuild. -- Kyle Fazzari Tue, 06 Oct 2015 19:44:52 +0000 click (0.4.39.1+15.10.20150702-0ubuntu1) wily; urgency=medium [ Michael Vogt ] * lp:~mvo/click/lp1456328-15.10-devlibs: - add ubuntu-sdk-15.10-dev1 - remove ubuntu-core-15.04-dev1 -- CI Train Bot Thu, 02 Jul 2015 11:43:15 +0000 click (0.4.39) vivid; urgency=low [ Marco Trevisan (Treviño) ] * handle "IP NOT FOUND" error (LP: #1433234) [ Michael Vogt ] * switch the default click chroot framework to ubuntu-sdk-14.04 -- CI Train Bot Thu, 07 May 2015 14:47:11 +0000 click (0.4.38.5) vivid; urgency=medium [ Sebastien Bacher ] * Don't try to replace initctl if it's not there (lp: #1430436) -- CI Train Bot Tue, 10 Mar 2015 18:52:17 +0000 click (0.4.38.4) vivid; urgency=low [ Michael Vogt ] * lp:~mvo/click/lp1232130-kill-on-remove-2: - When uninstalling a app, stop it if its running (LP: #1232130) * lp:~mvo/click/dont-crash-for-empty-db: - Do not crash when no database configuration is used and Click.DB.{get,props.overlay,gc,ensure_ownership} are called. * lp:~mvo/click/lp1219912-build-exclude: - add a new --ignore option to click {build,buildsource} (LP: #1219912) * lp:~mvo/click/fix-multiple-framework-validation: - fix framework validation for snappy * run debian/packagekit-check with "sh" as something in jenkins made it mode 0644 [ CI Train Bot ] * New rebuild forced. -- CI Train Bot Thu, 26 Feb 2015 17:07:09 +0000 click (0.4.37) vivid; urgency=low [ Michael Vogt ] * lp:~mvo/click/no-error-no-missing-systemctl: - fix a spurious error message on systems without systemctl * lp:~mvo/click/do-not-crash-in-build-on-broken-symlinks: - do not crash when building a click package that contains broken symlinks * lp:~mvo/click/dpkg-less-verbose: - do not show dpkg output on install unless --verbose is used * lp:~mvo/click/lp1394256-run-user-hooks: - ensures that click user hooks are run for all logged in users when click is used with "--all-users". * lp:~mvo/click/qt5-qmake-cross-armhf: - add qt5-qmake-arm-linux-gnueabihf to chroot (LP: #1393698) * lp:~mvo/click/chroot-15.04-multiarch: - add ubuntu-sdk-libs-tools and oxide-codecs-extra to the chroot * lp:~mvo/click/lp1394256-run-user-hooks-on-remove-too: - Run the click remove user hooks for all logged in users. * click/chroot.py: - use string.format() for chroot TARGET selection * skip 0.4.36 version and go straight to 0.4.37 (LP: #1418086) [ Zoltan Balogh ] * lp:~bzoltan/click/vivid-transition_mirrors: - use geoip to guess the most suitable country mirror when creating the click chroot -- CI Train Bot Fri, 13 Feb 2015 14:46:34 +0000 click (0.4.35) vivid; urgency=low [ Michael Vogt ] * lp:~mvo/click/add-run-shm: - Ensure /run/shm is available in a click chroot * lp:~mvo/click/ubuntu-core-framework: - Add "ubuntu-core-15.04-dev1" click chroot -- Ubuntu daily release Fri, 14 Nov 2014 12:29:14 +0000 click (0.4.34.2) vivid; urgency=medium [ Michael Vogt ] * Demote ubuntu-app-launch-tools from a click recommends to a suggests, since they are not needed on server installs. * Use dh-systemd to enable click system and user hook integration (LP: #1379657). * add ubuntu-sdk-15.04 based on ubuntu-sdk-libs/ubuntu-sdk-libs-dev * click/tests/preload.h: - replace deprecated "Attributes:" with annotations on the identifier * click/tests/Makefile.am: - add --libtool to g-ir-scanner so that it uses the generated libtool instead of the system libtool which is now part of the libtool-bin package in vivid [ Colin Watson ] * Make "click info" always try opening the input file as a package and only try to interpret it as a file in an installed package if that fails, rather than guessing by the input file extension. * Allow "click chroot install" and "click chroot upgrade" to operate on sessions. [ David Planella ] * Adds internationalization tools and CMake macros required by the new Qt Creator app and scope templates that provide internationalization code. [ Pete Woods ] * Include Canonical's cmake-extras project in the schroot. -- Ubuntu daily release Mon, 03 Nov 2014 12:49:25 +0000 click (0.4.33) utopic; urgency=medium [ Pete Woods ] * Add scope-facing APIs to chroot build (LP: #1370727). [ Colin Watson ] * Warn that "click install" without a registration may result in later garbage-collection. * Rearrange garbage-collection to remove versions of packages that have no user registrations and are not running, rather than using the artificial @gcinuse registration which never really worked properly. * Run garbage-collection immediately before running system hooks on system startup (LP: #1342858). * Add new -n/--name option to "click chroot", defaulting to "click" (LP: #1364327). [ Michael Vogt ] * Make click destroy more robust by unmounting any mounted filesystem inside the schroot first (LP: #1346723). * Stop apps if necessary when uninstalling them (LP: #1232130). * Add new "click framework {info,get-field}" subcommands. -- Ubuntu daily release Mon, 29 Sep 2014 14:18:41 +0000 click (0.4.32.1) utopic; urgency=low [ Michael Vogt ] * fix autopkgtest failure found in 0.4.32 -- Ubuntu daily release Tue, 09 Sep 2014 10:02:00 +0000 click (0.4.32) utopic; urgency=medium [ Daniel Holbach ] * Run click-review after a successful build of a click package. Offer --no-validate as an option. [ Colin Watson ] * Move integration tests to a location where they won't end up being installed into inappropriate places on the system module path (LP: #1337696). * Use six.with_metaclass for TestBuildCoreApps, so that it doesn't make pyflakes angry when running tests under Python 2. [ Michael Vogt ] * Add more integration tests for "click {list,register,verify,info}". * Only install trusted click packages by default. To override you can run "pkcon --allow-untrusted" (LP: #1360582) -- Ubuntu daily release Mon, 08 Sep 2014 09:50:43 +0000 click (0.4.31.3) utopic; urgency=medium * Reinstate most of 0.4.31.2; instead, just always pass --allow-unauthenticated to "click install" from the PackageKit plugin for now, until we can sort out sideloading (see LP #1360582). -- Colin Watson Mon, 25 Aug 2014 12:25:21 -0700 click (0.4.31.2.is.0.4.30) utopic; urgency=medium * Temporary everting to previous version as the current one makes installing click packages locally through pkcon install-local impossible, which is breaking some other projects relying on this functionality. Needs to be revisited. -- Åukasz 'sil2100' Zemczak Mon, 25 Aug 2014 17:34:07 +0200 click (0.4.31.2) utopic; urgency=medium [ Michael Vogt ] * Add "click info" interface to get manifest corresponding to file in installed package (LP: #1324853). * Add support for click package gpg signatures (LP: #1330770). [ Colin Watson ] * Ugly hack to get coverage reporting working again with gcovr 3.1. [ Ubuntu daily release ] * New rebuild forced -- Ubuntu daily release Fri, 22 Aug 2014 17:19:06 +0000 click (0.4.30) utopic; urgency=medium [ Colin Watson ] * Add many more unit tests to fill in some gaps in the coverage report. [ Michael Vogt ] * Exclude non-existing users from User.get_user_names() (LP: #1334611) [ Zoltan Balogh ] * Add a set of APIs to the 14.10 frameworks. Add ubuntu-ui-toolkit-doc to all frameworks. [ Ubuntu daily release ] * New rebuild forced -- Ubuntu daily release Wed, 06 Aug 2014 23:33:22 +0000 click (0.4.29.1) utopic; urgency=medium [ Michael Vogt ] * Trivial fix for the current autopkgtest failure. The ADT environment does not have the USER environment set. -- Ubuntu daily release Fri, 04 Jul 2014 15:10:01 +0000 click (0.4.29) utopic; urgency=medium [ Michael Vogt ] * Refactor click/chroot.py and improve tests. * Test-build two ubuntu-system-apps in a click chroot as part of the integration tests. * Generalise handling of -devN frameworks in "click chroot". * Add integration test for "click hook install". [ Colin Watson ] * Various adjustments for improved PEP-8 compliance. * Produce coverage only on request via ./configure --enable-gcov. Extend it to cover C code as well, and produce a merged coverage.xml. [ Brendan Donegan ] * Clean up integration tests - have one file per command and make some things clearer. [ Dimitri John Ledkov ] * Seed runtime target arch SDK app launcher, to enable remote debugging. -- Ubuntu daily release Thu, 03 Jul 2014 13:10:21 +0000 click (0.4.28) utopic; urgency=medium [ Colin Watson ] * Revert change in 0.4.27 to raise an error if no databases are added; this makes most of libclick unusable if click is not installed. -- Ubuntu daily release Mon, 23 Jun 2014 16:07:37 +0000 click (0.4.27) utopic; urgency=medium [ Michael Vogt ] * Fix failing autopkgtests. * Refactor the integration tests to be more modular. Add installation integration tests based on https://wiki.ubuntu.com/Process/Merges/TestPlan/click. * Add integration tests for buildsource, pkgdir and framework. [ Dimitri John Ledkov ] * Handle renaming of qtsensors5-dev to libqt5sensors5-dev in Qt 5.3. -- Ubuntu daily release Mon, 23 Jun 2014 11:31:50 +0000 click (0.4.26.1) utopic; urgency=medium [ Colin Watson ] * Fix a pyflakes complaint. -- Ubuntu daily release Tue, 10 Jun 2014 18:04:12 +0000 click (0.4.26) utopic; urgency=medium [ Michael Vogt ] * Add basic integration tests, run via autopkgtest. * Add coverage testing. [ Colin Watson ] * Fix DEB_BUILD_* environment variables when building on amd64 for i386 (LP: #1328486). -- Colin Watson Tue, 10 Jun 2014 19:02:33 +0100 click (0.4.25) utopic; urgency=medium [ Ted Gould ] * Check for ubuntu-app-launch-desktop hook (LP: #1326694). -- Ubuntu daily release Thu, 05 Jun 2014 18:18:50 +0000 click (0.4.24) utopic; urgency=medium [ Loïc Minier ] * Add 14.10 framework in places where all frameworks are listed. [ Colin Watson ] * Copy the pw_uid and pw_gid members from getpwnam's return value and cache those separately, since getpwnam returns a pointer to a static buffer so just caching its return value is not useful (LP: #1323998). * Handle the renaming of upstart-app-launch to ubuntu-app-launch: we now cope with both old and new names. -- Ubuntu daily release Wed, 04 Jun 2014 08:37:18 +0000 click (0.4.23.1) utopic; urgency=medium [ Colin Watson ] * chroot: Force dpkg-architecture to recalculate everything rather than picking up values from the environment, to avoid the test suite getting confused by environment variables exported by dpkg-buildpackage. -- Ubuntu daily release Tue, 20 May 2014 13:15:41 +0000 click (0.4.23) utopic; urgency=medium [ Michael Vogt ] * Show human-readable error message when a click chroot subcommand fails because of existing or non-existing chroots (LP: #1296820). * Selectively disable logging on some tests to avoid message spam during the test runs. * When running hooks, remove hook symlinks if framework requirements are not met (LP: #1271944). * Cleanup the chroot if "click chroot create" fails (unless --keep-broken-chroot is used) * Fix sources.list generation when native_arch and target_arch are on the same archive server (part of LP #1319153). * Add "click framework list" command to list available frameworks (LP: #1294659). [ Pete Woods ] * Add libunity-scopes-dev package to chroot (LP: #1320786). [ Sergio Schvezov ] * click chroot creation depends on dpkg-architecture, so recommend dpkg-dev. [ Colin Watson ] * chroot: Handle the case where we can execute binaries for the target architecture directly and thus don't need a cross-compiler (LP: #1319153). -- Colin Watson Tue, 20 May 2014 14:10:11 +0100 click (0.4.22) utopic; urgency=medium [ Michael Vogt ] * Update documentation for building the project. * Add support for multiple frameworks (LP: #1318757). -- Ubuntu daily release Wed, 14 May 2014 06:28:34 +0000 click (0.4.21.1) trusty; urgency=medium [ Colin Watson ] * When a hook command fails, include the command in the error message. * Don't allow failure of a single hook to prevent other hooks being run. * Log hook failures to stderr and exit non-zero, rather than propagating an exception which is then logged as a click crash. -- Ubuntu daily release Tue, 08 Apr 2014 09:41:55 +0000 click (0.4.21) trusty; urgency=medium * Add *_as_string variants of manifest methods, for clients that already have their own JSON parsing libraries and don't want to use JSON-GLib. * Write to stderr and exit non-zero when chrooted commands fail, rather than propagating an exception which is then logged as a click crash (LP: #1298457). * Make the get_manifests family of functions log errors about individual manifests to stderr rather than crashing (LP: #1297519). * Don't run user hooks until dbus has started; the content-hub hook needs to modify gsettings. * Don't rely on PyGObject supporting default None arguments; this was only added in 3.11.1. -- Colin Watson Tue, 08 Apr 2014 10:13:37 +0100 click (0.4.20) trusty; urgency=medium [ Colin Watson ] * Create system hook symlinks for all installed packages, not just current versions. This avoids missing AppArmor profiles when there are unregistered user-installed versions of packages lying around. -- Ubuntu daily release Mon, 24 Mar 2014 16:16:37 +0000 click (0.4.19) trusty; urgency=medium [ Colin Watson ] * Set Click.User.ensure_db visibility back to private, since it's no longer used by Click.Hook. (The C ABI is unaffected.) * Add brief documentation on Click's multiple-database scheme, based on my recent mail to ubuntu-phone. * Fix a few potential GLib critical messages from the PackageKit plugin. * Make libclick-0.4-dev depend on libjson-glib-dev for . * Add Requires.private to click-0.4.pc, so that programs built against libclick pick up the proper CFLAGS including glib and json-glib. * chroot: Allow creating 14.04 chroots. * Include _directory and _removable dynamic manifest keys in "click info" output (LP: #1293788). * Document -f and -s options to "click chroot" in click(1). * chroot: Fix code to make /finish.sh executable. * chroot: Make /usr/sbin/policy-rc.d executable in the chroot, as otherwise it has no effect. * chroot: Run apt-get dist-upgrade on the chroot before trying to install the basic build tool set. Fixes chroot creation for saucy. [ Benjamin Zeller ] * Take pkexec env vars into account when creating a chroot. [ Dimitri John Ledkov ] * Add session management to click chroot. -- Ubuntu daily release Tue, 18 Mar 2014 14:27:53 +0000 click (0.4.18.3) trusty; urgency=medium [ Colin Watson ] * Take a slightly different approach to fixing "click hook run-user": only try to update user registration symlinks if they already exist in the overlay database. -- Ubuntu daily release Wed, 12 Mar 2014 12:02:47 +0000 click (0.4.18.2) trusty; urgency=medium * Make "click hook run-user" ensure that the user registration directory exists before dropping privileges and trying to create symlinks in it (LP: #1291192). -- Colin Watson Wed, 12 Mar 2014 11:59:31 +0000 click (0.4.18.1) trusty; urgency=medium [ Colin Watson ] * If a user attempts to install a version of a package that is already installed in an underlay database, then just register the appropriate version for them rather than unpacking another copy. * Make "click hook run-system" and "click hook run-user" consistently use the bottom-most unpacked copy of a given version of a package, and update hook symlinks and user registration symlinks if necessary. -- Ubuntu daily release Tue, 11 Mar 2014 17:22:10 +0000 click (0.4.18) trusty; urgency=medium * Give gir1.2-click-0.4 an exact-versioned dependency on libclick-0.4-0. * Use is_symlink helper method in a few more places. * Add a similar is_dir helper method. * Ignore extraneous non-directories when walking a database root in Click.DB.get_packages and Click.DB.gc. * Make the PackageKit plugin tolerate the "_removable" dynamic manifest key being changed to a boolean in the future. * Document that users of "_removable" should tolerate it being a boolean. * Use libclick when removing packages, listing packages, or searching packages via the PackageKit plugin. * Add libclick interfaces to get package manifests, both individually (LP: #1287692) and for all installed packages (LP: #1287693). * Override description-starts-with-package-name Lintian error for click; this is describing the system as a whole rather than naming the package. * Add libclick interfaces to get the list of frameworks supported by the current system (LP: #1271633) and various properties of those frameworks (LP: #1287694). -- Colin Watson Tue, 11 Mar 2014 17:18:07 +0000 click (0.4.17.2) trusty; urgency=medium [ Colin Watson ] * Fix Click.User construction in "click pkgdir". -- Ubuntu daily release Thu, 06 Mar 2014 16:38:35 +0000 click (0.4.17.1) trusty; urgency=medium * gobject-introspection-1.0.pc is in libgirepository1.0-dev, not gobject-introspection. Fix Build-Depends. * Build-depend and depend on gir1.2-glib-2.0 and python3-gi. * Map gboolean to ctypes.c_int, not ctypes.c_bool. gboolean and gint are the same as far as glib is concerned, and ctypes does strange things with its bool type in callbacks. -- Colin Watson Thu, 06 Mar 2014 16:09:33 +0000 click (0.4.17) trusty; urgency=medium * Use full path to click in Upstart jobs to save a $PATH lookup. * Add systemd units to run Click system and user hooks at the appropriate times. We probably won't be using these for a while, but it does no harm to add them. * Move an initial core of functionality (database, hooks, osextras, query, user) from Python into a new "libclick" library, allowing performance-critical clients to avoid the cost of starting a new Python interpreter (LP: #1282311). -- Colin Watson Thu, 06 Mar 2014 14:35:26 +0000 click (0.4.16) trusty; urgency=medium [ Colin Watson ] * hooks: Fix expansion of "$$" in hook patterns to conform to the documented behaviour of expanding to the single character "$". * Move version detection out of configure.ac into a separate get-version script, since intltool-update has trouble with the previous approach. * Stop using unittest2 if available; the relevant improvements were integrated into the standard library's unittest in Python 2.7, and we no longer support 2.6. * user: When setting the registered version of a package to the version in an underlay database (e.g. a preinstalled version vs. one in the user-installed area), remove the overlay link rather than setting a new one equal to the underlay; this was always the intended behaviour but didn't work that way due to a typo. * Add Python 3.4 to list of tested versions. * Call setup.py from the top-level Makefile.am rather than from debian/rules, to make the build system a bit more unified. * Drop AM_GNU_GETTEXT and call intltoolize before autoreconf in autogen.sh; this fixes a bug whereby "make" after "./configure" always immediately needed to run aclocal. * Build-depend on python3-pep8 so that test_pep8_clean doesn't need to be skipped when running under Python 3. This can safely be removed for backports to precise. * Simplify click -> python3-click dependency given that both are Architecture: any. * Tighten packagekit-plugin-click -> click dependency to require a matching version. * Use dh_install --fail-missing to avoid future mistakes. * Sync up substvar use with what debhelper actually generates for us: add ${misc:Pre-Depends} to click and packagekit-plugin-click, and remove ${python3:Depends} from click-dev. * Reset SIGPIPE handling from Python's default of raising an exception to the Unix default of terminating the process (LP: #1285790). -- Ubuntu daily release Tue, 04 Mar 2014 15:23:45 +0000 click (0.4.15) trusty; urgency=medium [ Stéphane Graber ] * Set X-Auto-Uploader to no-rewrite-version * Set Vcs-Bzr to the new target branch -- Ubuntu daily release Thu, 30 Jan 2014 16:12:17 +0000 click (0.4.14) trusty; urgency=low [ Colin Watson ] * chroot: Print help if no subcommand given (LP: #1260669). * chroot: Recommend debootstrap from click-dev, and explicitly check for it in "click chroot create" (LP: #1260487). * chroot: Check for root in "create" and "destroy" (LP: #1260671). * hooks: Add a ${short-id} expansion to hook patterns; this is valid only in user-level or single-version hooks, and expands to a new "short application ID" without the version (LP: #1251635). * hooks: Strip any trailing slashes from the end of patterns, as they cause confusion with symlink-to-directory semantics and can never be useful (LP: #1253855). * install: Extend the interpretation of "framework" a little bit to allow a Click package to declare that it requires multiple frameworks. This will allow splitting up the SDK framework declarations into more fine-grained elements. * Policy version 3.9.5: no changes required. * build: Enforce only a single framework declaration for now, by request. [ Zoltan Balogh ] * Add qtmultimedia5-dev to the SDK framework list. [ Dimitri John Ledkov ] * chroot: Add "cmake" to build_pkgs, as it is expected for cmake to be available on any (Ubuntu) framework. -- Colin Watson Thu, 23 Jan 2014 17:30:54 +0000 click (0.4.13) trusty; urgency=low [ Robert Bruce Park ] * Ignore click packages when building click packages. [ Colin Watson ] * If "click build" or "click buildsource" is given a directory as the value of its -m/--manifest option, interpret that as indicating the "manifest.json" file in that directory (LP: #1251604). * Ensure correct permissions on /opt/click.ubuntu.com at boot, since a system image update may have changed clickpkg's UID/GID (LP: #1259253). -- Colin Watson Tue, 10 Dec 2013 14:33:42 +0000 click (0.4.12) trusty; urgency=low [ Colin Watson ] * Adjust top-level "click help" entry for "install" to point to pkcon. * Fix hook installation tests to test Unicode manifests properly. * Read version and date from debian/changelog when building documentation. * Declare click-dev Multi-Arch: foreign (LP: #1238796). * Build-depend on python3:any/python3-all:any rather than python3/python3-all. [ Brian Murray, Colin Watson ] * Add chroot management support. -- Colin Watson Thu, 21 Nov 2013 14:46:16 +0000 click (0.4.11) saucy; urgency=low * Drop --force-missing-framework from PackageKit plugin now that /usr/share/click/frameworks/ubuntu-sdk-13.10.framework is in ubuntu-sdk-libs. * Show a neater error message when a package's framework is not installed (LP: #1236671). * Show a neater error message when building a package whose manifest file cannot be parsed (LP: #1236669). * Show a neater error message when running "click install" with insufficient privileges (LP: #1236673). -- Colin Watson Fri, 11 Oct 2013 12:07:06 +0100 click (0.4.10) saucy; urgency=low * When removing packages, only drop privileges after ensuring the existence of the database directory (LP: #1233280). -- Colin Watson Mon, 30 Sep 2013 18:12:14 +0100 click (0.4.9) saucy; urgency=low * Explicitly build-depend on pkg-config, since it's needed even if the PackageKit/GLib-related build-dependencies are removed for backporting. * Remove some stray documentation references to Ubuntu 13.04. * Ensure that the user's overlay database directory exists when unregistering a preinstalled package (LP: #1232066). * Support packages containing code for multiple architectures, and document the "architecture" manifest field (LP: #1214380, #1214864). * Correctly pass through return values of commands as the exit status of the "click" wrapper. * Extend "click info" to take a registered package name as an alternative to a path to a Click package file (LP: #1232118). * Force unpacked files to be owner-writeable (LP: #1232128). -- Colin Watson Mon, 30 Sep 2013 15:24:49 +0100 click (0.4.8) saucy; urgency=low * Show a proper error message if "click build" or "click buildsource" is called on a directory that does not exist or does not contain a manifest file, rather than crashing (LP: #1228619). * Restore missing newlines after JSON dumps in "click info" and "click list --manifest". * Tidy up use of PackageKit IDs; local:click should refer to uninstalled packages, while installed:click refers to installed packages. * Expose application names and whether a package is removable via the PackageKit API: the IDs of installed applications are now formed as comma-separated key/value pairs, e.g. "installed:click,removable=1,app_name=foo,app_name=bar" (LP: #1209329). * Rename ClickUser.__setitem__ to ClickUser.set_version and ClickUser.__delitem__ to ClickUser.remove; with multiple databases it was impossible for these methods to fulfil the normal contract for mutable mappings, since deleting an item might simply expose an item in an underlying database. * Allow unregistering preinstalled packages. A preinstalled package cannot in general actually be removed from disk, but unregistering it for a user records it as being hidden from that user's list of registered packages. Reinstalling the same version unhides it. * Consolidate ClickInstaller.audit_control into ClickInstaller.audit. * Validate the shipped md5sums file in "click verify" (LP: #1217333). -- Colin Watson Tue, 24 Sep 2013 15:21:48 +0100 click (0.4.7) saucy; urgency=low * Run system hooks when removing a package from the file system (LP: #1227681). * If a hook symlink is already correct, don't unnecessarily remove and recreate it. * Improve "click hook install-system" and "click hook install-user" to remove any stale symlinks they find, and to run Exec commands only once per hook. This significantly speeds up system and session startup when lots of applications are installed (LP: #1227604). * Rename "click hook install-system" and "click hook install-user" to "click hook run-system" and "click hook run-user" respectively, to better fit their semantics. (I expect these commands only to have been used internally by click's own Upstart jobs.) * Filter version control metadata and editor backup files out of binary packages in "click build" (LP: #1223640). -- Colin Watson Fri, 20 Sep 2013 18:07:13 +0100 click (0.4.6) saucy; urgency=low * Make sure all unpacked files and directories are group- and world-readable, and (if owner-executable) also group- and world-executable (LP: #1226553). -- Colin Watson Tue, 17 Sep 2013 13:37:06 +0100 click (0.4.5) saucy; urgency=low * Document --force-missing-framework option in the error message produced when a package's required framework is not present. * Make "click pkgdir" exit 1 if a directory for the given package name or path is not found, rather than letting the exception propagate (LP: #1225923). * Run system hooks at boot time, in particular so that AppArmor profiles for packages in /custom are generated and loaded (LP: #1223085). -- Colin Watson Mon, 16 Sep 2013 20:55:28 +0100 click (0.4.4) saucy; urgency=low * Amend "click help install" to recommend using "pkcon install-local". * Run hooks when removing a per-user package registration. * Adjust usage lines for "click help verify" and "click help pkgdir" to indicate that options are allowed. * Add a click(1) manual page. * Use json.dump and json.load in most places rather than json.dumps and json.loads (which unnecessarily construct strings). * Add "click unregister", which unregisters a package for a user and removes it from disk if it is not being used. * Add RemovePackage support to the PackageKit plugin, mapped to "click unregister". * Attempt to remove the old version of a package after installing or registering a new one. * Remove code supporting PackageKit 0.7 API, and instead arrange to disable the PackageKit plugin if the new API is not available, since we don't need to build it on Ubuntu 12.04 LTS. * Report errors from click subprocesses in PackageKit plugin (LP: #1218483). * Implement PackageKit search by name and by details. * Reserve manifest keys starting with an underscore for use as dynamic properties of installed packages. * Add the dynamic key "_directory" to "click list --manifest" output, showing the directory where each package is unpacked (LP: #1221760). * Add the dynamic key "_removable" to "click list --manifest" output, which is 1 if a package is unpacked in a location from which it can be removed, otherwise 0. -- Colin Watson Mon, 09 Sep 2013 13:37:39 +0100 click (0.4.3) saucy; urgency=low * Add support for multiple installation root directories, configured in /etc/click/databases/. Define /usr/share/click/preinstalled, /custom/click, and /opt/click.ubuntu.com by default. * Add --all-users option to "click install" and "click register": this registers the installed package for a special pseudo-user "@all", making it visible to all users. * Add "click hook install-user", which runs all user-level hooks for all packages for a given user. This is useful at session startup to catch up with packages that may have been preinstalled and registered for all users. * Run "click hook install-user" on session startup from an Upstart user job. * Avoid calling "click desktophook" if /usr/share/click/hooks/upstart-app-launch-desktop.hook exists. * Force umask to a sane value when dropping privileges (022 for clickpkg, current-umask | 002 for other users; LP: #1215480). * Use aa-exec-click rather than aa-exec in .desktop files generated by "click desktophook" (LP: #1197047). -- Colin Watson Wed, 04 Sep 2013 17:01:58 +0100 click (0.4.2) saucy; urgency=low * Suppress dpkg calls to lchown when not running as root (LP: #1220125). -- Colin Watson Tue, 03 Sep 2013 10:12:29 +0100 click (0.4.1) saucy; urgency=low [ Sergio Schvezov ] * Compare mtimes for desktop files, not stat objects. -- Colin Watson Mon, 02 Sep 2013 14:54:49 +0100 click (0.4.0) saucy; urgency=low [ Colin Watson ] * Add "installed-size" as a mandatory field in the control area's "manifest" file; it should not be present in source manifest files, and is generated automatically by "click build". * Add an optional "icon" manifest key. * Consistently call clickpreload_init from preloaded functions in case they happen to be called before libclickpreload's constructor. * Run dpkg with --force-bad-path so that /sbin and /usr/sbin are not required to be on $PATH; we don't use the tools dpkg gets from there. [ Loïc Minier ] * Add fopen64 wrapper (LP: #1218674). -- Colin Watson Fri, 30 Aug 2013 17:59:34 +0100 click (0.3.4) saucy; urgency=low * Make "click desktophook" tolerate dangling symlinks in ~/.local/share/applications/. -- Colin Watson Wed, 28 Aug 2013 18:00:55 +0200 click (0.3.3) saucy; urgency=low * Recommend click-apparmor from click (suggested by Jamie Strandboge). -- Colin Watson Wed, 28 Aug 2013 12:17:23 +0200 click (0.3.2) saucy; urgency=low [ Jamie Strandboge ] * Document maintainer as an optional field. [ Matthias Klumpp ] * Support PackageKit 0.8 API. -- Colin Watson Tue, 27 Aug 2013 21:07:02 +0200 click (0.3.1) saucy; urgency=low [ Colin Watson ] * Fix some more failures with mock 0.7.2. * Work around the lack of a python-apt backport of apt_pkg.TagFile(sequence, bytes=True) to precise. [ Jamie Strandboge ] * Codify allowed characters for "application ID". * Fix typos in apparmor hook example. -- Colin Watson Tue, 13 Aug 2013 10:10:11 +0200 click (0.3.0) saucy; urgency=low * Insert a new "_click-binary" ar member immediately after "debian-binary"; this allows detecting the MIME type of a Click package even when it doesn't have the extension ".click" (LP: #1205346). * Declare the application/x-click MIME type, since the shared-mime-info upstream would rather not take the patch there at this point (https://bugs.freedesktop.org/show_bug.cgi?id=66689). * Make removal of old links for single-version hooks work even when the application ID is not a prefix of the pattern's basename. * Add an optional Hook-Name field to hook files, thereby allowing multiple hooks to attach to the same virtual name. * Rename click's own "desktop" hook to "click-desktop", making use of the new Hook-Name facility. -- Colin Watson Tue, 06 Aug 2013 11:08:46 +0100 click (0.2.10) saucy; urgency=low * Force click's stdout encoding to UTF-8 regardless of the locale. * Don't encode non-ASCII characters in JSON dumps. * Treat manifests as UTF-8. -- Colin Watson Tue, 30 Jul 2013 15:14:16 +0100 click (0.2.9) saucy; urgency=low * Tolerate dangling source symlinks in "click desktophook". * Handle the case where the clickpkg user cannot read the .click file, using some LD_PRELOAD trickery to allow passing it as a file descriptor opened by the privileged process (LP: #1204523). * Remove old links for single-version hooks when installing new versions (LP: #1206115). -- Colin Watson Mon, 29 Jul 2013 16:56:42 +0100 click (0.2.8) saucy; urgency=low * Check in advance whether the root is writable by the clickpkg user, not just by root, and do so in a way less vulnerable to useful exception text being eaten by a subprocess preexec_fn (LP: #1204570). * Actually install /var/lib/polkit-1/localauthority/10-vendor.d/com.ubuntu.click.pkla in the packagekit-plugin-click binary package. -- Colin Watson Thu, 25 Jul 2013 17:40:49 +0100 click (0.2.7) saucy; urgency=low * Fix error message when rejecting "_" from a package name or version (LP: #1204560). -- Colin Watson Wed, 24 Jul 2013 16:42:59 +0100 click (0.2.6) saucy; urgency=low * Adjust written .desktop files to avoid tickling some bugs in Unity 8's parsing. -- Colin Watson Wed, 24 Jul 2013 08:03:08 +0100 click (0.2.5) saucy; urgency=low * Ensure that ~/.local/share/applications exists if we need to write any .desktop files. -- Colin Watson Wed, 24 Jul 2013 07:44:44 +0100 click (0.2.4) saucy; urgency=low * Mangle Icon in .desktop files to point to an absolute path within the package unpack directory if necessary. * Add a "--" separator between aa-exec's options and the subsidiary command, per Jamie Strandboge. -- Colin Watson Tue, 23 Jul 2013 23:38:29 +0100 click (0.2.3) saucy; urgency=low * Set Path in generated .desktop files to the top-level package directory. * Revert part of geteuid() change in 0.2.2; ClickUser._drop_privileges and ClickUser._regain_privileges need to check the real UID, or else they will never regain privileges. * When running a hook, set HOME to the home directory of the user the hook is running as. -- Colin Watson Tue, 23 Jul 2013 22:57:03 +0100 click (0.2.2) saucy; urgency=low * dh_click: Support --name option. * Avoid ClickUser.__iter__ infecting its caller with dropped privileges. * Use geteuid() rather than getuid() in several places to check whether we need to drop or regain privileges. * Add a user-level hook to create .desktop files in ~/.local/share/applications/. (This should probably move to some other package at some point.) -- Colin Watson Tue, 23 Jul 2013 19:36:44 +0100 click (0.2.1) saucy; urgency=low * Fix "click help list". * Remove HOME from environment when running dpkg, so that it doesn't try to read .dpkg.cfg from it (which may fail when dropping privileges from root and produce a warning message). * Refuse to install .click directories at any level, not just the top. * Add "click pkgdir" command to print the top-level package directory from either a package name or a path within a package; based on work by Ted Gould, for which thanks. -- Colin Watson Mon, 22 Jul 2013 09:36:19 +0100 click (0.2.0) saucy; urgency=low * Revise and implement hooks specification. While many things have changed, the previous version was never fully implemented. However, I have incremented the default Click-Version value to 0.2 to reflect the design work. - The "hooks" manifest key now contains a dictionary keyed by application name. This means manifest authors have to repeat themselves much less in common cases. - There is now an explicit distinction between system-level and user-level hooks. System-level hooks may reflect multiple concurrent versions, and require a user name. - Hook symlinks are now named by a combination of the Click package name, the application name, and the Click package version. - The syntax of Pattern has changed to make it easier to extend with new substitutions. * Reject '_' and '/' characters in all of package name, application name, and package version. -- Colin Watson Fri, 19 Jul 2013 13:11:31 +0100 click (0.1.7) saucy; urgency=low * Correct name of .pkla file (now /var/lib/polkit-1/localauthority/10-vendor.d/com.ubuntu.click.pkla). -- Colin Watson Thu, 18 Jul 2013 17:00:46 +0100 click (0.1.6) saucy; urgency=low * Move defaults for frameworks and hooks directories to click.paths. * Install /var/lib/polkit-1/localauthority/10-vendor.d/10-click.pkla to allow the phablet user to install Click packages without being known to logind, as a temporary workaround. -- Colin Watson Thu, 18 Jul 2013 16:55:08 +0100 click (0.1.5) saucy; urgency=low * Fix infinite recursion in ClickUser.click_pw. * When all the files requested for installation are Click packages, override org.freedesktop.packagekit.package-install* PolicyKit actions to com.ubuntu.click.package-install, defined with a more open default policy. (This requires some backports to PackageKit, not in the archive yet.) -- Colin Watson Wed, 17 Jul 2013 15:46:48 +0100 click (0.1.4) saucy; urgency=low * Add support for per-user package registration. * Move install log file from $root/.click.log to $root/.click/log. * Add an autotools-based build system for our C components. * Initial version of a PackageKit plugin, in a new packagekit-plugin-click package; still experimental. * Restore compatibility with Python 3.2 (LP: #1200670). * Adjust tests to pass with mock 0.7.2 (as in Ubuntu 12.04 LTS). * Make the default root directory a configure option. * Add a simple "click list" command. -- Colin Watson Mon, 15 Jul 2013 15:55:48 +0100 click (0.1.3) saucy; urgency=low * Rename to click, per Mark Shuttleworth. -- Colin Watson Thu, 27 Jun 2013 15:57:25 +0100 click-package (0.1.2) saucy; urgency=low * Disable dh_sphinxdoc for builds that are not building architecture-independent packages. -- Colin Watson Tue, 25 Jun 2013 18:57:47 +0100 click-package (0.1.1) saucy; urgency=low * clickpackage.tests.test_install: Set NO_PKG_MANGLE when building fake packages, to avoid having Maintainer fields mangled on the buildds. -- Colin Watson Tue, 25 Jun 2013 17:32:00 +0100 click-package (0.1) saucy; urgency=low * Initial release. -- Colin Watson Mon, 24 Jun 2013 14:43:21 +0100 click-0.5.0/debian/click-dev.install000066400000000000000000000001451402441472600172600ustar00rootroot00000000000000etc/schroot/click usr/bin/dh_click usr/share/debhelper usr/share/man/man1/dh_click.1 usr/share/perl5 click-0.5.0/debian/click-doc.docs000066400000000000000000000000201402441472600165210ustar00rootroot00000000000000doc/_build/html click-0.5.0/debian/click.click-desktop.click-hook000066400000000000000000000002731402441472600216160ustar00rootroot00000000000000User-Level: yes Pattern: ${home}/.local/share/click/hooks/desktop/${id}.desktop Exec: [ -e /usr/share/click/hooks/lomiri-app-launch-desktop.hook ] || click desktophook Hook-Name: desktop click-0.5.0/debian/click.dirs000066400000000000000000000000331402441472600157730ustar00rootroot00000000000000usr/share/click/frameworks click-0.5.0/debian/click.install000066400000000000000000000001151402441472600165010ustar00rootroot00000000000000etc/click etc/init lib/systemd usr/bin/click usr/lib/*/click usr/lib/systemd click-0.5.0/debian/click.lintian-overrides000066400000000000000000000000541402441472600204730ustar00rootroot00000000000000click: description-starts-with-package-name click-0.5.0/debian/click.manpages000066400000000000000000000000271402441472600166300ustar00rootroot00000000000000doc/_build/man/click.1 click-0.5.0/debian/click.postinst000066400000000000000000000010601402441472600167160ustar00rootroot00000000000000#! /bin/sh set -e if [ "$1" = configure ]; then if ! getent passwd clickpkg >/dev/null; then adduser --quiet --system --home /nonexistent --no-create-home \ --disabled-password --shell /bin/false --group \ clickpkg fi mkdir -p -m 755 /opt/click.ubuntu.com chown clickpkg:clickpkg /opt/click.ubuntu.com # dh-systemd has no support yet for user systemd units # so we need to do this manually here if which systemctl >/dev/null 2>&1; then systemctl --global enable click-user-hooks.service || true fi fi #DEBHELPER# exit 0 click-0.5.0/debian/click.postrm000066400000000000000000000001731402441472600163630ustar00rootroot00000000000000#! /bin/sh set -e if [ "$1" = purge ]; then deluser --quiet --system clickpkg >/dev/null || true fi #DEBHELPER# exit 0 click-0.5.0/debian/click.sharedmimeinfo000066400000000000000000000011311402441472600200240ustar00rootroot00000000000000 Click package click-0.5.0/debian/compat000066400000000000000000000000021402441472600152250ustar00rootroot000000000000009 click-0.5.0/debian/control000066400000000000000000000103231402441472600154310ustar00rootroot00000000000000Source: click Section: admin Priority: optional Maintainer: Colin Watson Standards-Version: 3.9.5 Build-Depends: debhelper (>= 9~), dh-autoreconf, dh-python, dh-systemd (>= 1.3), gir1.2-glib-2.0, gobject-introspection (>= 0.6.7), libgee-0.8-dev, libgirepository1.0-dev (>= 0.6.7), libglib2.0-dev (>= 2.34), libjson-glib-dev (>= 0.10), pep8, pkg-config, pyflakes, python3-all:any, python3-apt, python3-coverage, python3-debian, python3-gi, python3-pep8, python3-setuptools, python3-six, python3-sphinx, python3:any (>= 3.2), python3:any (>= 3.3) | python3-mock, valac, Vcs-Bzr: https://code.launchpad.net/~click-hackers/click/devel Vcs-Browser: http://bazaar.launchpad.net/~click-hackers/click/devel/files X-Python-Version: >= 2.7 X-Python3-Version: >= 3.2 XS-Testsuite: autopkgtest Package: click Architecture: any Pre-Depends: ${misc:Pre-Depends}, Depends: adduser, python3-click-package (= ${binary:Version}), ${misc:Depends}, ${python3:Depends}, ${shlibs:Depends}, Recommends: click-apparmor, Suggests: click-reviewers-tools (>= 0.9), lomiri-app-launch-tools, Conflicts: click-package, packagekit-plugin-click, Replaces: click-package, packagekit-plugin-click, Provides: click-package, packagekit-plugin-click, Description: Click packages Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package provides common files, including the main click program. Package: click-dev Architecture: any Multi-Arch: foreign Depends: python3-click-package (= ${binary:Version}), ${misc:Depends}, ${perl:Depends}, Recommends: debootstrap, dpkg-dev, schroot (>= 1.6.10-2~), Description: build Click packages Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . click-dev provides support for building these packages. Package: python3-click-package Section: python Architecture: any Depends: gir1.2-click-0.4 (= ${binary:Version}), gir1.2-glib-2.0, python3-apt, python3-debian, python3-gi, ${misc:Depends}, ${python3:Depends}, Replaces: python3-click (<< 0.4.43), Description: Click packages (Python 3 interface) Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package provides Python 3 modules used by click, which may also be used directly. Package: libclick-0.4-0 Section: libs Architecture: any Multi-Arch: same Pre-Depends: ${misc:Pre-Depends}, Depends: ${misc:Depends}, ${shlibs:Depends}, Description: run-time Click package management library Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package provides a shared library for managing Click packages. Package: libclick-0.4-dev Section: libdevel Architecture: any Multi-Arch: same Pre-Depends: ${misc:Pre-Depends}, Depends: libclick-0.4-0 (= ${binary:Version}), libglib2.0-dev, libjson-glib-dev, ${misc:Depends}, ${shlibs:Depends}, Description: development files for Click package management library Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package provides development files needed to build programs for managing Click packages. Package: gir1.2-click-0.4 Section: introspection Architecture: any Depends: libclick-0.4-0 (= ${binary:Version}), ${gir:Depends}, ${misc:Depends}, Description: GIR bindings for Click package management library Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package can be used by other packages using the GIRepository format to generate dynamic bindings. Package: click-doc Section: doc Architecture: all Depends: ${misc:Depends}, ${sphinxdoc:Depends}, Conflicts: click-package-doc, Replaces: click-package-doc, Provides: click-package-doc, Description: Click packages (documentation) Click is a simplified packaging format that installs in a separate part of the file system, suitable for third-party applications. . This package provides documentation for click. click-0.5.0/debian/copyright000066400000000000000000000067461402441472600157770ustar00rootroot00000000000000Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: click Upstream-Contact: Source: https://launchpad.net/click Files: * Copyright: 2013, Canonical Ltd. License: GPL-3 Files: click/test/helpers.py Copyright: 2013, Canonical Ltd. 2007-2012 Michael Foord. License: Python Files: pk-plugin/* Copyright: 2010-2013, Matthias Klumpp 2011, Richard Hughes 2013, Canonical Ltd. License: GPL-3 License: GPL-3 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; version 3 of the License. . 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 . . A copy of the GNU General Public License version 3 is available in /usr/share/common-licenses/GPL-3. License: Python 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. . 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. . 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. . 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. . 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. . 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. . 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. . 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. click-0.5.0/debian/gir1.2-click-0.4.install000066400000000000000000000000431402441472600200000ustar00rootroot00000000000000usr/lib/*/girepository-1.0 usr/lib click-0.5.0/debian/libclick-0.4-0.install000066400000000000000000000000311402441472600176210ustar00rootroot00000000000000usr/lib/*/libclick*.so.* click-0.5.0/debian/libclick-0.4-0.symbols000066400000000000000000000101251402441472600176500ustar00rootroot00000000000000libclick-0.4.so.0 libclick-0.4-0 #MINVER# * Build-Depends-Package: libclick-0.4-dev click_database_error_quark@Base 0.4.17 click_db_add@Base 0.4.17 click_db_ensure_ownership@Base 0.4.17 click_db_gc@Base 0.4.17 click_db_get@Base 0.4.17 click_db_get_manifest@Base 0.4.18 click_db_get_manifest_as_string@Base 0.4.21 click_db_get_manifests@Base 0.4.18 click_db_get_manifests_as_string@Base 0.4.21 click_db_get_overlay@Base 0.4.17 click_db_get_packages@Base 0.4.17 click_db_get_path@Base 0.4.17 click_db_get_size@Base 0.4.17 click_db_get_type@Base 0.4.17 click_db_has_package_version@Base 0.4.18 click_db_maybe_remove@Base 0.4.17 click_db_new@Base 0.4.17 click_db_read@Base 0.4.17 click_dir_get_type@Base 0.4.17 click_dir_open@Base 0.4.17 click_dir_read_name@Base 0.4.17 click_ensuredir@Base 0.4.17 click_find_on_path@Base 0.4.17 click_find_package_directory@Base 0.4.17 click_framework_error_quark@Base 0.4.18 click_framework_get_base_name@Base 0.4.18 click_framework_get_base_version@Base 0.4.18 click_framework_get_fields@Base 0.4.18 click_framework_get_field@Base 0.4.18 click_framework_get_frameworks@Base 0.4.18 click_framework_get_name@Base 0.4.18 click_framework_get_type@Base 0.4.18 click_framework_has_framework@Base 0.4.18 click_framework_open@Base 0.4.18 click_get_db_dir@Base 0.4.17 click_get_frameworks_dir@Base 0.4.18 click_get_hooks_dir@Base 0.4.17 click_get_umask@Base 0.4.17 click_get_user_home@Base 0.4.45 click_hook_get_app_id@Base 0.4.17 click_hook_get_field@Base 0.4.17 click_hook_get_fields@Base 0.4.17 click_hook_get_hook_name@Base 0.4.17 click_hook_get_is_single_version@Base 0.4.17 click_hook_get_is_user_level@Base 0.4.17 click_hook_get_pattern@Base 0.4.17 click_hook_get_run_commands_user@Base 0.4.17 click_hook_get_short_app_id@Base 0.4.17 click_hook_get_type@Base 0.4.17 click_hook_install@Base 0.4.17 click_hook_install_package@Base 0.4.17 click_hook_open@Base 0.4.17 click_hook_open_all@Base 0.4.17 click_hook_remove@Base 0.4.17 click_hook_remove_package@Base 0.4.17 click_hook_run_commands@Base 0.4.17 click_hook_sync@Base 0.4.17 click_hooks_error_quark@Base 0.4.17 click_installed_package_get_package@Base 0.4.17 click_installed_package_get_path@Base 0.4.17 click_installed_package_get_type@Base 0.4.17 click_installed_package_get_version@Base 0.4.17 click_installed_package_get_writeable@Base 0.4.17 click_installed_package_new@Base 0.4.17 click_package_install_hooks@Base 0.4.17 click_package_remove_hooks@Base 0.4.17 click_pattern_format@Base 0.4.17 click_pattern_possible_expansion@Base 0.4.17 click_query_error_quark@Base 0.4.17 click_run_system_hooks@Base 0.4.17 click_run_user_hooks@Base 0.4.17 click_single_db_any_app_running@Base 0.4.17 click_single_db_app_running@Base 0.4.17 click_single_db_ensure_ownership@Base 0.4.17 click_single_db_gc@Base 0.4.17 click_single_db_get_manifest@Base 0.4.18 click_single_db_get_manifest_as_string@Base 0.4.21 click_single_db_get_packages@Base 0.4.17 click_single_db_get_path@Base 0.4.17 click_single_db_get_root@Base 0.4.17 click_single_db_get_type@Base 0.4.17 click_single_db_has_package_version@Base 0.4.18 click_single_db_maybe_remove@Base 0.4.17 click_single_db_new@Base 0.4.17 click_symlink_force@Base 0.4.17 click_unlink_force@Base 0.4.17 click_user_error_quark@Base 0.4.17 click_user_get_is_gc_in_use@Base 0.4.17 click_user_get_is_pseudo_user@Base 0.4.17 click_user_get_manifest@Base 0.4.18 click_user_get_manifest_as_string@Base 0.4.21 click_user_get_manifests@Base 0.4.18 click_user_get_manifests_as_string@Base 0.4.21 click_user_get_overlay_db@Base 0.4.17 click_user_get_package_names@Base 0.4.17 click_user_get_path@Base 0.4.17 click_user_get_type@Base 0.4.17 click_user_get_version@Base 0.4.17 click_user_has_package_name@Base 0.4.17 click_user_is_removable@Base 0.4.17 click_user_new_for_all_users@Base 0.4.17 click_user_new_for_gc_in_use@Base 0.4.17 click_user_new_for_user@Base 0.4.17 click_user_remove@Base 0.4.17 click_user_set_version@Base 0.4.17 click_users_get_type@Base 0.4.17 click_users_get_user@Base 0.4.17 click_users_get_user_names@Base 0.4.17 click_users_new@Base 0.4.17 click-0.5.0/debian/libclick-0.4-dev.install000066400000000000000000000001241402441472600202430ustar00rootroot00000000000000usr/include usr/lib/*/libclick*.so usr/lib/*/pkgconfig/click-*.pc usr/share/gir-1.0 click-0.5.0/debian/python3-click-package.install000066400000000000000000000000201402441472600214670ustar00rootroot00000000000000usr/lib/python3 click-0.5.0/debian/rules000077500000000000000000000027151402441472600151140ustar00rootroot00000000000000#! /usr/bin/make -f # The advantages of -Wl,-Bsymbolic-functions are of limited value here, and # they mean that the test suite's LD_PRELOAD tricks don't work properly. export DEB_LDFLAGS_MAINT_STRIP := -Wl,-Bsymbolic-functions %: dh $@ --with autoreconf,gir,python3,sphinxdoc,systemd \ --buildsystem autoconf $(EXTRA_DH_OPTIONS) PY3REQUESTED := $(shell py3versions -r) PY3DEFAULT := $(shell py3versions -d) # Run setup.py with the default python3 last so that the scripts use # #!/usr/bin/python3 and not #!/usr/bin/python3.X. PY3 := $(filter-out $(PY3DEFAULT),$(PY3REQUESTED)) python3 confflags := \ --with-python-interpreters='$(PY3)' \ --with-systemdsystemunitdir=/lib/systemd/system \ --with-systemduserunitdir=/usr/lib/systemd/user override_dh_autoreconf: dh_autoreconf -- ./autogen.sh override_dh_auto_configure: dh_auto_configure -- $(confflags) override_dh_auto_build: dh_auto_build $(MAKE) -C doc html man override_dh_auto_clean: dh_auto_clean $(MAKE) -C doc clean PYTHON_INSTALL_FLAGS := \ --force --root=$(CURDIR)/debian/tmp \ --no-compile -O0 --install-layout=deb override_dh_auto_install: dh_auto_install -- PYTHON_INSTALL_FLAGS='$(PYTHON_INSTALL_FLAGS)' rm -f debian/tmp/usr/lib/*/click/*.la override_dh_install: dh_install -X .la --fail-missing DH_AUTOSCRIPTDIR=debhelper debhelper/dh_click --name=click-desktop # Sphinx documentation is architecture-independent. override_dh_sphinxdoc-arch: override_dh_makeshlibs: dh_makeshlibs -- -c4 click-0.5.0/debian/tests/000077500000000000000000000000001402441472600151715ustar00rootroot00000000000000click-0.5.0/debian/tests/control000066400000000000000000000003711402441472600165750ustar00rootroot00000000000000Tests: run-tests.sh Depends: click, click-dev, debootstrap, debsig-verify, debsigs, gir1.2-click-0.4, iputils-ping, libclick-0.4-dev, python3-click-package, python3-six, schroot, sudo, @builddeps@, Restrictions: needs-root allow-stderr click-0.5.0/debian/tests/run-tests.sh000077500000000000000000000005331402441472600174750ustar00rootroot00000000000000#!/bin/sh set -e # some files like config.py are generated from config.py.in ./autogen.sh ./configure --prefix=/usr \ --sysconfdir=/etc \ --with-systemdsystemunitdir=/lib/systemd/system \ --with-systemduserunitdir=/usr/lib/systemd/user TEST_INTEGRATION=1 python3 -m unittest discover -vv click_package.tests.integration click-0.5.0/doc/000077500000000000000000000000001402441472600133525ustar00rootroot00000000000000click-0.5.0/doc/Makefile000066400000000000000000000127301402441472600150150ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/ClickPackages.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/ClickPackages.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/ClickPackages" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/ClickPackages" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." click-0.5.0/doc/conf.py000066400000000000000000000212171402441472600146540ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Click Packages documentation build configuration file, created by # sphinx-quickstart on Mon Apr 15 11:34:57 2013. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from datetime import datetime import io import os import re import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Click Packages' copyright = u'2013, Canonical Ltd.' changelog_header = re.compile(r'\w[-+0-9a-z.]* \(([^\(\) \t]+)\)') changelog_trailer = re.compile( r'^ \-\- .* <.*> ?((\w+\,\s*)?\d{1,2}\s+\w+\s+\d{4}\s+\d{1,2}:\d\d:\d\d)' r'\s+[-+]\d{4}(\s+\([^\\\(\)]\))?\s*$') with io.open('../debian/changelog', encoding="UTF-8") as changelog: line = changelog.readline() match = changelog_header.match(line) if match is None: raise ValueError( "Failed to parse first line of debian/changelog: '%s'" % line) click_version = match.group(1) for line in changelog: match = changelog_trailer.match(line) if match is not None: click_datetime = datetime.strptime( match.group(1), "%a, %d %b %Y %H:%M:%S") break elif changelog_header.match(line) is not None: break else: raise ValueError( "Failed to find trailer line in first entry of debian/changelog") # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = ".".join(click_version.split(".", 2)[:2]) # The full version, including alpha/beta/rc tags. release = click_version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: today = click_datetime.strftime("%Y-%m-%d") # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'ClickPackagesdoc' # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'ClickPackages.tex', u'Click Packages Documentation', u'Colin Watson', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('manpage', 'click', u'package management tool for Ubuntu Touch', [u'Colin Watson'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'Click', u'Click Packages Documentation', u'Colin Watson', 'Click', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' click-0.5.0/doc/constraints.rst000066400000000000000000000022051402441472600164520ustar00rootroot00000000000000================== Design constraints ================== Building packages ================= * Building packages should not require any more than the Python standard library. In particular, it should not require dpkg, python-debian, or any other such Debian-specific tools. Rationale: We want people to be able to build Click packages easily on any platform (or at least any platform that can manage a Python installation, which is not too onerous a requirement). Installing packages =================== * For the purpose of rapid prototyping, package installation is also implemented in Python. This may of course use Debian/Ubuntu-specific tools, since it will always be running on an Ubuntu system. In future, it will probably be re-implemented in C for performance. * Reading the system dpkg database is forbidden. This is partly to ensure strict separation, and partly because the system dpkg database is large and therefore slow to read. * Nothing should require root, although it may be acceptable to make use of root-only facilities if available (but remembering to pay attention to performance). click-0.5.0/doc/databases.rst000066400000000000000000000067621402441472600160460ustar00rootroot00000000000000========= Databases ========= (This is a lightly-edited copy of a brain-dump sent by Colin Watson to the ubuntu-phone mailing list, preserved here since it may be useful.) Click has multiple databases where packages may be unpacked: by default we have the "core" database for core apps (``/usr/share/click/preinstalled/``), the "custom" database for carrier/OEM customisations (``/custom/click/``), and the "default" database for user-installed applications (``/opt/click.ubuntu.com/``), although these are configurable in ``/etc/click/databases/``. Each database may have multiple unpacked versions of any given package. Each database may also have user registrations, which live in ``.click/users/`` relative to the database root. Each user has a subdirectory of that, which contains symlinks to the versions of each package they have registered. This means that on a tablet, say, I can install an app without it also showing up on my children's accounts; they'd need to install it separately, although the disk space for the unpacked copy of the app would be shared. There was an idea early on that we'd deal with preinstalled apps by going round and registering them all for all active users on first boot. This would have lots of problems for the packaging system, though. Most notably, doing it that way makes it hard for a user to remove an app and make it stick, because it would tend to reappear on system updates. You can probably fudge your way around this somehow, but it gets very fiddly and easy to get wrong. What we do instead is: we have an ``@all`` pseudo-user which you can register packages for, typically in the core database (``click register --root=/usr/share/click/preinstalled --all-users``). If a user wants to remove a package, we do this by creating a deliberately broken symlink pointing to ``@hidden`` in their user registration area in ``/opt/click.ubuntu.com/.click/users/$USERNAME/``. When click is asked to list the set of packages for a given user, it walks its way down the list of databases from top (default) to bottom (core). For each database, it checks registrations for that user, followed by registrations for ``@all``. It takes the first registration for any given package name that it finds. If that registration is ``@hidden``, then it ignores the package, otherwise it must be a link to the unpacked copy of the appropriate version of the package. There are still some things that can't be done just with static files in the image and instead have to be done at boot time and on session startup: we have to make sure the right AppArmor profiles are loaded, do things to the user's home directory like creating .desktop files, and that kind of thing. We run ``click hook run-system`` at boot time and ``click hook run-user`` on session startup, and these deal with running hooks for whatever packages are visible in context, according to the rules above. The effect of all this is that we can hide a core app for a carrier by doing this as root when preparing their custom overlay image:: click unregister --root=/custom/click --all-users PACKAGE-NAME This will create a symlink ``/custom/click/.click/users/@all/PACKAGE-NAME`` pointing to ``@hidden``. Unless a user explicitly installs the app in question, the effect of this will be that it's as if the app just isn't there. It shouldn't incur any more than a negligible cost at startup (basically just a readlink call); at the moment I think we might still create an AppArmor profile for it, which isn't free, but that can be fixed easily enough. click-0.5.0/doc/file-format.rst000066400000000000000000000205151402441472600163140ustar00rootroot00000000000000======================================== "Click" package file format, version 0.4 ======================================== This specification covers a packaging format intended for use by self-contained third-party applications. It is intentionally designed to make it easy to create such packages and for the archive of packages to be able to scale to very large numbers, as well as to ensure that packages do not execute any unverified code as root during installation and that installed packages are sandboxable. This implementation proposal uses the existing dpkg as its core, although that is entirely concealed from both users and application developers. The author believes that using something based on dpkg will allow us to reuse substantial amounts of package-management-related code elsewhere, not least the many years of careful design and bug-fixing of dpkg itself; although there are clearly several things we need to adjust. General format ============== The top-level binary format for Click packages is an ar archive containing control and data tar archives, as for .deb packages: see deb(5) for full details. The deb(5) format permits the insertion of underscore-prefixed ar members, so a "_click-binary" member should be inserted immediately after "debian-binary"; its contents should be the current version number of this specification followed by a newline. This makes it possible to assign a MIME type to Click packages without having to rely solely on their extension. Despite the similar format, the file extension for these packages is .click, to discourage attempts to install using dpkg directly (although it is still possible to use dpkg to inspect these files). Click packages should not be thought of as .deb packages, although they share tooling. Do not rely on the file extension remaining .click; it may change in the future. Control area ============ control ------- Every Click package must include the following control fields: * Click-Version: the current version number of this specification The package manager must refuse to process packages where any of these fields are missing or unparseable. It must refuse to process packages where Click-Version compares newer than the corresponding version it implements (according to rules equivalent to "dpkg --compare-versions"). It may refuse to process packages whose Click-Version field has an older major number than the version it implements (although future developers are encouraged to maintain the maximum possible degree of compatibility with packages in the wild). Several other fields are copied from the manifest, to ease interoperation with Debian package manipulation tools. The manifest is the primary location for these fields, and Click-aware tools must not rely on their presence in the control file. All dependency relations are forbidden. Packages implicitly depend on the entire contents of the Click system framework they declare. manifest -------- There must be a "manifest" file in the control area (typically corresponding to "manifest.json" in source trees), which must be a dictionary represented as UTF-8-encoded JSON. It must include the following keys: * name: unique name for the application * version: version number of the application * framework: the system framework(s) for which the package was built * installed-size: the size of the unpacked package in KiB; this should not be set directly in the source tree, but will be generated automatically by "click build" using "du -k -s --apparent-size" The package manager must refuse to process packages where any of these fields are missing or unparseable. It must refuse to process packages where the value of "framework" does not declare a framework implemented by the system on which the package is being installed. The value of "name" identifies the application, following Debian source package name rules; every package in the app store has a unique "name" identifier, and the app store will reject clashes. It is the developer's responsibility to choose a unique identifier. The recommended approach is to follow the Java package name convention, i.e. "com.mydomain.myapp", starting with the reverse of an Internet domain name owned by the person or organisation developing the application; note that it is not necessary for the application to contain any Java code in order to use this convention. The value of "version" provides a unique version for the application, following Debian version numbering rules. See deb-version(5) for full details. The syntax of "framework" is formally that of a Debian dependency relationship field. Currently, only a simple name is permitted, e.g. "framework": "ubuntu-sdk-13.10", or a list of simple names all of which must be satisfied, e.g. "framework": "ubuntu-sdk-14.04-qml, ubuntu-sdk-14.04-webapps"; version relationships and alternative dependencies are not currently allowed. The manifest may contain arbitrary additional optional keys; new optional keys may be defined without changing the version number of this specification. The following are currently recognised: * title: short (one-line) synopsis of the application * description: extended description of the application; may be multi-paragraph * maintainer: name and email address of maintainer of the application * architecture: one of the following: * "all", indicating a package containing no compiled code * a dpkg architecture name (e.g. "armhf") as a string, indicating a package that will only run on that architecture * a list of dpkg architecture names, indicating a package that will run on any of those architectures * hooks: see :doc:`hooks` * icon: icon to display in interfaces listing click packages; if the name refers to an existing file when resolved relative to the base directory of the package, the given file will be used; if not, the algorithm described in the `Icon Theme Specification `_ will be used to locate the icon Keys beginning with the two characters "x-" are reserved for local extensions: this file format will never define such keys to have any particular meaning. Keys beginning with an underscore ("_") are reserved for use as dynamic properties of installed packages. They must not appear in packages' manifest files, and attempts to set them there will be ignored. The following dynamic keys are currently defined: * _directory: the directory where a package is unpacked * _removable: 1 if a package is unpacked in a location from which it can be removed, otherwise 0 (this may be changed to a proper boolean in future; client code should be careful to permit either) Maintainer scripts ------------------ Maintainer scripts are forbidden, with one exception: see below. (If they are permitted in future, they will at most be required to consist only of verified debhelper-generated fragments that can be statically analysed.) Packages in Click system frameworks are encouraged to provide file triggers where appropriate (e.g. "interest /usr/share/facility"); these will be processed as normal for dpkg file triggers. The exception to maintainer scripts being forbidden is that a Click package may contain a preinst script with the effect of causing direct calls to dpkg to refuse to install it. The package manager must enforce the permitted text of this script. Data area ========= Unlike .debs, each package installs in a self-contained directory, and the filesystem tarball must be based at the root of that directory. The package must not assume any particular installation directory: if it needs to know where it is installed, it should look at argv[0] or similar. Within each package installation directory, the ".click" subdirectory will be used for metadata. This directory must not be present at the top level of package filesystem tarballs; the package manager should silently filter it out if present. (Rationale: scanning the filesystem tarball in advance is likely to impose a performance cost, especially for large packages.) The package manager should ensure that all unpacked files and directories are group- and world-readable, and (if owner-executable) also group- and world-executable. (Rationale: since packages are unpacked as a dedicated user not used when running applications, and since packages cannot write to their own unpack directories, any files that aren't world-readable are unusable.) click-0.5.0/doc/hooks.rst000066400000000000000000000247101402441472600152330ustar00rootroot00000000000000===== Hooks ===== Rationale --------- Of course, any sensible packaging format needs a hook mechanism of some kind; just unpacking a filesystem tarball isn't going to cut it. But part of the point of Click packages is to make packages easier to audit by removing their ability to run code at installation time. How do we resolve this? For most application packages, the code that needs to be run is to integrate with some system package; for instance, a package that provides an icon may need to update icon caches. Thus, the best way to achieve both these goals at once is to make sure the code for this is always in the integrated-with package. dpkg triggers are useful prior art for this approach. In general they get a lot of things right. The code to process a trigger runs in the postinst, which encourages an approach where trigger processing is a subset of full package configuration and shares code with it. Furthermore, the express inability to pass any user data through the trigger activation mechanism itself ensures that triggers must operate in a "catch up" style, ensuring that whatever data store they manage is up to date with the state of the parts of the file system they use as input. This naturally results in a system where the user can install integrating and integrated-with packages in either order and get the same result, a valuable property which developers are nevertheless unlikely to test explicitly in every case and which must therefore be encouraged by design. There are two principal problems with dpkg triggers (aside from the point that not all integrated-with packages use them, which is irrelevant because they don't support any hypothetical future hook mechanisms either). The first is that the inability to pass user data through trigger activation means that there is no way to indicate where an integrating package is installed, which matters when the hook files it provides cannot be in a single location under /usr/ but might be under /opt/ or even in per-user directories. The second is that processing dpkg triggers requires operating on the system dpkg database, which is large and therefore slow. Let us consider an example of the sort that might in future be delivered as a Click package, and one which is simple but not too simple. Our example package (com.ubuntu.example) delivers an AppArmor profile and two .desktop files. These are consumed by apparmor and desktop-integration (TBD) respectively, and each lists the corresponding directory looking for files to consume. We must assume that in the general case it will be at least inconvenient to cause the integrated-with packages to look in multiple directories, especially when the list of possible directories is not fixed, so we need a way to cause files to exist in those directories. On the other hand, we cannot unpack directly into those directories, because that takes us back to using dpkg itself, and is incompatible with system image updates where the root file system is read-only. What we can do with reasonable safety is populate symlink farms. Specification ------------- * Only system packages (i.e. .debs) may declare hooks. Click packages must be declarative in that they may not include code executed outside AppArmor confinement, which precludes declaring hooks. * "System-level hooks" are those which operate on the full set of installed package/version combinations. They may run as any (system) user. (Example: AppArmor profile handling.) * "User-level hooks" are those which operate on the set of packages registered by a given user. They run as that user, and thus would generally be expected to keep their state in the user's home directory or some similar user-owned file system location. (Example: desktop file handling.) * System-level and user-level hooks share a namespace. * A Click package may contain one or more applications (the common case will be only one). Each application has a name. * An "application ID" is a string unique to each application instance: it is made up of the Click package name, the application name (must consist only of characters for a Debian source package name, Debian version and [A-Z]), and the Click package version joined by underscores, e.g. ``com.ubuntu.clock_alarm_0.1``. * A "short application ID" is a string unique to each application, but not necessarily to each instance of it: it is made up of the Click package name and the application name (must consist only of characters for a Debian source package name, Debian version and [A-Z]) joined by an underscore, e.g. ``com.ubuntu.clock_alarm``. It is only valid in user-level hooks, or in system-level hooks with ``Single-Version: yes``. * An integrated-with system package may add ``*.hook`` files to ``/usr/share/click/hooks/``. These are standard Debian-style control files with the following keys: User-Level: yes (optional) If the ``User-Level`` key is present with the value ``yes``, the hook is a user-level hook. Pattern: (required) The value of ``Pattern`` is a string containing one or more substitution placeholders, as follows: ``${id}`` The application ID. ``${short-id}`` The short application ID (user-level or single-version hooks only). ``${user}`` The user name (user-level hooks only). ``${home}`` The user's home directory (user-level hooks only). ``$$`` The character '``$``'. At least one ``${id}`` or ``${short-id}`` substitution is required. For user-level hooks, at least one of ``${user}`` and ``${home}`` must be present. On install, the package manager creates the target path as a symlink to a path provided by the Click package; on upgrade, it changes the target path to be a symlink to the path in the new version of the Click package; on removal, it unlinks the target path. The terms "install", "upgrade", and "removal" are taken to refer to the status of the hook rather than of the package. That is, when upgrading between two versions of a package, if the old version uses a given hook but the new version does not, then that is a removal; if the old version does not use a given hook but the new version does, then that is an install; if both versions use a given hook, then that is an upgrade. For system-level hooks, one target path exists for each unpacked version, unless "``Single-Version: yes``" is used (see below). For user-level hooks, a target path exists only for the current version registered by each user for each package. Upgrades of user-level hooks may leave the symlink pointed at the same target (since the target will itself be via a ``current`` symlink in the user registration directory). ``Exec`` commands in hooks should take care to check the modification timestamp of the target. Exec: (optional) If the ``Exec`` key is present, its value is executed as if passed to the shell after the above symlink is modified. A non-zero exit status is an error; hook implementors must be careful to make commands in ``Exec`` fields robust. Note that this command intentionally takes no arguments, and will be run on install, upgrade, and removal; it must be written such that it causes the system to catch up with the current state of all installed hooks. ``Exec`` commands must be idempotent. Trigger: yes (optional) It will often be valuable to execute a dpkg trigger after installing a Click package to avoid code duplication between system and Click package handling, although we must do so asynchronously and any errors must not block the installation of Click packages. If "``Trigger: yes``" is set in a ``*.hook`` file, then "``click install``" will activate an asynchronous D-Bus service at the end of installation, passing the names of all the changed paths resulting from Pattern key expansions; this will activate any file triggers matching those paths, and process all the packages that enter the triggers-pending state as a result. User: (required, system-level hooks only) System-level hooks are run as the user whose name is specified as the value of ``User``. There is intentionally no default for this key, to encourage hook authors to run their hooks with the least appropriate privilege. Single-Version: yes (optional, system-level hooks only) By default, system-level hooks support multiple versions of packages, so target paths may exist at multiple versions. "``Single-Version: yes``" causes only the current version of each package to have a target path. Hook-Name: (optional) The value of ``Hook-Name`` is the name that Click packages may use to attach to this hook. By default, this is the base name of the ``*.hook`` file, with the ``.hook`` extension removed. Multiple hooks may use the same hook-name, in which case all those hooks will be run when installing, upgrading, or removing a Click package that attaches to that name. * A Click package may attach to zero or more hooks, by including a "hooks" entry in its manifest. If present, this must be a dictionary mapping application names to hook sets; each hook set is itself a dictionary mapping hook names to paths. The hook names are used to look up ``*.hook`` files with matching hook-names (see ``Hook-Name`` above). The paths are relative to the directory where the Click package is unpacked, and are used as symlink targets by the package manager when creating symlinks according to the ``Pattern`` field in ``*.hook`` files. * There is a dh_click program which installs the ``*.hook`` files in system packages and adds maintainer script fragments to cause click to catch up with any newly-provided hooks. It may be invoked using ``dh $@ --with click``. Examples -------- :: /usr/share/click/hooks/apparmor.hook: Pattern: /var/lib/apparmor/clicks/${id}.json Exec: /usr/bin/aa-clickhook User: root /usr/share/click/hooks/click-desktop.hook: User-Level: yes Pattern: /opt/click.ubuntu.com/.click/desktop-files/${user}_${id}.desktop Exec: click desktophook Hook-Name: desktop com.ubuntu.example/manifest.json: "hooks": { "example-app": { "apparmor": "apparmor/example-app.json", "desktop": "example-app.desktop" } } TODO: copy rather than symlink, for additional robustness? click-0.5.0/doc/index.rst000066400000000000000000000106351402441472600152200ustar00rootroot00000000000000.. Click Packages documentation master file, created by sphinx-quickstart on Mon Apr 15 11:34:57 2013. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ============== Click packages ============== *Click* is the code name used to describe a packaging format for Ubuntu mobile applications. This format specifies how individual apps are delivered to mobile devices, how they are packed into distributable format, and how they are installed on a mobile device by a system provided package manager. At a minimum they assume that a system framework exists providing all the necessary infrastructure and dependencies needed in order to install and run such apps. The click packaging format is completely independent from facilities to do full-system installations or upgrades. Compatibility ============= Currently, this package should remain compatible with Python 2.7, 3.2, 3.3, and 3.4; Ubuntu 12.04 LTS, Ubuntu 13.10, and Ubuntu 14.04 LTS. Build ===== If you run from a fresh git checkout, please ensure you have the required build dependencies first by running:: $ dpkg-checkbuilddeps and installing anything that is missing here. Then run:: $ ./autogen.sh $ ./configure --prefix=/usr \ --sysconfdir=/etc \ --with-systemdsystemunitdir=/lib/systemd/system \ --with-systemduserunitdir=/usr/lib/systemd/user $ make to build the project. Dependencies ------------ For Ubuntu 14.04, make sure you have the *python2.7* and *python3.4* packages installed. Unless you upgraded from a previous version of Ubuntu and haven't removed it yet, you won't have Python 3.3 and Python 3.2 available. Build them from source if necessary, install them say into ``/usr/local``, and make sure they are on your ``$PATH``. You'll need *tox* (Ubuntu package *python-tox*) installed in order to run the full test suite. You should be able to just say:: $ tox to run the full suite. Use tox's ``-e`` option to run the tests against a subset of Python versions. You shouldn't have to install anything manually into the virtual environments that tox creates, but you might have to if you don't have all the dependencies installed in your system Pythons. You'll need the *mock* and *python-debian* libraries. For Ubuntu 13.10, apt-get install the following packages:: * python-mock * python-debian * python3-debian Testing ======= After all of the above is installed, you can run ``tox`` to run the test suite against all supported Python versions. The ``./run-tests`` scripts just does an additional check to make sure you've got the preload shared library built. To run a specific testcase, use the standard python unittest syntax like:: $ python3 -m unittest click_package.tests.test_install or:: $ python2 -m unittest click_package.tests.test_build.TestClickBuilder.test_build Test coverage ------------- If you have python-coverage installed, you can get a Python test coverage report by typing: $ python-coverage combine $ python-coverage report This works also for python3-coverage. To get Vala/C coverage information, install the gcovr and lcov packages and run: $ ./configure --enable-gcov $ make coverage-html which will generate a "coveragereport/index.html" file for you. The combined coverage information can be obtained via: $ make coverage.xml Integration Tests ----------------- There is also a set of integration tests that have additional test dependencies that are listed in debian/test/control. Beware that some require to be run as root and they are designed to be run in a safe environment (like a schroot or a autopkgtest container) and may alter the system state (e.g adding test users). By default the tests will run against the installed click binary, but you can also use: $ LD_LIBRARY_PATH=$(pwd)/lib/click/.libs \ PYTHONPATH=$(pwd) \ GI_TYPELIB_PATH=$(pwd)/lib/click \ CLICK_BINARY=$(pwd)/bin/click \ TEST_INTEGRATION=1 \ python3 -m unittest discover click_package.tests.integration to run against the build tree. Documentation ============= To build the HTML version of the documentation, you'll need Sphinx (Ubuntu package *python-sphinx*). Then do:: $ (cd doc && make html) Contents: .. toctree:: :maxdepth: 2 file-format.rst constraints.rst hooks.rst databases.rst todo.rst Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` click-0.5.0/doc/manpage.rst000066400000000000000000000306071402441472600155220ustar00rootroot00000000000000===== click ===== SYNOPSIS ======== click command [options] [arguments] DESCRIPTION =========== *Click* is a packaging format for Ubuntu Touch applications, independent of the packaging format used to deliver the underlying system. The *click* program is the basic tool used to build, install, remove, and otherwise manipulate these packages. *click*'s various functions are available via a number of commands, described in detail below. While *click* supports per-user installation, packages are normally unpacked as a special ``clickpkg`` user, to ensure that applications cannot modify their own code; it is a design goal to ensure that *click* can be used to install untrusted code which is then confined using `AppArmor `_. As such, *click* should normally be run as root (e.g. using ``sudo``) when installing packages; it will drop privileges as needed. COMMAND OVERVIEW ================ :: click build DIRECTORY click buildsource DIRECTORY click chroot click contents PATH click framework list click hook install HOOK click hook remove HOOK click hook run-system click hook run-user click info PATH click install PACKAGE-FILE click list click pkgdir {PACKAGE-NAME|PATH} click register PACKAGE-NAME VERSION click unregister PACKAGE-NAME [VERSION] click verify PACKAGE-FILE COMMANDS ======== click build DIRECTORY --------------------- Build a Click package from the contents of DIRECTORY. The build directory must contain a JSON-formatted manifest, described further in Click's file-format documentation; by default, this is expected to be in ``manifest.json`` at the top level of the build directory. The resulting ``.click`` file is written to the current directory, so to avoid confusion you should generally ensure that your working directory is not inside the build directory when running this command. While it is possible to build a new version of a Click package by unpacking and repacking an existing package, this is not normally recommended because it requires some care to put the manifest file back in the right place. It is best to keep your application's code in separate revision control rather than relying on recovering it from packages. Options: -m PATH, --manifest=PATH Read package manifest from PATH (default: ``manifest.json``). -I file-pattern, --ignore=file-pattern Ignore the given shell-pattern when building the package. The option may be repeated multiple times to list multiple patterns to exclude. --no-validate Don't run checks from click-reviewers-tools on the resulting .click file. click buildsource DIRECTORY --------------------------- Build a source package in ``.tar.gz`` format from the contents of DIRECTORY. This allows you to distribute source code in the case where your package contains compiled code (and so the Click package does not constitute its own source). The resulting ``.tar.gz`` file is written to the current directory, so to avoid confusion you should generally ensure that your working directory is not inside the build directory when running this command. Options: -m PATH, --manifest=PATH Read package manifest from PATH (default: ``manifest.json``). -I file-pattern, --ignore=file-pattern Ignore the given shell-pattern when building the package. The option may be repeated multiple times to list multiple patterns to exclude. click chroot ------------ Manage chroot environments for cross-building Click packages. Options: -a ARCH, --architecture ARCH Set the target architecture. -f FRAMEWORK, --framework FRAMEWORK Set the target framework (default: ubuntu-sdk-13.10). -s SERIES, --series SERIES Set the target series for newly-created chroots (default: a series appropriate for the framework). This option is mainly for debugging; use -f instead. Subcommands: begin-session SESSION Begin a persistent chroot session. create Create a chroot. destroy Destroy a chroot. end-session SESSION End a persistent chroot session. install [-n SESSION] PACKAGES Install packages in the chroot. maint [-n SESSION] COMMAND ARGUMENTS Run a maintenance command in the chroot. Unlike ``run``, this runs its command as root inside the chroot, and its effects on the chroot will persist after ``click chroot maint`` exits. If a session name is given, run the command in that session. The session must previously have been created by ``click chroot begin-session``. run [-n SESSION] COMMAND ARGUMENTS Run a program in the chroot. If a session name is given, run the command in that session. The session must previously have been created by ``click chroot begin-session``. upgrade [-n SESSION] Upgrade the chroot. click contents PATH ------------------- Display the contents of the Click package in PATH as a file listing. click framework list -------------------- Display a list of available frameworks as one framework per line. click hook install HOOK ----------------------- Install files associated with HOOK for any Click packages that attach to it. This is normally only called by maintainer scripts of system packages, by way of dh_click(1). Options: --root=PATH Look for additional packages in PATH. click hook remove HOOK ---------------------- Remove files associated with HOOK for any Click packages that attach to it. This is normally only called by maintainer scripts of system packages, by way of dh_click(1). Options: --root=PATH Look for additional packages in PATH. click hook run-system ------------------------- Run all system-level hooks for all installed Click packages. This is useful when starting up from images with preinstalled packages which may not have had their system-level hooks run properly when building the image. Options: --root=PATH Look for additional packages in PATH. click hook run-user ----------------------- Run all user-level hooks for all Click packages registered for a given user. This is useful at session startup to catch up with packages that may have been preinstalled and registered for all users. Options: --root=PATH Look for additional packages in PATH. --user=USER Run user-level hooks for USER (default: current user). click info {PACKAGE-NAME|PACKAGE-FILE} -------------------------------------- When given a package name (that is, a string containing no ``/`` characters), display the manifest for that package, if it is registered for the current user. When given a path (that is, a string containing at least one ``/`` character, or a string containing no ``/`` characters that is not a registered package name), attempt to treat that as a path to a file containing a Click package and display the manifest for that package. Options: --root=PATH Look for additional packages in PATH. --user=USER List packages registered by USER (if you have permission). click install PACKAGE-FILE -------------------------- Install the Click package in PACKAGE-FILE. This is a low-level tool; to install a package as an ordinary user you should generally use ``pkcon install-local PACKAGE-FILE`` or some higher-level user interface instead, which take care to use the correct set of options. (Do not use ``sudo`` when invoking ``pkcon``, as it needs to know the calling user.) ``click install`` may be used to preinstall a package in an image such that it will be available to all users by default. When doing this, you should normally install it to one of the databases defined in ``/etc/click/databases/`` other than the default of ``/opt/click.ubuntu.com``. For example: sudo click install --root=/custom/click --all-users foo.click The ``--force-missing-framework`` option is necessary while working with development versions of SDKs which have not yet put a framework declaration in place. You should always register installed packages either for a specific user or for all users; if you do not do this then the packages may be garbage-collected later. You can do this using the ``--user`` or ``--all-users`` options to this command, or using the ``click register`` command. Options: --root=PATH Install packages underneath PATH. --force-missing-framework Install despite missing system framework. --user=USER Register package for USER. --all-users Register package for all users. click list ---------- Display a list of installed packages, either as one package per line with each line containing a package name and version separated by a tab (the default), or as a JSON array of manifests. By default, ``click list`` shows only packages registered for the current user. The ``--all`` option causes it to show all installed packages, regardless of user registrations. Options: --root=PATH Look for additional packages in PATH. --all List all installed packages. --user=USER List packages registered by USER (if you have permission). --manifest Format output as a JSON array of manifests. click pkgdir {PACKAGE-NAME|PATH} -------------------------------- When given a package name (that is, a string containing no ``/`` characters), display the directory where that package is installed, if it is registered for the current user. When given a path (that is, a string containing at least one ``/`` character), attempt to treat that as a path to a file within a Click package and print the top-level directory where that package is installed, if one exists. This is particularly useful in hooks that need to find the top-level package directory based on a symbolic link to a single file within it. Exits zero if and only if a directory for the given package name or path was found. Options: --root=PATH Look for additional packages in PATH. --user=USER List packages registered by USER (if you have permission). click register PACKAGE-NAME VERSION ----------------------------------- Register an installed Click package for a user. This will normally cause user-level hooks to be run for that user, which are needed for things such as making the application's ``.desktop`` file available to the user interface. Options: --root=PATH Look for additional packages in PATH. --user=USER Register package for USER (default: current user). --all-users Register package for all users. click unregister PACKAGE-NAME [VERSION] --------------------------------------- Unregister an installed Click package for a user, and remove it entirely if no other users still have it registered and if it does not appear to be running. This will normally cause user-level hooks to be run for that user, which are needed for things such as removing the application's ``.desktop`` file from the user interface. If a version is specified, then the registered version must match it in order to be removed. Options: --root=PATH Look for additional packages in PATH. --user=USER Unregister package for USER (default: ``$SUDO_USER``, if known). --all-users Unregister package that was previously registered for all users. click verify PACKAGE-FILE ------------------------- Verify the Click package in PACKAGE-FILE. The ``--force-missing-framework`` option is necessary while working with development versions of SDKs which have not yet put a framework declaration in place. Options: --root=PATH Install packages underneath PATH. --force-missing-framework Install despite missing system framework. click-0.5.0/doc/todo.rst000066400000000000000000000030441402441472600150520ustar00rootroot00000000000000===== To do ===== * hook that gets notified about all installations * dbus interface etc. as backend for UI * method may not be feasible because caller may want to go away * but where do we send a completion/failure signal back to? * some way to manage shared data files * association with developer ID, to allow sharing of data * debug symbols * define exit statuses for "click install" * command to generate manifest template, like ``dh_make`` * check whether a package contains compiled code for an architecture not listed in the "architecture" manifest field Delta updates ============= It would be helpful to have some kind of delta update format. Tools such as ``rsync`` and ``zsync`` are probably the wrong answer. There's no particular reason to keep the .click file around as an rsync target, particularly since the unpacked application directory is kept pristine, and many devices won't have the kind of disk space where you want to keep 4.2GB files around just for the sake of it. We could do something ad-hoc with ``xdelta`` or ``bsdiff`` or whatever. `debdelta `_ seems like a good possibility. We're already using the .deb format, and debdelta is capable of doing patch upgrades without having the old .deb around (though it will need minor adjustments to cope with the different installation location of Click packages). Under the hood, it uses xdelta/bsdiff/etc. and can be extended with other backends if need be. If we used this then we could take advantage of a good deal of existing code. click-0.5.0/get-version000077500000000000000000000001271402441472600147750ustar00rootroot00000000000000#! /bin/sh perl -e '$_ = <>; chomp; s/.*\((.*)\).*/\1/; print; exit' $@ endif click-0.5.0/init/systemd/click-system-hooks.service.in000066400000000000000000000003211402441472600227530ustar00rootroot00000000000000[Unit] Description=Run Click system-level hooks Documentation=man:click(1) [Service] Type=oneshot RemainAfterExit=yes ExecStart=@bindir@/click hook run-system Restart=no [Install] WantedBy=multi-user.target click-0.5.0/init/systemd/click-user-hooks.service.in000066400000000000000000000003121402441472600224050ustar00rootroot00000000000000[Unit] Description=Run Click user-level hooks Documentation=man:click(1) [Service] Type=oneshot RemainAfterExit=yes ExecStart=@bindir@/click hook run-user Restart=no [Install] WantedBy=default.target click-0.5.0/lib/000077500000000000000000000000001402441472600133535ustar00rootroot00000000000000click-0.5.0/lib/Makefile.am000066400000000000000000000000201402441472600153770ustar00rootroot00000000000000SUBDIRS = click click-0.5.0/lib/click/000077500000000000000000000000001402441472600144405ustar00rootroot00000000000000click-0.5.0/lib/click/Makefile.am000066400000000000000000000043071402441472600165000ustar00rootroot00000000000000AM_CPPFLAGS = \ -I. \ -D_GNU_SOURCE AM_CFLAGS = \ $(LIBCLICK_CFLAGS) \ $(VALA_CFLAGS) \ $(COVERAGE_CFLAGS) \ -Wno-unused-but-set-variable \ -Wno-unused-function \ -Wno-unused-variable VALAC = $(srcdir)/valac-wrapper AM_VALAFLAGS = \ -H click.h \ --gir Click-0.4.gir \ --library click-0.4 \ --pkg posix \ --pkg gee-0.8 \ --pkg json-glib-1.0 \ --target-glib 2.32 lib_LTLIBRARIES = libclick-0.4.la libclick_0_4_la_SOURCES = \ database.vala \ deb822.vala \ framework.vala \ hooks.vala \ osextras.vala \ paths.vala \ posix-extra.vapi \ query.vala \ user.vala EXTRA_libclick_0_4_la_DEPENDENCIES = \ click.sym HEADER_FILES = \ click.h BUILT_SOURCES = paths.vala CLEANFILES = \ $(BUILT_SOURCES) \ $(HEADER_FILES) \ libclick_0_4_la_vala.stamp \ click.h \ database.c \ deb822.c \ framework.c \ hooks.c \ osextras.c \ paths.c \ query.c \ user.c do_subst = sed \ -e 's,[@]sysconfdir[@],$(sysconfdir),g' \ -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' paths.vala: paths.vala.in Makefile $(do_subst) < $(srcdir)/paths.vala.in > $@ includeclickdir = $(includedir)/click-0.4 includeclick_HEADERS = \ $(HEADER_FILES) libclick_0_4_la_LIBADD = $(LIBCLICK_LIBS) libclick_0_4_la_LDFLAGS = \ $(COVERAGE_LDFLAGS) \ -export-dynamic \ -export-symbols $(srcdir)/click.sym \ -version-info 4:0:4 EXTRA_DIST = click-0.4.pc.in pkgconfigdir = $(libdir)/pkgconfig pkgconfig_DATA = click-0.4.pc INTROSPECTION_COMPILER_ARGS = \ --includedir $(srcdir) \ --includedir $(builddir) \ --shared-library libclick-0.4.so.0 girdir = $(datadir)/gir-1.0 gir_DATA = Click-0.4.gir typelibdir = $(libdir)/girepository-1.0 typelib_DATA = Click-0.4.typelib # We intentionally don't install a VAPI at this point; libclick is written # in Vala for implementation convenience, but this probably won't be # appropriate for most of its clients. The intent is that the C API is # canonical (with its reflections via gobject-introspection). #vapidir = $(VAPIGEN_VAPIDIR) #vapi_DATA = click-0.4.vapi noinst_DATA = click-0.4.vapi CLEANFILES += $(gir_DATA) $(typelib_DATA) $(noinst_DATA) $(HEADER_FILES) $(gir_DATA) $(noinst_DATA): libclick_0_4_la_vala.stamp %.typelib: %.gir $(INTROSPECTION_COMPILER) $(INTROSPECTION_COMPILER_ARGS) $< -o $@ click-0.5.0/lib/click/click-0.4.pc.in000066400000000000000000000017211402441472600167560ustar00rootroot00000000000000# Copyright (C) 2014 Canonical Ltd. # # This file is part of click. # # click 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; version 3 of the License. # # click 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 click. If not, see . prefix=@prefix@ exec_prefix=@exec_prefix@ libdir=@libdir@ includedir=@includedir@ Name: @PACKAGE_NAME@ Description: Click package manipulation library Version: @PACKAGE_VERSION@ URL: https://click.readthedocs.io/en/latest/ Requires.private: glib-2.0 gobject-2.0 json-glib-1.0 Libs: -L${libdir} -lclick-0.4 Cflags: -I${includedir}/click-0.4 click-0.5.0/lib/click/click.sym000066400000000000000000000051701402441472600162620ustar00rootroot00000000000000click_database_error_quark click_db_add click_db_ensure_ownership click_db_gc click_db_get click_db_get_manifest click_db_get_manifest_as_string click_db_get_manifests click_db_get_manifests_as_string click_db_get_overlay click_db_get_packages click_db_get_path click_db_get_size click_db_get_type click_db_has_package_version click_db_maybe_remove click_db_new click_db_read click_dir_get_type click_dir_open click_dir_read_name click_ensuredir click_find_on_path click_find_package_directory click_framework_error_quark click_framework_get_base_name click_framework_get_base_version click_framework_get_fields click_framework_get_field click_framework_get_frameworks click_framework_get_name click_framework_get_type click_framework_has_framework click_framework_open click_get_db_dir click_get_frameworks_dir click_get_hooks_dir click_get_umask click_get_user_home click_hook_get_app_id click_hook_get_field click_hook_get_fields click_hook_get_hook_name click_hook_get_is_single_version click_hook_get_is_user_level click_hook_get_pattern click_hook_get_run_commands_user click_hook_get_short_app_id click_hook_get_type click_hook_install click_hook_install_package click_hook_open click_hook_open_all click_hook_remove click_hook_remove_package click_hook_run_commands click_hook_sync click_hooks_error_quark click_installed_package_get_package click_installed_package_get_path click_installed_package_get_type click_installed_package_get_version click_installed_package_get_writeable click_installed_package_new click_package_install_hooks click_package_remove_hooks click_pattern_format click_pattern_possible_expansion click_query_error_quark click_run_system_hooks click_run_user_hooks click_single_db_any_app_running click_single_db_app_running click_single_db_ensure_ownership click_single_db_gc click_single_db_get_manifest click_single_db_get_manifest_as_string click_single_db_get_packages click_single_db_get_path click_single_db_get_root click_single_db_get_type click_single_db_has_package_version click_single_db_maybe_remove click_single_db_new click_symlink_force click_unlink_force click_user_error_quark click_user_get_is_gc_in_use click_user_get_is_pseudo_user click_user_get_manifest click_user_get_manifest_as_string click_user_get_manifests click_user_get_manifests_as_string click_user_get_overlay_db click_user_get_package_names click_user_get_path click_user_get_type click_user_get_version click_user_has_package_name click_user_is_removable click_user_new_for_all_users click_user_new_for_gc_in_use click_user_new_for_user click_user_remove click_user_set_version click_users_get_type click_users_get_user click_users_get_user_names click_users_new click-0.5.0/lib/click/database.vala000066400000000000000000000547361402441472600170700ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Click databases. */ namespace Click { public errordomain DatabaseError { /** * A package/version does not exist. */ DOES_NOT_EXIST, /** * Failure to remove package. */ REMOVE, /** * Failure to ensure correct ownership of database files. */ ENSURE_OWNERSHIP, /** * Package manifest cannot be parsed. */ BAD_MANIFEST, /** * No database available for the given request */ INVALID } private string? app_pid_command = null; private unowned string? get_app_pid_command () { if (app_pid_command == null) { if (find_on_path ("lomiri-app-pid")) app_pid_command = "lomiri-app-pid"; else app_pid_command = ""; } if (app_pid_command == "") return null; else return app_pid_command; } public class InstalledPackage : Object, Gee.Hashable { public string package { get; construct; } public string version { get; construct; } public string path { get; construct; } public bool writeable { get; construct; default = true; } public InstalledPackage (string package, string version, string path, bool writeable = true) { Object (package: package, version: version, path: path, writeable: writeable); } public uint hash () { return package.hash () ^ version.hash () ^ path.hash () ^ (writeable ? 1 : 0); } public bool equal_to (InstalledPackage obj) { return package == obj.package && version == obj.version && path == obj.path && writeable == obj.writeable; } } public class SingleDB : Object { public string root { get; construct; } public DB master_db { private get; construct; } public SingleDB (string root, DB master_db) { Object (root: root, master_db: master_db); } private bool show_messages () { return Environment.get_variable ("TEST_QUIET") == null; } /** * get_path: * @package: A package name. * @version: A version string. * * Returns: The path to this version of this package. */ public string get_path (string package, string version) throws DatabaseError { var try_path = Path.build_filename (root, package, version); if (exists (try_path)) return try_path; else throw new DatabaseError.DOES_NOT_EXIST ("%s %s does not exist in %s", package, version, root); } /** * has_package_version: * @package: A package name. * @version: A version string. * * Returns: True if this version of this package is unpacked in this * database, otherwise false. * * Since: 0.4.18 */ public bool has_package_version (string package, string version) { try { get_path (package, version); return true; } catch (DatabaseError e) { return false; } } /** * get_packages: * @all_versions: If true, return all versions, not just current ones. * * Returns: A list of #InstalledPackage instances corresponding to * package versions in only this database. */ public List get_packages (bool all_versions = false) throws Error { var ret = new List (); foreach (var package in Click.Dir.open (root)) { if (package == ".click") continue; if (all_versions) { var package_path = Path.build_filename (root, package); if (! is_dir (package_path)) continue; foreach (var version in Click.Dir.open (package_path)) { var version_path = Path.build_filename (package_path, version); if (is_symlink (version_path) || ! is_dir (version_path)) continue; ret.prepend(new InstalledPackage (package, version, version_path)); } } else { var current_path = Path.build_filename (root, package, "current"); if (! is_symlink (current_path)) continue; var version = FileUtils.read_link (current_path); if (! ("/" in version)) ret.prepend(new InstalledPackage (package, version, current_path)); } } ret.reverse (); return ret; } /** * get_manifest: * @package: A package name. * @version: A version string. * * Returns: A #Json.Object containing the manifest of this version * of this package. The manifest may include additional dynamic * keys (starting with an underscore) corresponding to dynamic * properties of installed packages. * * Since: 0.4.18 */ public Json.Object get_manifest (string package, string version) throws DatabaseError { /* Extract the raw manifest from the file system. */ var path = get_path (package, version); var manifest_path = Path.build_filename (path, ".click", "info", @"$package.manifest"); var parser = new Json.Parser (); try { parser.load_from_file (manifest_path); } catch (Error e) { throw new DatabaseError.BAD_MANIFEST ("Failed to parse manifest in %s: %s", manifest_path, e.message); } var node = parser.get_root (); if (node.get_node_type () != Json.NodeType.OBJECT) throw new DatabaseError.BAD_MANIFEST ("Manifest in %s is not a JSON object", manifest_path); var manifest = node.dup_object (); /* Set up dynamic keys. */ var to_remove = new List (); foreach (var name in manifest.get_members ()) { if (name.has_prefix ("_")) to_remove.prepend (name); } foreach (var name in to_remove) manifest.remove_member (name); manifest.set_string_member ("_directory", path); return manifest; } /** * get_manifest_as_string: * @package: A package name. * @version: A version string. * * Returns: A JSON string containing the serialised manifest of this * version of this package. The manifest may include additional * dynamic keys (starting with an underscore) corresponding to * dynamic properties of installed packages. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifest_as_string (string package, string version) throws DatabaseError { var manifest = get_manifest (package, version); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (manifest); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /* * app_running: * @package: A package name. * @app_name: An application name. * @version: A version string. * * Returns: True if @app_name from version @version of @package is * known to be running, otherwise false. */ public bool app_running (string package, string app_name, string version) { string[] command = { get_app_pid_command (), @"$(package)_$(app_name)_$(version)" }; assert (command[0] != null); try { int exit_status; Process.spawn_sync (null, command, null, SpawnFlags.SEARCH_PATH | SpawnFlags.STDOUT_TO_DEV_NULL, null, null, null, out exit_status); return Process.check_exit_status (exit_status); } catch (Error e) { return false; } } /* * any_app_running: * @package: A package name. * @version: A version string. * * Returns: True if any application from version @version of * @package is known to be running, otherwise false. */ public bool any_app_running (string package, string version) throws DatabaseError { if (get_app_pid_command () == null) return false; var manifest_path = Path.build_filename (get_path (package, version), ".click", "info", @"$package.manifest"); var parser = new Json.Parser (); try { parser.load_from_file (manifest_path); var manifest = parser.get_root ().get_object (); if (! manifest.has_member ("hooks")) return false; var hooks = manifest.get_object_member ("hooks"); foreach (unowned string app_name in hooks.get_members ()) { if (app_running (package, app_name, version)) return true; } } catch (Error e) { } return false; } private void remove_unless_running (string package, string version) throws Error { if (any_app_running (package, version)) return; var version_path = get_path (package, version); if (show_messages ()) message ("Removing %s", version_path); package_remove_hooks (master_db, package, version); var file = File.new_for_path (version_path); try { rmtree (file, null); } catch (Error e) { warning ("Error removing '%s': %s", version_path, e.message); } var package_path = Path.build_filename (root, package); var current_path = Path.build_filename (package_path, "current"); if (is_symlink (current_path) && FileUtils.read_link (current_path) == version) { if (FileUtils.unlink (current_path) < 0) throw new DatabaseError.REMOVE ("unlink %s failed: %s", current_path, strerror (errno)); /* TODO: Perhaps we should relink current to the * latest remaining version. However, that requires * version comparison, and it's not clear whether * it's worth it given that current is mostly * superseded by user registration. */ } if (DirUtils.remove (package_path) < 0) { if (errno != Posix.ENOTEMPTY && errno != Posix.EEXIST) throw new DatabaseError.REMOVE ("rmdir %s failed: %s", package_path, strerror (errno)); } } /** * maybe_remove: * @package: A package name. * @version: A version string. * * Remove a package version if it is not in use. * * "In use" may mean registered for another user, or running. In * either case, we do nothing. We will already have removed at * least one registration by this point, so if no registrations are * left but it is running, then gc will be able to come along later * and clean things out. */ public void maybe_remove (string package, string version) throws Error { var users_db = new Users (master_db); foreach (var user_name in users_db.get_user_names ()) { var user_db = users_db.get_user (user_name); string reg_version; try { reg_version = user_db.get_version (package); } catch (UserError e) { continue; } if (reg_version == version) { /* In use. */ return; } } remove_unless_running (package, version); } /** * gc: * * Remove package versions that have no user registrations and that * are not running. * * This is rather like maybe_remove, but is suitable for bulk use, * since it only needs to scan the database once rather than once * per package. * * For historical reasons, we don't count @gcinuse as a real user * registration, and remove any such registrations we find. We can * drop this once we no longer care about upgrading versions from * before this change to something more current in a single step. */ public void gc () throws Error { // Clean up user registrations: registrations for old packages should // be updated to point to the newest version available. TODO: Check // registration timestamps and compare to package timestamps before // blindly re-registering so old versions can still be registered if // they were done so after the new package was installed. foreach (var package in master_db.get_packages(true)) { var users_db = new Users (master_db); foreach (var name in users_db.get_user_names ()) { var user_db = users_db.get_user (name); try { string registered_version = user_db.get_version (package.package); string[] args = { "dpkg", "--compare-versions", registered_version, "lt", package.version }; int exit_status; Process.spawn_sync (null, args, null, SpawnFlags.SEARCH_PATH, null, null, null, out exit_status); // This will throw if non-zero Process.check_exit_status (exit_status); // Update the user's registered version if necessary. user_db.set_version (package.package, package.version); } catch { // User was either not registered for this app or was // registered for this version (or newer). Either way, // skip it. } } } var users_db = new Users (master_db); var user_reg = new Gee.HashMultiMap (); foreach (var user_name in users_db.get_user_names ()) { var user_db = users_db.get_user (user_name); foreach (var package in user_db.get_package_names ()) { var version = user_db.get_version (package); if (version == "current") continue; /* Odd multimap syntax; this should really * be more like foo[package] += version. */ if (! user_db.is_gc_in_use) user_reg[package] = version; } } var gc_in_use_user_db = new User.for_gc_in_use (master_db); foreach (var package in Click.Dir.open (root)) { if (package == ".click") continue; var package_path = Path.build_filename (root, package); if (! is_dir (package_path)) continue; foreach (var version in Click.Dir.open (package_path)) { if (version == "current") continue; if (version in user_reg[package]) /* In use. */ continue; if (gc_in_use_user_db.has_package_name (package)) gc_in_use_user_db.remove (package); remove_unless_running (package, version); } } } private delegate void WalkFunc (string dirpath, string[] dirnames, string[] filenames) throws Error; /** * walk: * * An reduced emulation of Python's os.walk. */ private void walk (string top, WalkFunc func) throws Error { string[] dirs = {}; string[] nondirs = {}; foreach (var name in Click.Dir.open (top)) { var path = Path.build_filename (top, name); if (is_dir (path)) dirs += name; else nondirs += name; } func (top, dirs, nondirs); foreach (var name in dirs) { var path = Path.build_filename (top, name); if (! is_symlink (path)) walk (path, func); } } private delegate void ClickpkgForeachFunc (string path) throws DatabaseError; /** * foreach_clickpkg_path: * * Call a delegate for each path which should be owned by clickpkg. */ private void foreach_clickpkg_path (ClickpkgForeachFunc func) throws Error { if (exists (root)) func (root); foreach (var package in Click.Dir.open (root)) { var path = Path.build_filename (root, package); if (package == ".click") { func (path); var log_path = Path.build_filename (path, "log"); if (exists (log_path)) func (log_path); var users_path = Path.build_filename (path, "users"); if (exists (users_path)) func (users_path); } else { walk (path, (dp, dns, fns) => { func (dp); foreach (var dn in dns) { var dnp = Path.build_filename (dp, dn); if (is_symlink (dnp)) func (dnp); } foreach (var fn in fns) { var fnp = Path.build_filename (dp, fn); func (fnp); } }); } } } /** * ensure_ownership: * * Ensure correct ownership of files in the database. * * On a system that is upgraded by delivering a new system image * rather than by package upgrades, it is possible for the clickpkg * UID to change. The overlay database must then be adjusted to * account for this. */ public void ensure_ownership () throws Error { errno = 0; unowned Posix.Passwd? pw = Posix.getpwnam ("clickpkg"); if (pw == null) throw new DatabaseError.ENSURE_OWNERSHIP ("Cannot get password file entry for " + "clickpkg: %s", strerror (errno)); Posix.Stat st; if (Posix.stat (root, out st) < 0) return; if (st.st_uid == pw.pw_uid && st.st_gid == pw.pw_gid) return; foreach_clickpkg_path ((path) => { if (Posix.chown (path, pw.pw_uid, pw.pw_gid) < 0) throw new DatabaseError.ENSURE_OWNERSHIP ("Cannot set ownership of %s: %s", path, strerror (errno)); }); } } public class DB : Object { private Gee.ArrayList db = new Gee.ArrayList (); public DB () {} public void read (string? db_dir = null) throws FileError { string real_db_dir = (db_dir == null) ? get_db_dir () : db_dir; foreach (var name in Click.Dir.open (real_db_dir)) { if (! name.has_suffix (".conf")) continue; var path = Path.build_filename (real_db_dir, name); var config = new KeyFile (); string root; try { config.load_from_file (path, KeyFileFlags.NONE); root = config.get_string ("Click Database", "root"); } catch (Error e) { warning ("%s", e.message); continue; } assert (root != null); add (root); } } public int size { get { return db.size; } } public new SingleDB @get (int index) throws DatabaseError { if (index >= db.size) throw new DatabaseError.INVALID ("invalid index %i for db of size %i", index, db.size); return db.get (index); } public new void add (string root) { db.add (new SingleDB (root, this)); } /** * overlay: * * The directory where changes should be written. */ public string overlay { get { if (db.size == 0) return ""; else return db.last ().root; } } /** * get_path: * @package: A package name. * @version: A version string. * * Returns: The path to this version of this package. */ public string get_path (string package, string version) throws DatabaseError { foreach (var single_db in db) { try { return single_db.get_path (package, version); } catch (DatabaseError e) { } } throw new DatabaseError.DOES_NOT_EXIST ("%s %s does not exist in any database", package, version); } /** * has_package_version: * @package: A package name. * @version: A version string. * * Returns: True if this version of this package is unpacked, * otherwise false. */ public bool has_package_version (string package, string version) { try { get_path (package, version); return true; } catch (DatabaseError e) { return false; } } /** * get_packages: * @all_versions: If true, return all versions, not just current ones. * * Returns: A list of #InstalledPackage instances corresponding to * package versions in all databases. */ public List get_packages (bool all_versions = false) throws Error { var ret = new List (); var seen = new Gee.HashSet (); var writeable = true; for (int i = db.size - 1; i >= 0; --i) { var child_packages = db[i].get_packages (all_versions); foreach (var pkg in child_packages) { string seen_id; if (all_versions) seen_id = ( pkg.package + "_" + pkg.version); else seen_id = pkg.package.dup (); if (! (seen_id in seen)) { ret.prepend(new InstalledPackage (pkg.package, pkg.version, pkg.path, writeable)); seen.add (seen_id); } } writeable = false; } ret.reverse (); return ret; } /** * get_manifest: * @package: A package name. * @version: A version string. * * Returns: A #Json.Object containing the manifest of this version * of this package. * * Since: 0.4.18 */ public Json.Object get_manifest (string package, string version) throws DatabaseError { foreach (var single_db in db) { try { return single_db.get_manifest (package, version); } catch (DatabaseError e) { if (e is DatabaseError.BAD_MANIFEST) throw e; } } throw new DatabaseError.DOES_NOT_EXIST ("%s %s does not exist in any database", package, version); } /** * get_manifest_as_string: * @package: A package name. * @version: A version string. * * Returns: A JSON string containing the serialised manifest of this * version of this package. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifest_as_string (string package, string version) throws DatabaseError { var manifest = get_manifest (package, version); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (manifest); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /** * get_manifests: * @all_versions: If true, return manifests for all versions, not * just current ones. * * Returns: A #Json.Array containing manifests of all packages in * this database. The manifest may include additional dynamic keys * (starting with an underscore) corresponding to dynamic properties * of installed packages. * * Since: 0.4.18 */ public Json.Array get_manifests (bool all_versions = false) throws Error { var ret = new Json.Array (); foreach (var inst in get_packages (all_versions)) { Json.Object obj; try { obj = get_manifest (inst.package, inst.version); } catch (DatabaseError e) { warning ("%s", e.message); continue; } /* This should really be a boolean, but it was * mistakenly made an int when the "_removable" key * was first created. We may change this in future. */ obj.set_int_member ("_removable", inst.writeable ? 1 : 0); ret.add_object_element (obj); } return ret; } /** * get_manifests_as_string: * @all_versions: If true, return manifests for all versions, not * just current ones. * * Returns: A JSON string containing a serialised array of manifests * of all packages in this database. The manifest may include * additional dynamic keys (starting with an underscore) * corresponding to dynamic properties of installed packages. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifests_as_string (bool all_versions = false) throws Error { var manifests = get_manifests (all_versions); var node = new Json.Node (Json.NodeType.ARRAY); node.set_array (manifests); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } private void ensure_db () throws Error { if (db.size == 0) throw new DatabaseError.INVALID ("no database loaded"); } public void maybe_remove (string package, string version) throws Error { ensure_db(); db.last ().maybe_remove (package, version); } public void gc () throws Error { ensure_db(); db.last ().gc (); } public void ensure_ownership () throws Error { ensure_db(); db.last ().ensure_ownership (); } } } click-0.5.0/lib/click/deb822.vala000066400000000000000000000035321402441472600162760ustar00rootroot00000000000000/* Copyright (C) 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Simple deb822-like file parsing. */ namespace Click { private Regex? field_re = null; private Regex? blank_re = null; /** * parse_deb822_file: * @path: Path to a file. * * A very simple deb822-like file parser. * * Note that this only supports a single paragraph composed only of simple * (non-folded/multiline) fields, which is fortunately all we need in Click. * * Returns: A mapping of field names to values. */ private Gee.Map parse_deb822_file (string path) throws Error { if (field_re == null) field_re = new Regex ("^([^:[:space:]]+)[[:space:]]*:[[:space:]]" + "([^[:space:]].*?)[[:space:]]*$"); if (blank_re == null) blank_re = new Regex ("^[[:space:]]*$"); var ret = new Gee.HashMap (); var channel = new IOChannel.file (path, "r"); string line; while (channel.read_line (out line, null, null) == IOStatus.NORMAL && line != null) { MatchInfo match_info; if (blank_re.match (line)) break; if (field_re.match (line, 0, out match_info)) { var key = match_info.fetch (1); var value = match_info.fetch (2); if (key != null && value != null) ret[key.down ()] = value; } } return ret; } } click-0.5.0/lib/click/framework.vala000066400000000000000000000062511402441472600173060ustar00rootroot00000000000000/* Copyright (C) 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Click frameworks. */ namespace Click { public errordomain FrameworkError { /** * Requested framework does not exist. */ NO_SUCH_FRAMEWORK, /** * Missing hook field. */ MISSING_FIELD } public class Framework : Object { public string name { get; construct; } private Gee.Map fields; private Framework (string name) { Object (name: name); } /** * Framework.open: * @name: The name of the framework to open. * * Returns: (transfer full): A newly-allocated #Click.Framework. * * Since: 0.4.18 */ public static Framework open (string name) throws FrameworkError { var path = Path.build_filename (get_frameworks_dir (), @"$name.framework"); try { var framework = new Framework (name); framework.fields = parse_deb822_file (path); return framework; } catch (Error e) { throw new FrameworkError.NO_SUCH_FRAMEWORK ("No click framework '%s' installed", name); } } /** * has_framework: * @name: A framework name. * * Returns: True if a framework by this name exists, otherwise false. * * Since: 0.4.18 */ public static bool has_framework (string name) { var path = Path.build_filename (get_frameworks_dir (), @"$name.framework"); return exists (path); } /** * get_frameworks: * * Returns: (element-type ClickFramework) (transfer full): A #List * of all #Click.Framework instances installed on the system. * * Since: 0.4.18 */ public static List get_frameworks () { var ret = new List (); Click.Dir dir; try { dir = Click.Dir.open (get_frameworks_dir ()); } catch (FileError e) { return ret; } foreach (var entry in dir) { if (! entry.has_suffix (".framework")) continue; try { ret.prepend (open (entry[0:-10])); } catch (Error e) { continue; } } ret.reverse (); return ret; } /** * get_fields: * * Returns: A list of field names defined by this framework. * * Since: 0.4.18 */ public List get_fields () { var ret = new List (); foreach (var key in fields.keys) ret.prepend (key); ret.reverse (); return ret; } public string get_field (string key) throws FrameworkError { string value = fields[key.down ()]; if (value == null) throw new FrameworkError.MISSING_FIELD ("Framework '%s' has no field named '%s'", name, key); return value; } public string? get_base_name () { return fields["base-name"]; } public string? get_base_version () { return fields["base-version"]; } } } click-0.5.0/lib/click/hooks.vala000066400000000000000000000777531402441472600164530ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Click package hooks. * * See doc/hooks.rst for the draft specification. */ namespace Click { public errordomain HooksError { /** * Requested hook does not exist. */ NO_SUCH_HOOK, /** * Missing hook field. */ MISSING_FIELD, /** * Invalid application name. */ BAD_APP_NAME, /** * Requested user does not exist. */ NO_SUCH_USER, /** * Failure to drop privileges. */ DROP_PRIVS, /** * Not yet implemented. */ NYI, /** * Hook command failed. */ COMMAND_FAILED, /** * Some hooks were not run successfully. */ INCOMPLETE } /* vala implementation of click.framework.validate_framework() * * Note that the required_frameworks string has the form * framework1, framework2, ... * See doc/file-format.rst for details. */ private bool validate_framework (string required_frameworks) { // valid framework names, cf. debian policy §5.6.1 Regex valid_framework_re; try { valid_framework_re = new Regex ("^[a-z][a-z0-9.+-]+"); } catch (RegexError e) { error ("Could not compile regex /^[a-z][a-z0-9.+-]+/: %s", e.message); } var base_version = ""; foreach (var framework_name in required_frameworks.split (",")) { framework_name = framework_name.strip (); if (!valid_framework_re.match (framework_name)) return false; Framework framework; // now check the base-version try { framework = Framework.open (framework_name); } catch (FrameworkError e) { return false; } if (base_version == "") base_version = framework.get_base_version (); if (base_version != framework.get_base_version ()) return false; } return true; } private bool validate_framework_for_package (DB db, string package, string? version) { var manifest = read_manifest (db, package, version); if (!manifest.has_member ("framework")) return true; var required_frameworks = manifest.get_string_member ("framework"); return validate_framework (required_frameworks); } private Json.Object read_manifest (DB db, string package, string? version) { if (version == null) return new Json.Object (); var parser = new Json.Parser (); try { var manifest_path = Path.build_filename (db.get_path (package, version), ".click", "info", @"$package.manifest"); parser.load_from_file (manifest_path); var manifest = parser.get_root ().get_object (); return manifest.ref (); } catch (Error e) { return new Json.Object (); } } private Json.Object read_manifest_hooks (DB db, string package, string? version) { var manifest = read_manifest (db, package, version); if (! manifest.has_member ("hooks")) return new Json.Object (); var hooks = manifest.get_object_member ("hooks"); return hooks.ref (); } private class PreviousEntry : Object, Gee.Hashable { public string path { get; construct; } public string package { get; construct; } public string version { get; construct; } public string app_name { get; construct; } public PreviousEntry (string path, string package, string version, string app_name) { Object (path: path, package: package, version: version, app_name: app_name); } public uint hash () { return path.hash () ^ package.hash () ^ version.hash () ^ app_name.hash (); } public bool equal_to (PreviousEntry obj) { return path == obj.path && package == obj.package && version == obj.version && app_name == obj.app_name; } } private class UnpackedPackage : Object, Gee.Hashable { public string package { get; construct; } public string version { get; construct; } public string? user_name { get; construct; } public UnpackedPackage (string package, string version, string? user_name = null) { Object (package: package, version: version, user_name: user_name); } public uint hash () { return package.hash () ^ version.hash () ^ (user_name != null ? user_name.hash () : 0); } public bool equal_to (UnpackedPackage obj) { return package == obj.package && version == obj.version && user_name == obj.user_name; } } private class RelevantApp : Object, Gee.Hashable { public string package { get; construct; } public string version { get; construct; } public string app_name { get; construct; } public string? user_name { get; construct; } public string relative_path { get; construct; } public RelevantApp (string package, string version, string app_name, string? user_name, string relative_path) { Object (package: package, version: version, app_name: app_name, user_name: user_name, relative_path: relative_path); } public uint hash () { return package.hash () ^ version.hash () ^ app_name.hash () ^ (user_name != null ? user_name.hash () : 0) ^ relative_path.hash (); } public bool equal_to (RelevantApp obj) { return package == obj.package && version == obj.version && app_name == obj.app_name && user_name == obj.user_name && relative_path == obj.relative_path; } } private class AppHook : Object, Gee.Hashable, Gee.Comparable { public string app_name { get; construct; } public string hook_name { get; construct; } public AppHook (string app_name, string hook_name) { Object (app_name: app_name, hook_name: hook_name); } public uint hash () { return app_name.hash () ^ hook_name.hash (); } public bool equal_to (AppHook obj) { return app_name == obj.app_name && hook_name == obj.hook_name; } public int compare_to (AppHook obj) { var ret = strcmp (app_name, obj.app_name); if (ret != 0) return ret; return strcmp (hook_name, obj.hook_name); } } private class ParsedPattern : Object { public bool is_expansion { get; construct; } public string text { get; construct; } public ParsedPattern (bool is_expansion, string text) { Object (is_expansion: is_expansion, text: text); } } private Regex? expansion_re = null; /** * pattern_parse: * @format_string: A format string. * * Parse @format_string into segments. * * Returns: A list of #ParsedPattern segments. */ private Gee.List pattern_parse (string format_string) { const string EXPANSION = "\\$(?:\\$|{(.*?)})"; var ret = new Gee.ArrayList (); MatchInfo match_info; var last_end = 0; if (expansion_re == null) { try { expansion_re = new Regex (EXPANSION); } catch (RegexError e) { error ("Could not compile regex /%s/: %s", EXPANSION, e.message); } } expansion_re.match (format_string, 0, out match_info); while (match_info.matches ()) { int start, end; var fetched = match_info.fetch_pos (0, out start, out end); assert (fetched); string? key = null; if (start + 2 == end && format_string[start] == '$' && format_string[start + 1] == '$') ++start; else key = match_info.fetch (1); if (last_end < start) { var segment = format_string.substring (last_end, start - last_end); ret.add (new ParsedPattern (false, segment)); } if (key != null) ret.add (new ParsedPattern (true, key)); last_end = end; try { match_info.next (); } catch (RegexError e) { break; } } if (last_end < format_string.length) ret.add (new ParsedPattern (false, format_string.substring (last_end))); return ret; } /** * pattern_format: * @format_string: A format string. * @args: A #GLib.Variant of type "a{sms}", binding keys to values. * * Apply simple $-expansions to a string. * * `${key}` is replaced by the value of the `key` argument; `$$` is replaced * by `$`. Any `$` character not followed by `{...}` is preserved intact. * * Returns: The expanded string. */ public string pattern_format (string format_string, Variant args) { string[] pieces = {}; foreach (var segment in pattern_parse (format_string)) { if (segment.is_expansion) { unowned string value; if (args.lookup (segment.text, "m&s", out value)) pieces += value; } else pieces += segment.text; } return string.joinv ("", pieces); } /** * click_pattern_possible_expansion: * @s: A string. * @format_string: A format string. * @args: A #GLib.Variant of type "a{sms}", binding keys to values. * * Check if @s is a possible $-expansion of @format_string. * * Entries in @args have the effect of binding some keys to fixed values; * unspecified keys may take any value, and will bind greedily to the * longest possible string. * * Returns: If @s is a possible expansion, then this function returns a * (possibly empty) dictionary #GLib.Variant mapping all the unspecified * keys to their bound values. Otherwise, it returns null. */ public Variant? pattern_possible_expansion (string s, string format_string, Variant args) { string[] regex_pieces = {}; string[] group_names = {}; foreach (var segment in pattern_parse (format_string)) { if (segment.is_expansion) { unowned string value; if (args.lookup (segment.text, "m&s", out value)) regex_pieces += Regex.escape_string (value); else { regex_pieces += "(.*)"; group_names += segment.text; } } else regex_pieces += Regex.escape_string (segment.text); } var joined = string.joinv ("", regex_pieces); Regex compiled; try { compiled = new Regex ("^" + joined + "$"); } catch (RegexError e) { return null; } MatchInfo match_info; var builder = new VariantBuilder (new VariantType ("a{ss}")); if (compiled.match (s, 0, out match_info)) { for (int group_i = 0; group_i < group_names.length; ++group_i) { var match = match_info.fetch (group_i + 1); assert (match != null); builder.add ("{ss}", group_names[group_i], match); } return builder.end (); } else return null; } public class Hook : Object { public DB db { private get; construct; } public string name { internal get; construct; } private Gee.Map fields; private Hook (DB db, string name) { Object (db: db, name: name); } /** * Hook.open: * @db: A #Click.DB. * @name: The name of the hook to open. * * Returns: (transfer full): A newly-allocated #Click.Hook. */ public static Hook open (DB db, string name) throws HooksError { var hook_path = Path.build_filename (get_hooks_dir (), @"$name.hook"); try { var hook = new Hook (db, name); hook.fields = parse_deb822_file (hook_path); return hook; } catch (Error e) { throw new HooksError.NO_SUCH_HOOK ("No click hook '%s' installed", name); } } /** * open_all: * @db: A #Click.DB. * @hook_name: (allow-none): A string to match against Hook-Name * fields, or null. * * Returns: (element-type ClickHook) (transfer full): A #List of * #Click.Hook instances whose Hook-Name fields equal the value of * @hook_name. */ public static List open_all (DB db, string? hook_name = null) throws FileError { var ret = new List (); var dir = get_hooks_dir (); foreach (var name in Click.Dir.open (dir)) { if (! name.has_suffix (".hook")) continue; var path = Path.build_filename (dir, name); try { var hook = new Hook (db, name[0:-5]); hook.fields = parse_deb822_file (path); if (hook_name == null || hook.get_hook_name () == hook_name) ret.prepend (hook); } catch (Error e) { continue; } } ret.reverse (); return ret; } /** * get_fields: * * Returns: A list of field names defined by this hook. */ public List get_fields () { var ret = new List (); foreach (var key in fields.keys) ret.prepend (key); ret.reverse (); return ret; } public string get_field (string key) throws HooksError { string value = fields[key.down ()]; if (value == null) throw new HooksError.MISSING_FIELD ("Hook '%s' has no field named '%s'", name, key); return value; } /** * is_user_level: * * True if this hook is a user-level hook, otherwise false. */ public bool is_user_level { get { return fields["user-level"] == "yes"; } } /** * is_single_version: * * True if this hook is a single-version hook, otherwise false. */ public bool is_single_version { get { return is_user_level || fields["single-version"] == "yes"; } } /** * get_hook_name: * * Returns: This hook's Hook-Name field, or the base of its file * name with the ".hook" extension removed if that field is missing. */ public string get_hook_name () { if (fields.has_key ("hook-name")) return fields["hook-name"]; else return name; } /** * get_short_app_id: * @package: A package name. * @app_name: An application name. * * Returns: The short application ID based on @package and * @app_name. */ public string get_short_app_id (string package, string app_name) throws HooksError { /* TODO: Perhaps this check belongs further up the stack * somewhere? */ if ("_" in app_name || "/" in app_name) throw new HooksError.BAD_APP_NAME ("Application name '%s' may not contain _ " + "or / characters", app_name); return @"$(package)_$(app_name)"; } /** * get_app_id: * @package: A package name. * @version: A version string. * @app_name: An application name. * * Returns: The application ID based on @package, @version, and * @app_name. */ public string get_app_id (string package, string version, string app_name) throws HooksError { var short_app_id = get_short_app_id (package, app_name); return @"$(short_app_id)_$(version)"; } /** * get_pattern: * @package: A package name. * @version: A version string. * @app_name: An application name. * @user_name: (allow-none): A user name, or null. */ public string get_pattern (string package, string version, string app_name, string? user_name = null) throws HooksError { var builder = new VariantBuilder (new VariantType ("a{sms}")); var app_id = get_app_id (package, version, app_name); var pattern = get_field ("pattern"); var user_home = get_user_home (user_name); builder.add ("{sms}", "id", app_id); builder.add ("{sms}", "user", user_name); builder.add ("{sms}", "home", user_home); if (is_single_version) { var short_app_id = get_short_app_id (package, app_name); builder.add ("{sms}", "short-id", short_app_id); } var ret = pattern_format (pattern, builder.end ()); var len = ret.length; while (len > 0) { if (ret[len - 1] == Path.DIR_SEPARATOR) --len; else break; } if (len == ret.length) return ret; else return ret.substring (0, len); } private void priv_drop_failure (string name) throws HooksError { throw new HooksError.DROP_PRIVS ("Cannot drop privileges (%s): %s", name, strerror (errno)); } /* This function is not async-signal-safe, but runs between fork() and * execve(). As such, it is not safe to run hooks from a multi-threaded * process. Do not use the GLib main loop with this! */ private void drop_privileges_inner (string user_name) throws HooksError { if (Posix.geteuid () != 0) return; errno = 0; unowned Posix.Passwd? pw = Posix.getpwnam (user_name); if (pw == null) throw new HooksError.NO_SUCH_USER ("Cannot get password file entry for user " + "'%s': %s", user_name, strerror (errno)); Posix.gid_t[] supp = {}; Posix.setgrent (); unowned PosixExtra.Group? gr; while ((gr = PosixExtra.getgrent ()) != null) { foreach (unowned string member in gr.gr_mem) { if (member == user_name) { supp += gr.gr_gid; break; } } } Posix.endgrent (); if (PosixExtra.setgroups (supp.length, supp) < 0) priv_drop_failure ("setgroups"); /* Portability note: this assumes that we have * [gs]etres[gu]id, which is true on Linux but not * necessarily elsewhere. If you need to support something * else, there are reasonably standard alternatives * involving other similar calls; see e.g. * gnulib/lib/idpriv-drop.c. */ if (PosixExtra.setresgid (pw.pw_gid, pw.pw_gid, pw.pw_gid) < 0) priv_drop_failure ("setresgid"); if (PosixExtra.setresuid (pw.pw_uid, pw.pw_uid, pw.pw_uid) < 0) priv_drop_failure ("setresuid"); { Posix.uid_t ruid, euid, suid; Posix.gid_t rgid, egid, sgid; assert (PosixExtra.getresuid (out ruid, out euid, out suid) == 0 && ruid == pw.pw_uid && euid == pw.pw_uid && suid == pw.pw_uid); assert (PosixExtra.getresgid (out rgid, out egid, out sgid) == 0 && rgid == pw.pw_gid && egid == pw.pw_gid && sgid == pw.pw_gid); } Environment.set_variable ("HOME", pw.pw_dir, true); Posix.umask (get_umask () | Posix.S_IWOTH); } private void drop_privileges (string user_name) { try { drop_privileges_inner (user_name); } catch (HooksError e) { error ("%s", e.message); } } /** * get_run_commands_user: * @user_name: (allow-none): A user name, or null. * * Returns: The user name under which this hook will be run. */ public string get_run_commands_user (string? user_name = null) throws HooksError { if (is_user_level) return user_name; return get_field ("user"); } /** * run_commands: * @user_name: (allow-none): A user name, or null. * * Run any commands specified by the hook to keep itself up to date. */ public void run_commands (string? user_name = null) throws Error { if (fields.has_key ("exec")) { string[] argv = {"/bin/sh", "-c", fields["exec"]}; var target_user_name = get_run_commands_user (user_name); SpawnChildSetupFunc drop = () => drop_privileges (target_user_name); int exit_status; Process.spawn_sync (null, argv, null, SpawnFlags.SEARCH_PATH, drop, null, null, out exit_status); try { Process.check_exit_status (exit_status); } catch (Error e) { throw new HooksError.COMMAND_FAILED ("Hook command '%s' failed: %s", fields["exec"], e.message); } } if (fields["trigger"] == "yes") throw new HooksError.NYI ("'Trigger: yes' not yet implemented"); } private List get_previous_entries (string? user_name = null) throws Error { var ret = new List (); var link_dir_path = Path.get_dirname (get_pattern ("", "", "", user_name)); /* TODO: This only works if the application ID only appears, at * most, in the last component of the pattern path. */ foreach (var entry in Click.Dir.open (link_dir_path)) { var path = Path.build_filename (link_dir_path, entry); var exp_builder = new VariantBuilder (new VariantType ("a{sms}")); exp_builder.add ("{sms}", "user", user_name); exp_builder.add ("{sms}", "home", get_user_home (user_name)); var exp = pattern_possible_expansion (path, fields["pattern"], exp_builder.end ()); unowned string? id = null; if (exp != null) exp.lookup ("id", "&s", out id); if (id == null) continue; var tokens = id.split ("_", 3); if (tokens.length < 3) continue; /* tokens == { package, app_name, version } */ ret.prepend (new PreviousEntry (path, tokens[0], tokens[2], tokens[1])); } ret.reverse (); return ret; } /** * install_link: * @package: A package name. * @version: A version string. * @app_name: An application name. * @relative_path: A relative path within the unpacked package. * @user_name: (allow-none): A user name, or null. * @user_db: (allow-none): A #Click.User, or null. * * Install a hook symlink. * * This should be called with dropped privileges if necessary. */ private void install_link (string package, string version, string app_name, string relative_path, string? user_name = null, User? user_db = null) throws Error { string path; if (is_user_level) path = user_db.get_path (package); else path = db.get_path (package, version); var target = Path.build_filename (path, relative_path); var link = get_pattern (package, version, app_name, user_name); if (is_symlink (link) && FileUtils.read_link (link) == target) return; ensuredir (Path.get_dirname (link)); symlink_force (target, link); } /** * install_package: * @package: A package name. * @version: A version string. * @app_name: An application name. * @relative_path: A relative path within the unpacked package. * @user_name: (allow-none): A user name, or null. * * Run this hook in response to @package being installed. */ public void install_package (string package, string version, string app_name, string relative_path, string? user_name = null) throws Error { if (! is_user_level) assert (user_name == null); /* Remove previous versions if necessary. */ if (is_single_version) { var entries = get_previous_entries (user_name); foreach (var prev in entries) { if (prev.package == package && prev.app_name == app_name && prev.version != version) unlink_force (prev.path); } } if (is_user_level) { var user_db = new User.for_user (db, user_name); user_db.drop_privileges (); try { install_link (package, version, app_name, relative_path, user_name, user_db); } finally { user_db.regain_privileges (); } } else install_link (package, version, app_name, relative_path); run_commands (user_name); } /** * remove_package: * @package: A package name. * @version: A version string. * @app_name: An application name. * @user_name: (allow-none): A user name, or null. * * Run this hook in response to @package being removed. */ public void remove_package (string package, string version, string app_name, string? user_name = null) throws Error { unlink_force (get_pattern (package, version, app_name, user_name)); run_commands (user_name); } private Gee.ArrayList get_all_packages_for_user (string user_name, User user_db) throws Error { var ret = new Gee.ArrayList (); foreach (var package in user_db.get_package_names ()) ret.add (new UnpackedPackage (package, user_db.get_version (package), user_name)); return ret; } /** * get_all_packages: * @user_name: (allow-none): A user name, or null. * * Return a list of all unpacked packages. * * If running a user-level hook, this returns (package, version, * user) for the current version of each package registered for each * user, or only for a single user if user is not null. * * If running a system-level hook, this returns (package, version, * null) for each version of each unpacked package. * * Returns: A list of all unpacked packages. */ private List get_all_packages (string? user_name = null) throws Error { var ret = new Gee.ArrayList (); if (is_user_level) { if (user_name != null) { var user_db = new User.for_user (db, user_name); ret.add_all (get_all_packages_for_user (user_name, user_db)); } else { var users_db = new Users (db); var user_names = users_db.get_user_names (); foreach (var one_user_name in user_names) { if (one_user_name.has_prefix ("@")) continue; var one_user_db = users_db.get_user (one_user_name); ret.add_all (get_all_packages_for_user (one_user_name, one_user_db)); } } } else { foreach (var inst in db.get_packages (true)) ret.add (new UnpackedPackage (inst.package, inst.version)); } /* Flatten into a List to avoid introspection problems in * case this method is ever exposed. */ var ret_list = new List (); foreach (var element in ret) ret_list.prepend (element); ret_list.reverse (); return ret_list; } /** * get_relevant_apps: * @user_name: (allow-none): A user name, or null. * * Returns: A list of all applications relevant for this hook. */ private List get_relevant_apps (string? user_name = null) throws Error { var ret = new List (); var hook_name = get_hook_name (); foreach (var unpacked in get_all_packages (user_name)) { // if the app is not using a valid framework (anymore) // we don't consider it relevant (anymore) if (!validate_framework_for_package (db, unpacked.package, unpacked.version)) continue; var manifest_hooks = read_manifest_hooks (db, unpacked.package, unpacked.version); foreach (var app_name in manifest_hooks.get_members ()) { var hooks = manifest_hooks.get_object_member (app_name); if (hooks.has_member (hook_name)) { var relative_path = hooks.get_string_member (hook_name); ret.prepend (new RelevantApp (unpacked.package, unpacked.version, app_name, unpacked.user_name, relative_path)); } } } ret.reverse (); return ret; } /** * install: * @user_name: (allow-none): A user name, or null. * * Install files associated with this hook for any packages that * attach to it. */ public void install (string? user_name = null) throws Error { foreach (var app in get_relevant_apps (user_name)) install_package (app.package, app.version, app.app_name, app.relative_path, app.user_name); } /** * remove: * @user_name: (allow-none): A user name, or null. * * Remove files associated with this hook for any packages that * attach to it. */ public void remove (string? user_name = null) throws Error { foreach (var app in get_relevant_apps (user_name)) remove_package (app.package, app.version, app.app_name, app.user_name); } /** * sync: * @user_name: (allow-none): A user name, or null. * * Run a hook for all installed packages (system-level if @user_name * is null, otherwise user-level). * * This is useful to catch up with preinstalled packages. */ public void sync (string? user_name = null) throws Error { if (! is_user_level) assert (user_name == null); var seen = new Gee.HashSet (); foreach (var app in get_relevant_apps (user_name)) { unowned string package = app.package; unowned string version = app.version; unowned string app_name = app.app_name; seen.add (@"$(package)_$(app_name)_$(version)"); if (is_user_level) { var user_db = new User.for_user (db, user_name); var overlay_path = Path.build_filename (user_db.get_overlay_db (), package); user_db.drop_privileges (); try { if (exists (overlay_path)) user_db.raw_set_version (package, version); install_link (package, version, app_name, app.relative_path, app.user_name, user_db); } finally { user_db.regain_privileges (); } } else install_link (package, version, app_name, app.relative_path); } foreach (var prev in get_previous_entries (user_name)) { unowned string package = prev.package; unowned string version = prev.version; unowned string app_name = prev.app_name; if (! (@"$(package)_$(app_name)_$(version)" in seen)) unlink_force (prev.path); } run_commands (user_name); } } private string? get_user_home (string? user_name) { if (user_name == null) return null; /* TODO: caching */ unowned Posix.Passwd? pw = Posix.getpwnam (user_name); if (pw == null) return null; return pw.pw_dir; } private Gee.TreeSet get_app_hooks (Json.Object manifest) { var items = new Gee.TreeSet (); /* sorted */ foreach (var app_name in manifest.get_members ()) { var hooks = manifest.get_object_member (app_name); foreach (var hook_name in hooks.get_members ()) items.add (new AppHook (app_name, hook_name)); } return items; } /** * package_install_hooks: * @db: A #Click.DB. * @package: A package name. * @old_version: (allow-none): The old version of the package, or null. * @new_version: The new version of the package. * @user_name: (allow-none): A user name, or null. * * Run hooks following install of a Click package. * * If @user_name is null, only run system-level hooks. If @user_name is not * null, only run user-level hooks for that user. */ public void package_install_hooks (DB db, string package, string? old_version, string new_version, string? user_name = null) throws Error { var old_manifest = read_manifest_hooks (db, package, old_version); var new_manifest = read_manifest_hooks (db, package, new_version); /* Remove any targets for single-version hooks that were in the old * manifest but not the new one. */ var old_app_hooks = get_app_hooks (old_manifest); var new_app_hooks = get_app_hooks (new_manifest); foreach (var app_hook in new_app_hooks) old_app_hooks.remove (app_hook); foreach (var app_hook in old_app_hooks) { foreach (var hook in Hook.open_all (db, app_hook.hook_name)) { if (hook.is_user_level != (user_name != null)) continue; if (! hook.is_single_version) continue; hook.remove_package (package, old_version, app_hook.app_name, user_name); } } var new_app_names = new_manifest.get_members (); new_app_names.sort (strcmp); foreach (var app_name in new_app_names) { var app_hooks = new_manifest.get_object_member (app_name); var hook_names = app_hooks.get_members (); hook_names.sort (strcmp); foreach (var hook_name in hook_names) { var relative_path = app_hooks.get_string_member (hook_name); foreach (var hook in Hook.open_all (db, hook_name)) { if (hook.is_user_level != (user_name != null)) continue; hook.install_package (package, new_version, app_name, relative_path, user_name); } } } } /** * package_remove_hooks: * @db: A #Click.DB. * @package: A package name. * @old_version: The old version of the package. * @user_name: (allow-none): A user name, or null. * * Run hooks following removal of a Click package. * * If @user_name is null, only run system-level hooks. If @user_name is not * null, only run user-level hooks for that user. */ public void package_remove_hooks (DB db, string package, string old_version, string? user_name = null) throws Error { var old_manifest = read_manifest_hooks (db, package, old_version); foreach (var app_hook in get_app_hooks (old_manifest)) { foreach (var hook in Hook.open_all (db, app_hook.hook_name)) { if (hook.is_user_level != (user_name != null)) continue; hook.remove_package (package, old_version, app_hook.app_name, user_name); } } } /** * run_system_hooks: * @db: A #Click.DB. * * Run system-level hooks for all installed packages. * * This is useful when starting up from images with preinstalled packages * which may not have had their system-level hooks run properly when * building the image. It is suitable for running at system startup. */ public void run_system_hooks (DB db) throws Error { db.gc (); db.ensure_ownership (); string[] failed = {}; foreach (var hook in Hook.open_all (db)) { if (! hook.is_user_level) { try { hook.sync (); } catch (HooksError e) { warning ("System-level hook %s failed: %s", hook.name, e.message); failed += hook.name; } } } if (failed.length != 0) throw new HooksError.INCOMPLETE ("Some system-level hooks failed: %s", string.joinv (", ", failed)); } /** * run_user_hooks: * @db: A #Click.DB. * @user_name: (allow-none): A user name, or null to run hooks for the * current user. * * Run user-level hooks for all installed packages. * * This is useful to catch up with packages that may have been preinstalled * and registered for all users. It is suitable for running at session * startup. */ public void run_user_hooks (DB db, string? user_name = null) throws Error { if (user_name == null) user_name = Environment.get_user_name (); string[] failed = {}; foreach (var hook in Hook.open_all (db)) { if (hook.is_user_level) { try { hook.sync (user_name); } catch (HooksError e) { warning ("User-level hook %s failed: %s", hook.name, e.message); failed += hook.name; } } } if (failed.length != 0) throw new HooksError.INCOMPLETE ("Some user-level hooks failed: %s", string.joinv (", ", failed)); } } click-0.5.0/lib/click/osextras.vala000066400000000000000000000130571402441472600171630ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Extra OS-level utility functions. */ namespace Click { /** * find_on_path: * @command: A command name. * * Returns: True if the command is on the executable search path, otherwise * false. */ public bool find_on_path (string command) { unowned string? path = Environment.get_variable ("PATH"); if (path == null) return false; var elements = path.split(":"); foreach (var element in elements) { if (element == "") continue; var filename = Path.build_filename (element, command); if (FileUtils.test (filename, FileTest.IS_REGULAR) && FileUtils.test (filename, FileTest.IS_EXECUTABLE)) return true; } return false; } /** * ensuredir: * @directory: A path. * * If @directory does not already exist, create it and its parents as * needed. */ public void ensuredir (string directory) throws FileError { if (is_dir (directory)) return; if (DirUtils.create_with_parents (directory, 0777) < 0) { var code = FileUtils.error_from_errno (errno); var quark = Quark.from_string ("g-file-error-quark"); var err = new Error (quark, code, "ensuredir %s failed: %s", directory, strerror (errno)); throw (FileError) err; } } /** * rmtree: * @file: A #File to delete * * Errors when the directory can not be removed */ public bool rmtree (File file, Cancellable? cancellable = null) throws Error { var enumerator = file.enumerate_children(FileAttribute.STANDARD_NAME, FileQueryInfoFlags.NOFOLLOW_SYMLINKS, cancellable); while (enumerator != null) { File child; if (!enumerator.iterate(null, out child, cancellable)) { return false; } if (child == null) { break; } try { if (FileUtils.test (child.get_path(), FileTest.IS_DIR)) { rmtree (child, cancellable); } else { child.delete (cancellable); } } catch (Error e) { warning ("Error removing '%s': %s", child.get_path (), e.message); continue; } } return file.delete(cancellable); } /** * unlink_force: * @path: A path to unlink. * * Unlink path, without worrying about whether it exists. Errors other than * %ENOENT will set the provided error location. */ public void unlink_force (string path) throws FileError { if (FileUtils.unlink (path) < 0 && errno != Posix.ENOENT) { var code = FileUtils.error_from_errno (errno); var quark = Quark.from_string ("g-file-error-quark"); var err = new Error (quark, code, "unlink %s failed: %s", path, strerror (errno)); throw (FileError) err; } } /** * symlink_force: * @target: The intended target of the symbolic link. * @link_name: A path where the symbolic link should be created. * * Create a symlink link_name -> target, even if link_name exists. */ public void symlink_force (string target, string link_name) throws FileError { unlink_force (link_name); /* This produces a harmless warning when compiling C code generated * by valac 0.22.1: * https://bugzilla.gnome.org/show_bug.cgi?id=725151 */ if (FileUtils.symlink (target, link_name) < 0) { var code = FileUtils.error_from_errno (errno); var quark = Quark.from_string ("g-file-error-quark"); var err = new Error (quark, code, "symlink %s -> %s failed: %s", link_name, target, strerror (errno)); throw (FileError) err; } } /** * click_get_umask: * * Returns: The current umask. */ public int get_umask () { var mask = Posix.umask (0); Posix.umask (mask); return (int) mask; } public class Dir : Object { private SList entries; private unowned SList cur; private Dir () { } /** * open: * @path: The path to the directory to open. * @flags: For future use; currently must be set to 0. * * Like GLib.Dir.open(), but ignores %ENOENT. */ public static Dir? open (string path, uint _flags = 0) throws FileError { Dir dir = new Dir (); dir.entries = new SList (); GLib.Dir real_dir; try { real_dir = GLib.Dir.open (path, _flags); string? name; while ((name = real_dir.read_name ()) != null) dir.entries.prepend (name); dir.entries.sort (strcmp); } catch (FileError e) { if (! (e is FileError.NOENT)) throw e; } dir.cur = dir.entries; return dir; } /** * read_name: * * Like GLib.Dir.read_name(), but returns entries in sorted order. */ public unowned string? read_name () { if (cur == null) return null; unowned string name = cur.data; cur = cur.next; return name; } internal class Iterator : Object { private Dir dir; public Iterator (Dir dir) { this.dir = dir; } public unowned string? next_value () { return dir.read_name (); } } internal Iterator iterator () { return new Iterator (this); } } private bool exists (string path) { return FileUtils.test (path, FileTest.EXISTS); } private bool is_symlink (string path) { return FileUtils.test (path, FileTest.IS_SYMLINK); } private bool is_dir (string path) { return FileUtils.test (path, FileTest.IS_DIR); } } click-0.5.0/lib/click/paths.vala.in000066400000000000000000000024731402441472600170370ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Click paths. */ namespace Click { internal static const string hooks_dir = "@pkgdatadir@/hooks"; internal static const string db_dir = "@sysconfdir@/click/databases"; internal static const string frameworks_dir = "@pkgdatadir@/frameworks"; /** * get_hooks_dir: * * Returns: The Click hooks directory. */ public string get_hooks_dir () { return hooks_dir; } /** * get_db_dir: * * Returns: The Click database configuration directory. */ public string get_db_dir () { return db_dir; } /** * get_frameworks_dir: * * Returns: The Click frameworks directory. */ public string get_frameworks_dir () { return frameworks_dir; } } click-0.5.0/lib/click/posix-extra.vapi000066400000000000000000000037611402441472600176130ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Things that should be in posix.vapi, but aren't. */ [CCode (cprefix = "", lower_case_cprefix = "")] namespace PosixExtra { /* https://bugzilla.gnome.org/show_bug.cgi?id=725149 */ [Compact] [CCode (cname = "struct group", cheader_filename = "grp.h")] public class Group { public string gr_name; public string gr_passwd; public Posix.gid_t gr_gid; [CCode (array_length = false, array_null_terminated = true)] public string[] gr_mem; } [CCode (cheader_filename = "grp.h")] public unowned Group? getgrent (); [CCode (cheader_filename = "unistd.h")] public int getresgid (out Posix.gid_t rgid, out Posix.gid_t egid, out Posix.gid_t sgid); [CCode (cheader_filename = "unistd.h")] public int getresuid (out Posix.uid_t ruid, out Posix.uid_t euid, out Posix.uid_t suid); [CCode (cheader_filename = "unistd.h")] public int setegid (Posix.gid_t egid); [CCode (cheader_filename = "unistd.h")] public int seteuid (Posix.uid_t euid); [CCode (cheader_filename = "sys/types.h,grp.h,unistd.h")] public int setgroups (size_t size, [CCode (array_length = false)] Posix.gid_t[] list); [CCode (cheader_filename = "unistd.h")] public int setresgid (Posix.gid_t rgid, Posix.gid_t egid, Posix.gid_t sgid); [CCode (cheader_filename = "unistd.h")] public int setresuid (Posix.uid_t ruid, Posix.uid_t euid, Posix.uid_t suid); } click-0.5.0/lib/click/query.vala000066400000000000000000000030021402441472600164450ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Query information about installed Click packages. */ namespace Click { public errordomain QueryError { /** * A path could not be canonicalised. */ PATH, /** * No package directory was found. */ NO_PACKAGE_DIR } public string find_package_directory (string path) throws QueryError { /* We require realpath (path, NULL) to be available. */ var dir = Posix.realpath (path); if (dir == null) throw new QueryError.PATH ("Failed to canonicalize %s: %s", path, strerror (errno)); do { var info_dir = Path.build_filename (dir, ".click", "info"); if (is_dir (info_dir)) return dir; if (dir == ".") break; var new_dir = Path.get_dirname (dir); if (new_dir == dir) break; dir = new_dir; } while (dir != null); throw new QueryError.NO_PACKAGE_DIR ("No package directory found for %s", path); } } click-0.5.0/lib/click/user.vala000066400000000000000000000567221402441472600162770ustar00rootroot00000000000000/* Copyright (C) 2013, 2014 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Registry of user-installed Click packages. * * Click packages are installed into per-package/version directories, so it * is quite feasible for more than one version of a given package to be * installed at once, allowing per-user installations; for instance, one * user of a tablet may be uncomfortable with granting some new permission * to an app, but another may be just fine with it. To make this useful, we * also need a registry of which users have which versions of each package * installed. * * We might have chosen to use a proper database. However, a major goal of * Click packages is extreme resilience; we must never get into a situation * where some previous error in package installation or removal makes it * hard for the user to install or remove other packages. Furthermore, the * simpler application execution can be the better. So, instead, we use * just about the simplest "database" format imaginable: a directory of * symlinks per user. */ namespace Click { struct LogindUser { uint32 uid; string name; string ObjectPath; } /* the logind dbus interface */ [DBus (name = "org.freedesktop.login1.Manager")] interface LogindManager : Object { public abstract LogindUser[] ListUsers () throws IOError; } /* Pseudo-usernames selected to be invalid as a real username, and alluding * to group syntaxes used in other systems. */ private const string ALL_USERS = "@all"; private const string GC_IN_USE_USER = "@gcinuse"; /* Pseudo-versions. In this case the @ doesn't allude to group syntaxes, * but since @ is conveniently invalid in version numbers we stick to the * same prefix used for pseudo-usernames. */ private const string HIDDEN_VERSION = "@hidden"; public errordomain UserError { /** * Failure to get password file entry. */ GETPWNAM, /** * Failure to create database directory. */ CREATE_DB, /** * Failure to set ownership of database directory. */ CHOWN_DB, /** * Requested user does not exist. */ NO_SUCH_USER, /** * Failure to drop privileges. */ DROP_PRIVS, /** * Failure to regain privileges. */ REGAIN_PRIVS, /** * Requested package is hidden. */ HIDDEN_PACKAGE, /** * Requested package does not exist. */ NO_SUCH_PACKAGE, /** * Failure to rename file. */ RENAME } private string db_top (string root) { /* This is deliberately outside any user's home directory so that it * can safely be iterated etc. as root. */ return Path.build_filename (root, ".click", "users"); } private string db_for_user (string root, string user) { return Path.build_filename (db_top (root), user); } private void try_create (string path) throws UserError { if (DirUtils.create (path, 0777) < 0) throw new UserError.CREATE_DB ("Cannot create database directory %s: %s", path, strerror (errno)); } private class CachedPasswd : Object { public Posix.uid_t uid; public Posix.gid_t gid; public CachedPasswd (Posix.uid_t uid, Posix.gid_t gid) { this.uid = uid; this.gid = gid; } } private void try_chown (string path, CachedPasswd pw) throws UserError { if (Posix.chown (path, pw.uid, pw.gid) < 0) throw new UserError.CHOWN_DB ("Cannot set ownership of database directory %s: %s", path, strerror (errno)); } public class Users : Object { public DB db { private get; construct; } private CachedPasswd? click_pw; public Users (DB db) { Object (db: db); click_pw = null; } /** * get_click_pw: * * Returns: The password file entry for the `clickpkg` user. */ private CachedPasswd get_click_pw () throws UserError { if (click_pw == null) { errno = 0; unowned Posix.Passwd pw = Posix.getpwnam ("clickpkg"); if (pw == null) throw new UserError.GETPWNAM ("Cannot get password file entry " + "for clickpkg: %s", strerror (errno)); click_pw = new CachedPasswd (pw.pw_uid, pw.pw_gid); } return click_pw; } internal void ensure_db () throws UserError { var create = new List (); /* Only modify the last database. */ var try_path = db_top (db.overlay); while (! exists (try_path)) { create.prepend (try_path); try_path = Path.get_dirname (try_path); } foreach (var path in create) { try_create (path); if (Posix.geteuid () == 0) try_chown (path, get_click_pw ()); } } /** * get_user_names: * * Returns: A list of user names with registrations. */ public List get_user_names () throws Error { var entries = new List (); var seen = new Gee.HashSet (); foreach (var single_db in db) { var users_db = db_top (single_db.root); foreach (var entry in Click.Dir.open (users_db)) { if (entry in seen) continue; // the user is not a pseudo user and does not/no-longer exist if (!entry.has_prefix ("@") && Posix.getpwnam (entry) == null) continue; var path = Path.build_filename (users_db, entry); if (is_dir (path)) { seen.add (entry.dup ()); entries.prepend (entry.dup ()); } } } entries.reverse (); return entries; } /** * get_user: * @user_name: A user name. * * Returns: (transfer full): A new #ClickUser instance for @user. */ public User get_user (string user_name) throws Error { foreach (var single_db in db) { var path = db_for_user (single_db.root, user_name); if (is_dir (path)) /* We only require the user path to exist in * any database; it doesn't matter which. */ return new User.for_user (db, user_name); } throw new UserError.NO_SUCH_USER( "User %s does not exist in any database", user_name); } } public class User : Object { public DB db { private get; construct; } public string name { private get; construct; } private Users? users; private CachedPasswd? user_pw; private int dropped_privileges_count; private Posix.mode_t? old_umask; private User (DB? db, string? name = null) throws FileError { DB real_db; string real_name; if (db != null) real_db = db; else { real_db = new DB (); real_db.read (); } if (name != null) real_name = name; else real_name = Environment.get_user_name ().dup (); Object (db: real_db, name: real_name); users = null; user_pw = null; dropped_privileges_count = 0; old_umask = null; } public User.for_user (DB? db, string? name = null) throws FileError { this (db, name); } public User.for_all_users (DB? db) throws FileError { this (db, ALL_USERS); } public User.for_gc_in_use (DB? db) throws FileError { this (db, GC_IN_USE_USER); } /** * True if and only if this user is a pseudo-user. */ public bool is_pseudo_user { get { return name.has_prefix ("@"); } } /** * True if and only if this user is the pseudo-user indicating that * a registration was in use at the time of package removal. */ public bool is_gc_in_use { get { return name == GC_IN_USE_USER; } } /** * get_user_pw: * * Returns: The password file entry for this user. */ private CachedPasswd get_user_pw () throws UserError { assert (! is_pseudo_user); if (user_pw == null) { errno = 0; unowned Posix.Passwd pw = Posix.getpwnam (name); if (pw == null) throw new UserError.GETPWNAM ("Cannot get password file entry for " + "%s: %s", name, strerror (errno)); user_pw = new CachedPasswd (pw.pw_uid, pw.pw_gid); } return user_pw; } /** * get_overlay_db: * * Returns: The path to the overlay database for this user, i.e. the * path where new packages will be installed. */ public string get_overlay_db () { return db_for_user (db.overlay, name); } private void ensure_db () throws UserError { if (users == null) users = new Users (db); users.ensure_db (); var path = get_overlay_db (); if (! exists (path)) { try_create (path); if (Posix.geteuid () == 0 && ! is_pseudo_user) try_chown (path, get_user_pw ()); } } /* Note on privilege handling: * We can normally get away without dropping privilege when reading, * but some filesystems are strict about how much they let root work * with user files (e.g. NFS root_squash). It is better to play it * safe and drop privileges for any operations on the user's * database. */ private void priv_drop_failure (string name) throws UserError { throw new UserError.DROP_PRIVS ("Cannot drop privileges (%s): %s", name, strerror (errno)); } internal void drop_privileges () throws UserError { if (dropped_privileges_count == 0 && Posix.getuid () == 0 && ! is_pseudo_user) { /* We don't bother with setgroups here; we only need * the user/group of created filesystem nodes to be * correct. */ var pw = get_user_pw (); if (PosixExtra.setegid (pw.gid) < 0) priv_drop_failure ("setegid"); if (PosixExtra.seteuid (pw.uid) < 0) priv_drop_failure ("seteuid"); old_umask = Posix.umask (get_umask () | Posix.S_IWOTH); } ++dropped_privileges_count; } private void priv_regain_failure (string name) { /* It is too dangerous to carry on from this point, even if * the caller has an exception handler. */ error ("Cannot regain privileges (%s): %s", name, strerror (errno)); } internal void regain_privileges () { --dropped_privileges_count; if (dropped_privileges_count == 0 && Posix.getuid () == 0 && ! is_pseudo_user) { if (old_umask != null) Posix.umask (old_umask); if (PosixExtra.seteuid (0) < 0) priv_regain_failure ("seteuid"); if (PosixExtra.setegid (0) < 0) priv_regain_failure ("setegid"); } } private bool is_valid_link (string path) { if (! is_symlink (path)) return false; try { var target = FileUtils.read_link (path); return ! target.has_prefix ("@"); } catch (FileError e) { return false; } } private List get_package_names_dropped () throws Error { var entries = new List (); var hidden = new Gee.HashSet (); for (int i = db.size - 1; i >= 0; --i) { var user_db = db_for_user (db[i].root, name); foreach (var entry in Click.Dir.open (user_db)) { if (entries.find_custom (entry, strcmp) != null || entry in hidden) continue; var path = Path.build_filename (user_db, entry); if (is_valid_link (path)) entries.prepend (entry.dup ()); else if (is_symlink (path)) hidden.add (entry.dup ()); } if (name != ALL_USERS) { var all_users_db = db_for_user (db[i].root, ALL_USERS); foreach (var entry in Click.Dir.open (all_users_db)) { if (entries.find_custom (entry, strcmp) != null || entry in hidden) continue; var path = Path.build_filename (all_users_db, entry); if (is_valid_link (path)) entries.prepend (entry.dup ()); else if (is_symlink (path)) hidden.add (entry.dup ()); } } } entries.reverse (); return entries; } /** * get_package_names: * * Returns: (transfer full): A list of package names installed for * this user. */ public List get_package_names () throws Error { drop_privileges (); try { return get_package_names_dropped (); } finally { regain_privileges (); } } /** * has_package_name: * @package: A package name. * * Returns: True if this user has a version of @package registered, * otherwise false. */ public bool has_package_name (string package) { try { get_version (package); return true; } catch (UserError e) { return false; } } /** * get_version: * @package: A package name. * * Returns: The version of @package registered for this user. */ public string get_version (string package) throws UserError { for (int i = db.size - 1; i >= 0; --i) { var user_db = db_for_user (db[i].root, name); var path = Path.build_filename (user_db, package); drop_privileges (); try { if (is_valid_link (path)) { try { var target = FileUtils.read_link (path); return Path.get_basename (target); } catch (FileError e) { } } else if (is_symlink (path)) throw new UserError.HIDDEN_PACKAGE ("%s is hidden for user %s", package, name); } finally { regain_privileges (); } var all_users_db = db_for_user (db[i].root, ALL_USERS); path = Path.build_filename (all_users_db, package); if (is_valid_link (path)) { try { var target = FileUtils.read_link (path); return Path.get_basename (target); } catch (FileError e) { } } else if (is_symlink (path)) throw new UserError.HIDDEN_PACKAGE ("%s is hidden for all users", package); } throw new UserError.NO_SUCH_PACKAGE ("%s does not exist in any database for user %s", package, name); } /** * raw_set_version: * @package: A package name. * @version: A version string. * * Set the version of @package to @version, without running any * hooks. Must be run with dropped privileges. */ internal void raw_set_version (string package, string version) throws Error { assert (dropped_privileges_count > 0); var user_db = get_overlay_db (); var path = Path.build_filename (user_db, package); var new_path = Path.build_filename (user_db, @".$package.new"); var target = db.get_path (package, version); var done = false; if (is_valid_link (path)) { unlink_force (path); try { if (get_version (package) == version) done = true; } catch (UserError e) { } } if (done) return; symlink_force (target, new_path); if (FileUtils.rename (new_path, path) < 0) throw new UserError.RENAME ("rename %s -> %s failed: %s", new_path, path, strerror (errno)); } /** * set_version: * @package: A package name. * @version: A version string. * * Register version @version of @package for this user. */ public void set_version (string package, string version) throws Error { /* Only modify the last database. */ ensure_db (); string? old_version = null; try { old_version = get_version (package); } catch (UserError e) { } drop_privileges (); try { raw_set_version (package, version); } finally { regain_privileges (); } if (! is_pseudo_user) package_install_hooks (db, package, old_version, version, name); // run user hooks for all logged in users if (name == ALL_USERS) run_user_install_hooks_for_all_logged_in_users (package, old_version, version); } private string[] get_logged_in_users() { string[] logged_in_users = {}; try { LogindManager logind = Bus.get_proxy_sync ( BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1"); var users = logind.ListUsers(); foreach (LogindUser user in users) { // FIXME: ideally we would read from /etc/adduser.conf if(user.uid >= 1000 && user.uid <= 30000) { logged_in_users += user.name; } } } catch (Error e) { warning ("Can not connect to logind"); } return logged_in_users; } private void run_user_install_hooks_for_all_logged_in_users (string package, string? old_version, string version) throws IOError { foreach (string username in get_logged_in_users()) package_install_hooks (db, package, old_version, version, username); } private void run_user_remove_hooks_for_all_logged_in_users (string package, string old_version) throws IOError { foreach (string username in get_logged_in_users()) package_remove_hooks (db, package, old_version, username); } private string get_dbus_session_bus_env_for_current_user() { string euid = "%i".printf((int)(Posix.geteuid ())); var dbus_session_file = Path.build_filename( "/run", "user", euid, "dbus-session"); string session_env; try { FileUtils.get_contents(dbus_session_file, out session_env); session_env = session_env.strip(); } catch (Error e) { warning("Can not get the dbus session to stop app (%s)", e.message); } return session_env; } private bool stop_single_app (string app_id) { // get the users dbus session when we run as root first as this // is where lomiri-app-stop listens string[] envp = Environ.get(); envp += get_dbus_session_bus_env_for_current_user(); string[] command = { "lomiri-app-stop", app_id }; bool res = false; try { int exit_status; Process.spawn_sync (null, command, envp, SpawnFlags.SEARCH_PATH, null, null, null, out exit_status); res = Process.check_exit_status (exit_status); } catch (Error e) { res = false; } return res; } private bool stop_running_apps_for_package (string package, string version) { var res = true; if (! find_on_path ("lomiri-app-stop")) return false; Json.Object manifest; try { manifest = get_manifest (package); } catch (Error e) { warning ("Can not get manifest for %s", package); return false; } if (! manifest.has_member ("hooks")) { warning ("No hooks in manifest %s", package); return false; } var hooks = manifest.get_object_member ("hooks"); foreach (unowned string app_name in hooks.get_members ()) res &= stop_single_app (@"$(package)_$(app_name)_$(version)"); return res; } private void package_remove_user_cache (string package) throws Error { drop_privileges(); try { string path = Path.build_filename ( Environment.get_user_cache_dir (), package); File file = File.new_for_path (path); rmtree (file, null); } catch (Error e) { warning ("Error removing cache for '%s': %s", package, e.message); } finally { regain_privileges(); } } /** * remove: * @package: A package name. * * Remove this user's registration of @package. */ public void remove (string package) throws Error { /* Only modify the last database. */ var user_db = get_overlay_db (); var path = Path.build_filename (user_db, package); string old_version; if (is_valid_link (path)) { var target = FileUtils.read_link (path); old_version = Path.get_basename (target); drop_privileges (); try { // stop before removing the path to the manifest stop_running_apps_for_package (package, old_version); unlink_force (path); } finally { regain_privileges (); } } else { try { old_version = get_version (package); } catch (UserError e) { throw new UserError.NO_SUCH_PACKAGE ("%s does not exist in any database " + "for user %s", package, name); } ensure_db (); drop_privileges (); try { // stop before removing the path to the manifest stop_running_apps_for_package (package, old_version); symlink_force (HIDDEN_VERSION, path); } finally { regain_privileges (); } } if (! is_pseudo_user) { package_remove_hooks (db, package, old_version, name); package_remove_user_cache (package); } // run user hooks for all logged in users if (name == ALL_USERS) run_user_remove_hooks_for_all_logged_in_users (package, old_version); } /** * get_path: * @package: A package name. * * Returns: The path at which @package is registered for this user. */ public string get_path (string package) throws UserError { for (int i = db.size - 1; i >= 0; --i) { var user_db = db_for_user (db[i].root, name); var path = Path.build_filename (user_db, package); if (is_valid_link (path)) return path; else if (is_symlink (path)) throw new UserError.HIDDEN_PACKAGE ("%s is hidden for user %s", package, name); var all_users_db = db_for_user (db[i].root, ALL_USERS); path = Path.build_filename (all_users_db, package); if (is_valid_link (path)) return path; else if (is_symlink (path)) throw new UserError.HIDDEN_PACKAGE ("%s is hidden for all users", package); } throw new UserError.NO_SUCH_PACKAGE ("%s does not exist in any database for user %s", package, name); } /** * get_manifest: * @package: A package name. * * Returns: A #Json.Object containing a package's manifest. * * Since: 0.4.18 */ public Json.Object get_manifest (string package) throws Error { var obj = db.get_manifest (package, get_version (package)); /* Adjust _directory to point to the user registration path. */ obj.set_string_member ("_directory", get_path (package)); /* This should really be a boolean, but it was mistakenly * made an int when the "_removable" key was first created. * We may change this in future. */ obj.set_int_member ("_removable", is_removable (package) ? 1 : 0); return obj; } /** * get_manifest_as_string: * @package: A package name. * * Returns: A JSON string containing a package's serialised * manifest. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifest_as_string (string package) throws Error { var manifest = get_manifest (package); var node = new Json.Node (Json.NodeType.OBJECT); node.set_object (manifest); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /** * get_manifests: * * Returns: A #Json.Array containing manifests of all packages * registered for this user. The manifest may include additional * dynamic keys (starting with an underscore) corresponding to * dynamic properties of installed packages. * * Since: 0.4.18 */ public Json.Array get_manifests () throws Error /* API-compatibility */ { var ret = new Json.Array (); foreach (var package in get_package_names ()) { try { ret.add_object_element (get_manifest (package)); } catch (Error e) { warning ("%s", e.message); } } return ret; } /** * get_manifests_as_string: * * Returns: A JSON string containing a serialised array of manifests * of all packages registered for this user. The manifest may * include additional dynamic keys (starting with an underscore) * corresponding to dynamic properties of installed packages. * This interface may be useful for clients with their own JSON * parsing tools that produce representations more convenient for * them. * * Since: 0.4.21 */ public string get_manifests_as_string () throws Error /* API-compatibility */ { var manifests = get_manifests (); var node = new Json.Node (Json.NodeType.ARRAY); node.set_array (manifests); var generator = new Json.Generator (); generator.set_root (node); return generator.to_data (null); } /** * is_removable: * @package: A package name. * * Returns: True if @package is removable for this user, otherwise * False. */ public bool is_removable (string package) { var user_db = get_overlay_db (); var path = Path.build_filename (user_db, package); if (exists (path)) return true; else if (is_symlink (path)) /* Already hidden. */ return false; var all_users_db = db_for_user (db.overlay, ALL_USERS); path = Path.build_filename (all_users_db, package); if (is_valid_link (path)) return true; else if (is_symlink (path)) /* Already hidden. */ return false; if (has_package_name (package)) /* Not in overlay database, but can be hidden. */ return true; else return false; } } } click-0.5.0/lib/click/valac-wrapper.in000066400000000000000000000030331402441472600175330ustar00rootroot00000000000000#! /bin/sh set -e # Wrapper for valac, working around the fact that the .gir files it # generates are missing the shared-library attribute in the namespace tag. # # https://bugzilla.gnome.org/show_bug.cgi?id=642576 # # Passing --shared-library to g-ir-compiler isn't enough for us, because # dh_girepository then fails to generate a shared library dependency. # # While we're here, work around showing up in our external header # file. We're careful only to make use of it internally. VALAC="@VALAC@" "$VALAC" "$@" header= gir= library= # Keep this in sync with any options used in lib/click/Makefile.am. -C is # emitted by automake. eval set -- "$(getopt -o CH: -l gir:,library:,pkg:,target-glib: -- "$@")" || \ { echo "$0: failed to parse valac options" >&2; exit 2; } while :; do case $1 in -C) shift ;; -H) header="$2"; shift 2 ;; --pkg|--target-glib) shift 2 ;; --gir) gir="$2"; shift 2 ;; --library) library="$2"; shift 2 ;; --) shift; break ;; *) echo "$0: failed to parse valac options" >&2; exit 2 ;; esac done [ "$header" ] || { echo "$0: failed to find -H in valac options" >&2; exit 2; } [ "$gir" ] || { echo "$0: failed to find --gir in valac options" >&2; exit 2; } [ "$library" ] || \ { echo "$0: failed to find --library in valac options" >&2; exit 2; } if egrep 'Gee|gee_' "$header"; then echo "libgee should not be exposed in our public header file." >&2 exit 1 fi sed -i '/^#include $/d' "$header" sed -i 's/\(\n') doctype = self._builder._doctype outfile.write("\n" % ( doctype[0], "" if doctype[1] is None else "\n PUBLIC '%s'" % doctype[1], "" if doctype[2] is None else "\n SYSTEM '%s'" % doctype[2])) try: self._tree.write(outfile, encoding="unicode") finally: outfile.close() def main(): parser = OptionParser(usage="%prog FILE [...]") parser.add_option( "-o", "--output", help="output file name (default: stdout)") options, args = parser.parse_args() if not args: parser.error("need at least one input file") coverage = Coverage() for arg in args: coverage.merge(arg) coverage.write(options.output) if __name__ == "__main__": main() click-0.5.0/preload/000077500000000000000000000000001402441472600142335ustar00rootroot00000000000000click-0.5.0/preload/Makefile.am000066400000000000000000000002561402441472600162720ustar00rootroot00000000000000pkglib_LTLIBRARIES = libclickpreload.la libclickpreload_la_SOURCES = clickpreload.c libclickpreload_la_LIBADD = @PRELOAD_LIBS@ libclickpreload_la_LDFLAGS = -avoid-version click-0.5.0/preload/clickpreload.c000066400000000000000000000342221402441472600170360ustar00rootroot00000000000000/* Copyright (C) 2013 Canonical Ltd. * Author: Colin Watson * * 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; version 3 of the License. * * 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 . */ /* Stub out a few syscalls that are unhelpful when installing Click * packages. This is roughly akin to the effect of using all of fakechroot, * fakeroot, and eatmydata, but a few orders of magnitude simpler. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include static int (*libc_chmod) (const char *, mode_t) = (void *) 0; static int (*libc_chown) (const char *, uid_t, gid_t) = (void *) 0; static int (*libc_execvp) (const char *, char * const []) = (void *) 0; static int (*libc_fchmod) (int, mode_t) = (void *) 0; static int (*libc_fchown) (int, uid_t, gid_t) = (void *) 0; static FILE *(*libc_fopen) (const char *, const char *) = (void *) 0; #ifdef __GLIBC__ static FILE *(*libc_fopen64) (const char *, const char *) = (void *) 0; #endif static struct group *(*libc_getgrnam) (const char *) = (void *) 0; static struct passwd *(*libc_getpwnam) (const char *) = (void *) 0; static int (*libc_lchown) (const char *, uid_t, gid_t) = (void *) 0; static int (*libc_link) (const char *, const char *) = (void *) 0; static int (*libc_mkdir) (const char *, mode_t) = (void *) 0; static int (*libc_mkfifo) (const char *, mode_t) = (void *) 0; static int (*libc_open) (const char *, int, mode_t) = (void *) 0; #ifdef __GLIBC__ static int (*libc_open64) (const char *, int, mode_t) = (void *) 0; #endif static int (*libc_symlink) (const char *, const char *) = (void *) 0; static int (*libc___xmknod) (int, const char *, mode_t, dev_t *) = (void *) 0; static int (*libc___xstat) (int, const char *, struct stat *) = (void *) 0; static int (*libc___xstat64) (int, const char *, struct stat64 *) = (void *) 0; uid_t euid; struct passwd root_pwd; struct group root_grp; const char *base_path; size_t base_path_len; const char *package_path; int package_fd; #define GET_NEXT_SYMBOL(name) \ do { \ char *err; \ libc_##name = dlsym (RTLD_NEXT, #name); \ if ((err = dlerror ()) != NULL) { \ fprintf (stderr, \ "Error getting address of symbol '" #name "': %s\n", \ err); \ fflush (stderr); \ _exit (1); \ } \ } while (0) static void __attribute__ ((constructor)) clickpreload_init (void) { const char *package_fd_str; /* Clear any old error conditions, albeit unlikely, as per dlsym(2) */ dlerror (); GET_NEXT_SYMBOL (chmod); GET_NEXT_SYMBOL (chown); GET_NEXT_SYMBOL (execvp); GET_NEXT_SYMBOL (fchmod); GET_NEXT_SYMBOL (fchown); GET_NEXT_SYMBOL (fopen); #ifdef __GLIBC__ GET_NEXT_SYMBOL (fopen64); #endif GET_NEXT_SYMBOL (getgrnam); GET_NEXT_SYMBOL (getpwnam); GET_NEXT_SYMBOL (lchown); GET_NEXT_SYMBOL (link); GET_NEXT_SYMBOL (mkdir); GET_NEXT_SYMBOL (mkfifo); GET_NEXT_SYMBOL (open); #ifdef __GLIBC__ GET_NEXT_SYMBOL (open64); #endif GET_NEXT_SYMBOL (symlink); GET_NEXT_SYMBOL (__xmknod); GET_NEXT_SYMBOL (__xstat); GET_NEXT_SYMBOL (__xstat64); euid = geteuid (); /* dpkg only cares about these fields. */ root_pwd.pw_uid = 0; root_grp.gr_gid = 0; base_path = getenv ("CLICK_BASE_DIR"); base_path_len = base_path ? strlen (base_path) : 0; package_path = getenv ("CLICK_PACKAGE_PATH"); package_fd_str = getenv ("CLICK_PACKAGE_FD"); package_fd = atoi (package_fd_str); } /* dpkg calls chown/fchown/lchown to set permissions of extracted files. If * we aren't running as root, we don't care. */ int chown (const char *path, uid_t owner, gid_t group) { if (euid != 0) return 0; if (!libc_chown) clickpreload_init (); return (*libc_chown) (path, owner, group); } int fchown (int fd, uid_t owner, gid_t group) { if (euid != 0) return 0; if (!libc_fchown) clickpreload_init (); return (*libc_fchown) (fd, owner, group); } int lchown (const char *path, uid_t owner, gid_t group) { if (euid != 0) return 0; if (!libc_lchown) clickpreload_init (); return (*libc_lchown) (path, owner, group); } /* Similarly, we don't much care about passwd/group lookups when we aren't * root. (This could be more sanely replaced by having dpkg cache those * lookups itself.) */ struct passwd *getpwnam (const char *name) { if (!libc_getpwnam) clickpreload_init (); /* also needed for root_pwd */ if (euid != 0) return &root_pwd; return (*libc_getpwnam) (name); } struct group *getgrnam (const char *name) { if (!libc_getgrnam) clickpreload_init (); /* also needed for root_grp */ if (euid != 0) return &root_grp; return (*libc_getgrnam) (name); } /* dpkg calls chroot to run maintainer scripts when --instdir is used (which * we use so that we can have independently-rooted filesystem tarballs). * However, there is exactly one maintainer script ever used by Click * packages, and that's a static preinst which doesn't touch the filesystem * except to be executed with /bin/sh. Chrooting for this causes more * problems than it solves. */ int chroot (const char *path) { return 0; } /* dpkg executes the static preinst. We don't want it. */ int execvp (const char *file, char * const argv[]) { if (strcmp (file, "/.click/tmp.ci/preinst") == 0) _exit (0); if (!libc_execvp) clickpreload_init (); return (*libc_execvp) (file, argv); } /* dpkg calls fsync/sync_file_range quite a lot. However, Click packages * never correspond to essential system facilities, so it's OK to compromise * perfect write reliability in the face of hostile filesystem * implementations for performance. * * (Note that dpkg only started using fsync/sync_file_range relatively * recently, and on many reasonable filesystem configurations using those * functions buys us nothing; most of dpkg's reliability comes from other * strategies, such as careful unpack and renaming into place.) */ int fsync (int fd) { return 0; } int sync_file_range(int fd, off64_t offset, off64_t nbytes, unsigned int flags) { return 0; } /* Sandboxing: * * We try to insulate against dpkg getting confused enough by malformed * archives to write outside the instdir. This is not full confinement, and * generally for system security it should be sufficient to run "click * install" as a specialised user; as such we don't necessarily wrap all * possible relevant functions here. The main purpose of this is just to * provide a useful error message if dpkg gets confused. */ static void clickpreload_assert_path_in_instdir (const char *verb, const char *pathname) { if (strncmp (pathname, base_path, base_path_len) == 0 && (pathname[base_path_len] == '\0' || pathname[base_path_len] == '/')) return; /* When building click in a chroot with pkgbinarymangler, dpkg-deb is in * fact a wrapper shell script, and bash checks at startup whether it * can open /dev/tty for writing. This is harmless, so allow it. */ if (strcmp (verb, "write-open") == 0 && strcmp (pathname, "/dev/tty") == 0) return; fprintf (stderr, "Sandbox failure: 'click install' not permitted to %s '%s'\n", verb, pathname); fflush (stderr); exit (1); } int link (const char *oldpath, const char *newpath) { if (!libc_link) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("make hard link", newpath); return (*libc_link) (oldpath, newpath); } int mkdir (const char *pathname, mode_t mode) { if (!libc_mkdir) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("mkdir", pathname); return (*libc_mkdir) (pathname, mode); } int mkfifo (const char *pathname, mode_t mode) { if (!libc_mkfifo) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("mkfifo", pathname); return (*libc_mkfifo) (pathname, mode); } int symlink (const char *oldpath, const char *newpath) { if (!libc_symlink) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("make symbolic link", newpath); return (*libc_symlink) (oldpath, newpath); } /* As well as write sandboxing, our versions of fopen, open, and stat also * trap accesses to the package path and turn them into accesses to a fixed * file descriptor instead. With some cooperation from click.install, this * allows dpkg to read packages in paths not readable by the clickpkg user. * * We cannot do this entirely perfectly. In particular, we have to seek to * the start of the file on open, but the file offset is shared among all * duplicates of a file descriptor. Let's hope that dpkg doesn't open the * .deb multiple times and expect to have independent file offsets ... */ FILE *fopen (const char *pathname, const char *mode) { int for_reading = (strncmp (mode, "r", 1) == 0 && strncmp (mode, "r+", 2) != 0); if (!libc_fopen) clickpreload_init (); /* also needed for package_path */ if (for_reading && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return fdopen (dup_fd, mode); } if (!for_reading) clickpreload_assert_path_in_instdir ("write-fdopen", pathname); return (*libc_fopen) (pathname, mode); } #ifdef __GLIBC__ FILE *fopen64 (const char *pathname, const char *mode) { int for_reading = (strncmp (mode, "r", 1) == 0 && strncmp (mode, "r+", 2) != 0); if (!libc_fopen64) clickpreload_init (); /* also needed for package_path */ if (for_reading && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return fdopen (dup_fd, mode); } if (!for_reading) clickpreload_assert_path_in_instdir ("write-fdopen", pathname); return (*libc_fopen64) (pathname, mode); } #endif int open (const char *pathname, int flags, ...) { int for_writing = ((flags & O_WRONLY) || (flags & O_RDWR)); mode_t mode = 0; int ret; if (!libc_open) clickpreload_init (); /* also needed for package_path */ if (!for_writing && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return dup_fd; } if (for_writing) clickpreload_assert_path_in_instdir ("write-open", pathname); if (flags & O_CREAT) { va_list argv; va_start (argv, flags); mode = va_arg (argv, mode_t); va_end (argv); } ret = (*libc_open) (pathname, flags, mode); return ret; } #ifdef __GLIBC__ int open64 (const char *pathname, int flags, ...) { int for_writing = ((flags & O_WRONLY) || (flags & O_RDWR)); mode_t mode = 0; int ret; if (!libc_open64) clickpreload_init (); /* also needed for package_path */ if (!for_writing && package_path && strcmp (pathname, package_path) == 0) { int dup_fd = dup (package_fd); lseek (dup_fd, 0, SEEK_SET); /* also changes offset of package_fd */ return dup_fd; } if (for_writing) clickpreload_assert_path_in_instdir ("write-open", pathname); if (flags & O_CREAT) { va_list argv; va_start (argv, flags); mode = va_arg (argv, mode_t); va_end (argv); } ret = (*libc_open64) (pathname, flags, mode); return ret; } #endif int __xmknod (int ver, const char *pathname, mode_t mode, dev_t *dev) { if (!libc___xmknod) clickpreload_init (); /* also needed for base_path, base_path_len */ clickpreload_assert_path_in_instdir ("mknod", pathname); return (*libc___xmknod) (ver, pathname, mode, dev); } int __xstat (int ver, const char *pathname, struct stat *buf) { if (!libc___xstat) clickpreload_init (); /* also needed for package_path */ if (package_path && strcmp (pathname, package_path) == 0) return __fxstat (ver, package_fd, buf); return (*libc___xstat) (ver, pathname, buf); } int __xstat64 (int ver, const char *pathname, struct stat64 *buf) { if (!libc___xstat64) clickpreload_init (); /* also needed for package_path */ if (package_path && strcmp (pathname, package_path) == 0) return __fxstat64 (ver, package_fd, buf); return (*libc___xstat64) (ver, pathname, buf); } /* As well as write sandboxing, our versions of chmod and fchmod also * prevent the 0200 (u+w) permission bit from being removed from unpacked * files. dpkg normally expects to be run as root which can override DAC * write permissions, so a mode 04xx file is not normally a problem for it, * but it is a problem when running dpkg as non-root. Since unpacked * packages are non-writeable from the point of view of the package's code, * forcing u+w is safe. */ int chmod (const char *path, mode_t mode) { if (!libc_chmod) clickpreload_init (); /* also needed for package_path */ clickpreload_assert_path_in_instdir ("chmod", path); mode |= S_IWUSR; return (*libc_chmod) (path, mode); } int fchmod (int fd, mode_t mode) { if (!libc_fchmod) clickpreload_init (); mode |= S_IWUSR; return (*libc_fchmod) (fd, mode); } click-0.5.0/run-click000077500000000000000000000001341402441472600144200ustar00rootroot00000000000000#! /bin/sh set -e LD_LIBRARY_PATH=lib/click/.libs GI_TYPELIB_PATH=lib/click bin/click "$@" click-0.5.0/run-tests000077500000000000000000000003521402441472600144770ustar00rootroot00000000000000#! /bin/sh # Depends: # python, # python-mock, # python3, # python3 (>= 3.3) | python3-mock # python-tox if ! [ -r preload/.libs/libclickpreload.so ]; then echo "W: preload bits not built; will skip some tests" >&2 fi tox click-0.5.0/schroot/000077500000000000000000000000001402441472600142665ustar00rootroot00000000000000click-0.5.0/schroot/Makefile.am000066400000000000000000000001121402441472600163140ustar00rootroot00000000000000schroot_clickdir = @sysconfdir@/schroot/click schroot_click_DATA = fstab click-0.5.0/schroot/fstab000066400000000000000000000012341402441472600153100ustar00rootroot00000000000000# fstab: static file system information for chroots. # Note that the mount point will be prefixed by the chroot path # (CHROOT_PATH) # # /proc /proc none rw,bind 0 0 /sys /sys none rw,bind 0 0 /dev /dev none rw,bind 0 0 /dev/pts /dev/pts none rw,bind 0 0 /home /home none rw,rbind 0 0 /tmp /tmp none rw,bind 0 0 /run/shm /run/shm none rw,bind 0 0 click-0.5.0/setup.py.in000077500000000000000000000024661402441472600147370ustar00rootroot00000000000000#! /usr/bin/env python3 import sys from setuptools import find_packages, setup from setuptools.command.test import test requirements = [] def require(package, pypi_name=None): try: __import__(package) except ImportError: requirements.append(package if pypi_name is None else pypi_name) require('debian', 'python-debian') if sys.version < "3.3": require('mock') require('chardet') if "@GCOVR@": require('coverage') class test_extra(test): def run(self): if "@GCOVR@": coverage_executable = "python3-coverage" self.spawn([ coverage_executable, "run", "-m", "unittest", "discover", "-vv", "click_package.tests"]) self.spawn([coverage_executable, "combine"]) self.spawn([ coverage_executable, "xml", "-o", "coverage-python.xml"]) self.spawn([coverage_executable, "report"]) else: test.run(self) setup( name="click", version="@PACKAGE_VERSION@", description="Click package manager", author="Colin Watson", author_email="cjwatson@ubuntu.com", license="GNU GPL", packages=find_packages(), scripts=['bin/click'], install_requires=requirements, cmdclass={"test": test_extra}, test_suite="click_package.tests", ) click-0.5.0/tox.ini000066400000000000000000000002141402441472600141150ustar00rootroot00000000000000[tox] envlist = py27,py32,py33,py34,py35,py36 [testenv] commands = python -m unittest discover -vv click_package.tests sitepackages = True