From ee1a6a206bcc9c6cf29f4b3379e808ce4c030377 Mon Sep 17 00:00:00 2001 From: clime Date: Oct 16 2017 12:52:41 +0000 Subject: update --- diff --git a/rpkg-client/.gitignore b/rpkg-client/.gitignore new file mode 100644 index 0000000..58a7f5f --- /dev/null +++ b/rpkg-client/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*.pyo +.cache +*.tar.gz +*.src.rpm diff --git a/rpkg-client/.tito/packages/.readme b/rpkg-client/.tito/packages/.readme new file mode 100644 index 0000000..b9411e2 --- /dev/null +++ b/rpkg-client/.tito/packages/.readme @@ -0,0 +1,3 @@ +the .tito/packages directory contains metadata files +named after their packages. Each file has the latest tagged +version and the project's relative directory. diff --git a/rpkg-client/.tito/packages/rpkg-client b/rpkg-client/.tito/packages/rpkg-client new file mode 100644 index 0000000..5085848 --- /dev/null +++ b/rpkg-client/.tito/packages/rpkg-client @@ -0,0 +1 @@ +0.8-1 ./ diff --git a/rpkg-client/.tito/tito.props b/rpkg-client/.tito/tito.props new file mode 100644 index 0000000..eab3f19 --- /dev/null +++ b/rpkg-client/.tito/tito.props @@ -0,0 +1,5 @@ +[buildconfig] +builder = tito.builder.Builder +tagger = tito.tagger.VersionTagger +changelog_do_not_remove_cherrypick = 0 +changelog_format = %s (%ae) diff --git a/rpkg-client/LICENSE b/rpkg-client/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/rpkg-client/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + + 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 +convey 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 2 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, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision 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, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This 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. diff --git a/rpkg-client/README.md b/rpkg-client/README.md new file mode 100644 index 0000000..5aba0b9 --- /dev/null +++ b/rpkg-client/README.md @@ -0,0 +1,55 @@ +This application is an RPM packaging utility based on python-rpkg library. It works with both [DistGit](https://github.com/release-engineering/dist-git) +and Git repositories and it handles two types of directory content: _packed_ content and _unpacked_ content. + +- Packed (unexpanded) content is that composed of tarballs, patches, and a .spec file. +- Unpacked (expanded) content is that composed of plain source files and a .spec file. + +For packed content, if you ask `rpkg` to make srpm (`rpkg srpm`), it will download any external +files from the appropriate storage (e.g. lookaside cache for DistGit) and then it will invoke +`rpmbuild -bs` with `_sourcedir`, `_specdir`, `_builddir`, `_srcrpmdir`,`_rpmdir` macros all +set to the working directory. + +For unpacked content, if you ask `rpkg` to do the same thing, it will download external sources (if any) +and then it will also generate a tarball from the whole content of the working directory named according +to `Source0` definition present in the associated .spec file. This tarball and the .spec are then passed +to the same rpmbuild command as above for the packed content. + +Note that by dynamically creating the tarball in the working directory according to the `Source0` +definition, the directory content becomes packed because there is at least one file, which is referenced +from the .spec file as `Source` or `Patch`. You can find the exact definitions of "packed" and "unpacked" +in `rpkg` man pages (see PACKED VS UNPACKED section for examples) or with `rpkg make-source --help`. + +Apart from generating srpms from the application sources, you can also run other useful packaging commands +like `rpkg lint` to check the .spec file and the overall package conformance to RPM standard, `rpkg local` +to locally build the package into an rpm, or `rpkg copr-build` to build an srpm and send it for build to +[COPR](https://copr.fedorainfracloud.org). + +Examples: +``` + $ cd unpacked-copr-build-example + $ ls . + doc LICENSE README.md rpkg rpkg.bash rpkg-client.spec rpkg.conf rpkglib run_tests.sh setup.py tests + $ rpkg copr-build user/project + Wrote: copr-build-example/rpkg-client-0.8.tar.gz + Wrote: copr-build-example/rpkg-client-0.8-1.fc25.src.rpm + Uploading package rpkg-client-0.8-1.fc25.src.rpm + 100% |################################| 49kB 263kB/s eta 0:00:00 + Build was added to example: + https://copr.fedorainfracloud.org/coprs/build/625402/ + Created builds: 625402 + ... +``` +``` + $ cd prep-example + $ ls . + doc LICENSE README.md rpkg rpkg.bash rpkg-client.spec rpkg.conf rpkglib run_tests.sh setup.py tests + $ rpkg make-source + Wrote: rpkg-client/rpkg-client-0.8.tar.gz + $ rpkg prep + Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.bd5cCF + + umask 022 + ... + $ rpkg clean +``` + +You can find more information and more examples in rpkg man pages (`man rpkg`). diff --git a/rpkg-client/doc/rpkg_man_page.py b/rpkg-client/doc/rpkg_man_page.py new file mode 100644 index 0000000..68ce5f1 --- /dev/null +++ b/rpkg-client/doc/rpkg_man_page.py @@ -0,0 +1,273 @@ +# Print a man page from the help texts. +# +# Copyright (C) 2011 Red Hat Inc. +# Author(s): Jesse Keating +# +# 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 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + + +import os +import sys +import datetime + + +# We could substitute the "" in .TH with the rpkg version if we knew it +man_header = """\ +.\\" man page for rpkg +.TH rpkg 1 "%(today)s" "" "rpm\\-packager" +.SH "NAME" +rpkg \\- RPM Packaging utility +.SH "SYNOPSIS" +.B "rpkg" +[ +.I global_options +] +.I "command" +[ +.I command_options +] +[ +.I command_arguments +] +.br +.B "rpkg" +.B "help" +.br +.B "rpkg" +.I "command" +.B "\\-\\-help" +.SH "DESCRIPTION" +.B "rpkg" +is a script to maintain RPM package content. It is designed to work with expanded sources as well +as with tarballs and patches. Note that you should trust the .spec files you work with because +many operations (like `rpkg srpm`, `rpkg lint`, or `rpkg is-packed`) involve parsing the spec file, +which brings along evalution of any shell or lua scriplets. +""" + +man_footer = """\ +.SH "EXAMPLES" + + $ cd prep-example + $ ls . + doc LICENSE README.md rpkg rpkg.bash rpkg-client.spec rpkg.conf rpkglib run_tests.sh setup.py tests + $ rpkg prep + error: File rpkg-client/rpkg-client-0.8.tar.gz: No such file or directory + $ rpkg make-source + Wrote: rpkg-client/rpkg-client-0.8.tar.gz + $ rpkg prep + Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.bd5cCF + + umask 022 + ... + $ rpkg clean + + In this example, we run prep phase of rpmbuild process in an originally unpacked directory. At first we get + an error about the tarball not being present. We first need to run `rpkg make-source` manually to create it + (which makes the working directory content "packed" by the way). Then `rpkg prep` can be successfully executed. + The following applies also to `rpkg local` and `rpkg install`. In the end, the generated tarball can be removed + with `rpkg clean` if the working directory is a Git repo. + + $ cd unpacked-copr-build-example + $ ls . + doc LICENSE README.md rpkg rpkg.bash rpkg-client.spec rpkg.conf rpkglib run_tests.sh setup.py tests + $ rpkg copr-build user/project + Wrote: copr-build-example/rpkg-client-0.8.tar.gz + Wrote: copr-build-example/rpkg-client-0.8-1.fc25.src.rpm + Uploading package rpkg-client-0.8-1.fc25.src.rpm + 100% |################################| 49kB 263kB/s eta 0:00:00 + Build was added to example: + https://copr.fedorainfracloud.org/coprs/build/625402/ + Created builds: 625402 + ... + + This example illustrates launching a COPR build directly from an unpacked (expanded) content. + SRPM is first build and then sent to COPR with copr-cli tool. + +.SH "PACKED VS UNPACKED" + +While it is quite intuitive what is packed content (.spec + tarballs + patches) +and what is unpacked content (.spec + original application source files), it +might be useful to know how exactly rpkg differentiates between these two. +You can go through the following examples to get overview how this tool exactly +works. + +.SS PACKED CONTENT: + + $ cd source0-present-example + $ grep -E '(Source|Patch)' testpkg.spec + Source0: foo.tar.gz + $ ls . + foo.tar.gz testpkg.spec + $ rpkg make-source + Could not execute make_source: Not an unpacked content. + $ rpkg srpm + Failed to get module name from Git url or pushurl. + Wrote: source0-present-example/testpkg-1-1.fc25.src.rpm + + The error about module name is caused by running `rpkg` + on a plain directory content and not a Git repo. In this + case module name is read out from the spec file. The + error about not being able to make source is expected for + packed content (tarballs to be put into srpm are expected + to be present already). + + $ cd only-ignored-files-example + $ grep -E '(Source|Patch)' testpkg.spec + Source0: https://example.org/foo.tar.gz + $ ls . + README.md testpkg.spec + $ rpkg make-source + Could not execute make_source: Not an unpacked content. + $ echo '%_disable_source_fetch 0' >> ~/.rpmmacros + $ rpkg srpm + Failed to get module name from Git url or pushurl + warning: Downloading https://example.org/foo.tar.gz to only-ignored-files-example/foo.tar.gz + Wrote: only-ignored-files-example/testpkg-1-1.fc25.src.rpm + + In this example, sources are downloaded from network when + srpm is being built. The %_disable_source_fetch rpm macro + must be set to 0 and the tarball url must be valid for this + to work. The content is recognized as packed because there + are only ignored files in the directory (.spec and readmes). + +.SS UNPACKED CONTENT: + + $ cd cpp-source-file-present-example + $ git init . + $ git remote add origin https://github.com/testpkg.git + $ grep -E '(Source|Patch)' testpkg.spec + Source0: foo.tar.gz + $ ls . + main.cpp testpkg.spec + $ rpkg make-source + Wrote: cpp-source-file-present-example/foo.tar.gz + $ rpkg srpm + Wrote: cpp-source-file-present-example/testpkg-1-1.fc25.src.rpm + + foo.tar.gz (the only Source referenced from the .spec file) is + not present and there is unignored main.cpp file that makes the + content recognized as unpacked. When `rpkg make-source` is invoked, + foo.tar.gz is created and will contain the main.cpp file (as + well as the .spec metadata file but that is just because the + whole content directory is packed). Note that the error about + failing to get module name from Git url disappeared because + we have run `git init .` and `git remote add ...`. + +.SH "SEE ALSO" +.UR "https://pagure.io/rpkg-client/" +.BR "https://pagure.io/rpkg-client/" +""" + + +class ManFormatter(object): + def __init__(self, man): + self.man = man + + def write(self, data): + for line in data.split('\n'): + self.man.write(' %s\n' % line) + + +def strip_usage(s): + """Strip "usage: " string from beginning of string if present""" + if s.startswith('usage: '): + return s.replace('usage: ', '', 1) + else: + return s + + +def man_constants(): + """Global constants for man file templates""" + today = datetime.date.today() + today_manstr = today.strftime(r'%Y\-%m\-%d') + return {'today': today_manstr} + + +def generate(parser, subparsers): + """\ + Generate the man page on stdout + + Given the argparse based parser and subparsers arguments, generate + the corresponding man page and write it to stdout. + """ + + # Not nice, but works: Redirect any print statement output to + # stderr to avoid clobbering the man page output on stdout. + man_file = sys.stdout + sys.stdout = sys.stderr + + mf = ManFormatter(man_file) + + choices = subparsers.choices + k = sorted(choices.keys()) + + man_file.write(man_header % man_constants()) + + helptext = parser.format_help() + helptext = strip_usage(helptext) + helptextsplit = helptext.split('\n') + helptextsplit = [line for line in helptextsplit + if not line.startswith(' -h, --help')] + + man_file.write('.SS "%s"\n' % ("Global Options",)) + + outflag = False + for line in helptextsplit: + if line == "optional arguments:": + outflag = True + elif line == "": + outflag = False + elif outflag: + man_file.write("%s\n" % line) + + help_texts = {} + for pa in subparsers._choices_actions: + help_texts[pa.dest] = getattr(pa, 'help', None) + + man_file.write('.SH "COMMAND OVERVIEW"\n') + + for command in k: + cmdparser = choices[command] + if not cmdparser.add_help: + continue + usage = cmdparser.format_usage() + usage = strip_usage(usage) + usage = ''.join(usage.split('\n')) + usage = ' '.join(usage.split()) + if help_texts[command]: + man_file.write('.TP\n.B "%s"\n%s\n' % (usage, help_texts[command])) + else: + man_file.write('.TP\n.B "%s"\n' % (usage)) + + man_file.write('.SH "COMMAND REFERENCE"\n') + for command in k: + cmdparser = choices[command] + if not cmdparser.add_help: + continue + + man_file.write('.SS "%s"\n' % cmdparser.prog) + + help = help_texts[command] + if help and not cmdparser.description: + if not help.endswith('.'): + help = "%s." % help + cmdparser.description = help + + h = cmdparser.format_help() + mf.write(h) + + man_file.write(man_footer) + + +if __name__ == '__main__': + module_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + sys.path.insert(0, module_path) + + from rpkglib.cli import rpkgClient + client = rpkgClient(name='rpkg', config=None) + client.do_imports('rpkglib') + + generate(client.parser, client.subparsers) diff --git a/rpkg-client/foo.spec b/rpkg-client/foo.spec new file mode 100644 index 0000000..9c7ad1f --- /dev/null +++ b/rpkg-client/foo.spec @@ -0,0 +1,36 @@ +Name: +Version: +Release: 1%{?dist} +Summary: + +Group: +License: +URL: +Source0: + +BuildRequires: +Requires: + +%description + + +%prep +%setup -q + + +%build +%configure +make %{?_smp_mflags} + + +%install +%make_install + + +%files +%doc + + + +%changelog + diff --git a/rpkg-client/rpkg b/rpkg-client/rpkg new file mode 100755 index 0000000..a8b64b5 --- /dev/null +++ b/rpkg-client/rpkg @@ -0,0 +1,78 @@ +#!/usr/bin/python +# rpkg - a script to interact with the Red Hat Packaging system +# +# Copyright (C) 2011 Red Hat Inc. +# Author(s): Jesse Keating +# +# 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 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +import pyrpkg +import pyrpkg.cli +import pyrpkg.utils +import os +import sys +import logging +from six.moves import configparser +import argparse +from rpkglib.cli import rpkgClient +from os.path import expanduser + +# Setup an argparser and parse the known commands to get the config file +parser = argparse.ArgumentParser(add_help=False) +parser.add_argument('-C', '--config', help='Specify a config file to use. ' + 'If not specified, ~/.config/rpkg is used and if that ' + 'does not exists, then /etc/rpkg.conf is tried.') + +(args, other) = parser.parse_known_args() + +config_to_use = '/etc/rpkg.conf' +for custom_config in [args.config, expanduser('~/.config/rpkg')]: + if custom_config and os.path.exists(custom_config): + config_to_use = custom_config + break + +# Setup a configuration object and read config file data +config = configparser.SafeConfigParser() +config.read(config_to_use) + +client = rpkgClient(config) +client.do_imports('rpkglib') +client.parse_cmdline() + +if not client.args.path: + try: + client.args.path = pyrpkg.utils.getcwd() + except: + print('Could not get current path, have you deleted it?') + sys.exit(1) + +# setup the logger -- This logger will take things of INFO or DEBUG and +# log it to stdout. Anything above that (WARN, ERROR, CRITICAL) will go +# to stderr. Normal operation will show anything INFO and above. +# Quiet hides INFO, while Verbose exposes DEBUG. In all cases WARN or +# higher are exposed (via stderr). +log = pyrpkg.log +client.setupLogging(log) + +if client.args.v: + log.setLevel(logging.DEBUG) +elif client.args.q: + log.setLevel(logging.WARNING) +else: + log.setLevel(logging.INFO) + +# Run the necessary command +try: + sys.exit(client.args.command()) +except KeyboardInterrupt: + pass +except Exception as e: + log.error('Could not execute %s: %s' % + (client.args.command.__name__, str(e))) + if client.args.v: + raise + sys.exit(1) diff --git a/rpkg-client/rpkg-client.spec b/rpkg-client/rpkg-client.spec new file mode 100644 index 0000000..74d3bb0 --- /dev/null +++ b/rpkg-client/rpkg-client.spec @@ -0,0 +1,122 @@ +Name: rpkg-client +Version: 0.9 +Release: 1%{?dist} +Summary: RPM packaging utitility +License: GPLv2+ +URL: https://pagure.io/rpkg-client.git + +# How to obtain the sources +# git clone https://pagure.io/rpkg-client.git +# cd rpkg-client +# tito build --tgz +Source0: %{name}-%{version}.tar.gz + +BuildArch: noarch + +%description +This package contains the rpkg utility. We are putting +the actual 'rpkg' package into a subpackage because there already exists package +https://admin.fedoraproject.org/pkgdb/package/rpms/rpkg. This package, however, +does not actually produce rpkg rpm, which this package does. + +%package -n rpkg +Summary: RPM packaging utitility +BuildArch: noarch + +BuildRequires: python +BuildRequires: python-setuptools +BuildRequires: python-devel +BuildRequires: python2-rpkg +BuildRequires: python2-mock + +%if 0%{?rhel} +BuildRequires: pytest +%else +BuildRequires: python2-pytest +%endif + +Requires: python2-rpkg + +%description -n rpkg +This is an RPM packaging utility based on python-rpkg library. +It works with both DistGit and standard Git repositories and it handles +packed directory content as well as unpacked content. + +%prep +%setup -q + +%check +./run_tests.sh + +%build +%py2_build +%{__python2} doc/rpkg_man_page.py > rpkg.1 + +%install +%py2_install +install -d %{buildroot}%{_mandir}/man1 +install -p -m 0644 rpkg.1 %{buildroot}%{_mandir}/man1 + +install -d %{buildroot}%{_sysconfdir} +install -d %{buildroot}%{_datarootdir}/bash-completion/completions + +cp -a rpkg.conf %{buildroot}%{_sysconfdir}/ +cp -a rpkg.bash %{buildroot}%{_datarootdir}/bash-completion/completions/ + +%files -n rpkg +%license LICENSE +%{python2_sitelib}/* + +%config(noreplace) %{_sysconfdir}/rpkg.conf +%{_datadir}/bash-completion/completions/rpkg.bash + +%{_bindir}/rpkg +%{_mandir}/*/* + +%changelog +* Mon Oct 16 2017 clime 0.9-1 +- update spec descriptions +- added is-packed subcommand +- try reading ~/.config/rpkg before /etc/rpkg +- add unittests +- for source downloading, try both url formats + with/without hashtype +- add make-source subcommand +- patch srpm to generate Source0 if unpacked content +- override load_ns_module_name to work with any length + namespaces +- added --spec for srpm, make-source, and copr-build +- fixed tagging not to include host dist tag +- docs update +- make all config values optional + +* Thu Jul 27 2017 clime 0.8-1 +- fix man pages to only include actually provided part of pyrpkg functionality +- add rpkglib to provide functional interface +- change summary of wrapper package + +* Wed Jul 26 2017 clime 0.7-1 +- use %%py2_build and %%py2_install macros +- explicitly invoke python2 for doc generation +- remove no longer needed $BUILDROOT removal in %%install clause +- add missing BuildRequires on python-setuptools + +* Fri Jul 07 2017 clime 0.6-1 +- fix build error + +* Tue Jun 27 2017 clime 0.5-1 +- remove Requires bash-completion + +* Tue Jun 27 2017 clime 0.4-1 +- move config file to /etc/rpkg.conf +- add Requires bash-completion + +* Tue Jun 27 2017 clime 0.3-1 +- remove some directories from %%files in .spec +- add (for now) short README.md + +* Tue Jun 20 2017 clime 0.2-1 +- new rpkg-client package built with tito + +* Mon Jun 12 2017 clime 0.1-1 +- Initial version diff --git a/rpkg-client/rpkg.bash b/rpkg-client/rpkg.bash new file mode 100644 index 0000000..8a14b40 --- /dev/null +++ b/rpkg-client/rpkg.bash @@ -0,0 +1,321 @@ +# rpkg bash completion + +_rpkg() +{ + COMPREPLY=() + + in_array() + { + local i + for i in $2; do + [[ $i = $1 ]] && return 0 + done + return 1 + } + + _filedir_exclude_paths() + { + _filedir "$@" + for ((i=0; i<=${#COMPREPLY[@]}; i++)); do + [[ ${COMPREPLY[$i]} =~ /?\.git/? ]] && unset COMPREPLY[$i] + done + } + + local cur prev + # _get_comp_words_by_ref is in bash-completion >= 1.2, which EL-5 lacks. + if type _get_comp_words_by_ref &>/dev/null; then + _get_comp_words_by_ref cur prev + else + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + fi + + # global options + + local options="--help -v -q" + local options_value="--dist --release --user --path" + local commands="build chain-build ci clean clog clone co container-build container-build-config commit compile copr-build diff gimmespec giturl help \ + gitbuildhash import install lint local mockbuild mock-config new new-sources patch prep pull push scratch-build sources \ + srpm switch-branch tag unused-patches upload verify-files verrel" + + # parse main options and get command + + local command= + local command_first= + local path= + + local i w + for (( i = 0; i < ${#COMP_WORDS[*]} - 1; i++ )); do + w="${COMP_WORDS[$i]}" + # option + if [[ ${w:0:1} = - ]]; then + if in_array "$w" "$options_value"; then + ((i++)) + [[ "$w" = --path ]] && path="${COMP_WORDS[$i]}" + fi + # command + elif in_array "$w" "$commands"; then + command="$w" + command_first=$((i+1)) + break + fi + done + + # complete base options + + if [[ -z $command ]]; then + if [[ $cur == -* ]]; then + COMPREPLY=( $(compgen -W "$options $options_value" -- "$cur") ) + return 0 + fi + + case "$prev" in + --config) + _filedir_exclude_paths + ;; + --dist) + ;; + --user|-u) + ;; + --path) + _filedir_exclude_paths + ;; + *) + COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) + ;; + esac + + return 0 + fi + + # parse command specific options + + local options= + local options_target= options_arches= options_branch= options_string= options_file= options_dir= options_srpm= + local after= after_more= + + case $command in + help|gimmespec|gitbuildhash|giturl|lint|new|unused-patches|verrel) + ;; + build) + options="--nowait --background --skip-tag --scratch --md5" + options_arches="--arches" + options_srpm="--srpm" + options_target="--target" + ;; + chain-build) + options="--nowait --background" + options_target="--target" + after="package" + after_more=true + ;; + clean) + options="--dry-run -x" + ;; + clog) + options="--raw" + ;; + clone|co) + options="--branches --anonymous" + options_branch="-b" + after="package" + ;; + container-build) + options="--scratch --nowait" + options_target="--target" + options_string="--repo-url" + ;; + container-build-config) + options="--get-autorebuild" + options_bool="--set-autorebuild" + ;; + commit|ci) + options="--push --clog --raw --tag" + options_string="--message" + options_file="--file" + after="file" + after_more=true + ;; + compile|install) + options="--short-circuit --nocheck" + options_arch="--arch" + options_dir="--builddir" + ;; + copr-build) + options="--nowait" + after="package" + after_more=true + ;; + diff) + options="--cached" + after="file" + after_more=true + ;; + import) + options="--create" + options_branch="--branch" + after="srpm" + ;; + lint) + options="--info" + options_file="--rpmlintconf" + ;; + local) + options="--md5" + options_arch="--arch" + options_dir="--builddir" + ;; + mock-config) + options="--target" + options_arch="--arch" + ;; + mockbuild) + options="--md5 --no-clean --no-cleanup-after --no-clean-all" + options_mroot="--root" + ;; + patch) + options="--rediff" + options_string="--suffix" + ;; + prep|verify-files) + options_arch="--arch" + options_dir="--builddir" + ;; + pull) + options="--rebase --no-rebase" + ;; + push) + options="--force" + ;; + scratch-build) + options="--nowait --background --md5" + options_target="--target" + options_arches="--arches" + options_srpm="--srpm" + ;; + sources) + options_dir="--outdir" + ;; + srpm) + options="--md5" + ;; + switch-branch) + options="--list" + after="branch" + ;; + tag) + options="--clog --raw --force --list --delete" + options_string="--message" + options_file="--file" + after_more=true + ;; + upload|new-sources) + after="file" + after_more=true + ;; + esac + + local all_options="--help $options" + local all_options_value="$options_target $options_arches $options_branch $options_string $options_file $options_dir $options_srpm $options_bool" + + # count non-option parameters + + local i w + local last_option= + local after_counter=0 + for (( i = $command_first; i < ${#COMP_WORDS[*]} - 1; i++)); do + w="${COMP_WORDS[$i]}" + if [[ ${w:0:1} = - ]]; then + if in_array "$w" "$all_options"; then + last_option="$w" + continue + elif in_array "$w" "$all_options_value"; then + last_option="$w" + ((i++)) + continue + fi + fi + in_array "$last_option" "$options_arches" || ((after_counter++)) + done + + # completion + + if [[ -n $options_target ]] && in_array "$prev" "$options_target"; then + COMPREPLY=( $(compgen -W "$(_rpkg_target)" -- "$cur") ) + + elif [[ -n $options_arches ]] && in_array "$last_option" "$options_arches"; then + COMPREPLY=( $(compgen -W "$(_rpkg_arch) $all_options" -- "$cur") ) + + elif [[ -n $options_srpm ]] && in_array "$prev" "$options_srpm"; then + _filedir_exclude_paths "*.src.rpm" + + elif [[ -n $options_branch ]] && in_array "$prev" "$options_branch"; then + COMPREPLY=( $(compgen -W "$(_rpkg_branch "$path")" -- "$cur") ) + + elif [[ -n $options_file ]] && in_array "$prev" "$options_file"; then + _filedir_exclude_paths + + elif [[ -n $options_dir ]] && in_array "$prev" "$options_dir"; then + _filedir_exclude_paths -d + + elif [[ -n $options_bool ]] && in_array "$prev" "$options_bool"; then + COMPREPLY=( $(compgen -W "true false" -- "$cur") ) + + elif [[ -n $options_string ]] && in_array "$prev" "$options_string"; then + COMPREPLY=( ) + + else + local after_options= + + if [[ $after_counter -eq 0 ]] || [[ $after_more = true ]]; then + case $after in + file) _filedir_exclude_paths ;; + srpm) _filedir_exclude_paths "*.src.rpm" ;; + branch) after_options="$(_rpkg_branch "$path")" ;; + package) after_options="$(_rpkg_package "$cur")";; + esac + fi + + if [[ $cur != -* ]]; then + all_options= + all_options_value= + fi + + COMPREPLY+=( $(compgen -W "$all_options $all_options_value $after_options" -- "$cur" ) ) + fi + + return 0 +} && +complete -F _rpkg rpkg + +_rpkg_target() +{ + koji list-targets --quiet 2>/dev/null | cut -d" " -f1 +} + +_rpkg_arch() +{ + echo "i386 x86_64 ppc ppc64 s390 s390x sparc sparc64" +} + +_rpkg_branch() +{ + local git_options= format="--format %(refname:short)" + [[ -n $1 ]] && git_options="--git-dir=$1/.git" + + git $git_options for-each-ref $format 'refs/remotes' | sed 's,.*/,,' + git $git_options for-each-ref $format 'refs/heads' +} + +_rpkg_package() +{ + repoquery -C --qf=%{sourcerpm} "$1*" 2>/dev/null | sort -u | sed -r 's/(-[^-]*){2}\.src\.rpm$//' +} + +# Local variables: +# mode: shell-script +# sh-basic-offset: 4 +# sh-indent-comment: t +# indent-tabs-mode: nil +# End: +# ex: ts=4 sw=4 et filetype=sh diff --git a/rpkg-client/rpkg.conf b/rpkg-client/rpkg.conf new file mode 100644 index 0000000..9bd84ab --- /dev/null +++ b/rpkg-client/rpkg.conf @@ -0,0 +1,5 @@ +[rpkg] +lookaside = http://localhost/repo/pkgs +lookaside_cgi = https://localhost/repo/pkgs/upload.cgi +gitbaseurl = ssh://%(user)s@localhost/%(module)s +anongiturl = git://localhost/%(module)s diff --git a/rpkg-client/rpkglib/__init__.py b/rpkg-client/rpkglib/__init__.py new file mode 100644 index 0000000..9e4a356 --- /dev/null +++ b/rpkg-client/rpkglib/__init__.py @@ -0,0 +1,227 @@ +import os +import rpm +import shutil +import re + +import pyrpkg +from pyrpkg.utils import cached_property +from pyrpkg.errors import rpkgError +from pyrpkg.sources import SourcesFile + +from rpkglib.lookaside import CGILookasideCache +from rpkglib import utils + +from exceptions import NotUnpackedException, RpmSpecParseException, NoSourceZeroException + +class Commands(pyrpkg.Commands): + def __init__(self, *args, **kwargs): + """Init the object and some configuration details.""" + super(Commands, self).__init__(*args, **kwargs) + self.source_entry_type = 'bsd' + self.distgit_namespaced = True + self.lookaside_namespaced = True + self._ns_module_name = None + + def load_rpmdefines(self): + """Populate rpmdefines""" + self._rpmdefines = [ + "--define '_sourcedir %s'" % self.path, + "--define '_specdir %s'" % self.path, + "--define '_builddir %s'" % self.path, + "--define '_srcrpmdir %s'" % self.path, + "--define '_rpmdir %s'" % self.path, + ] + + @cached_property + def lookasidecache(self): + return CGILookasideCache( + self.lookasidehash, self.lookaside, self.lookaside_cgi, + client_cert=self.cert_file, ca_cert=self.ca_cert) + + @property + def ns_module_name(self): + if not self._ns_module_name: + self.load_ns_module_name() + return self._ns_module_name + + def load_ns_module_name(self): + """Loads the namespace module name""" + try: + replacements = {'user': self.user, 'module':'(.*)/?'} + gitbaseurl_pattern = self.gitbaseurl%replacements + '$' + anongiturl_pattern = self.anongiturl%replacements + '$' + + match = re.match(gitbaseurl_pattern, self.push_url) + if not match: + match = re.match(anongiturl_pattern, self.push_url) + + if match: + ns_module_name = match.group(1) + if ns_module_name.endswith('.git'): + ns_module_name = ns_module_name[:-len('.git')] + self._ns_module_name = ns_module_name + return + except rpkgError: + pass + + self._ns_module_name = self.module_name + + def sources(self, outdir=None): + """Download source files""" + if not os.path.exists(self.sources_filename): + return + + # Default to putting the files where the module is + if not outdir: + outdir = self.path + + sourcesf = SourcesFile(self.sources_filename, self.source_entry_type) + + for entry in sourcesf.entries: + outfile = os.path.join(outdir, entry.file) + self.lookasidecache.download( + self.ns_module_name, + entry.file, entry.hash, outfile, + hashtype=entry.hashtype) + + def srpm(self, outdir=None): + """Create an srpm using hashtype from content in the module + + Requires sources already downloaded. + """ + + self.srpmname = os.path.join(self.path, "%s-%s-%s.src.rpm" + % (self.module_name, self.ver, self.rel)) + + # See if we need to build the srpm + if os.path.exists(self.srpmname): + self.log.debug('Srpm found, rewriting it.') + + cmd = ['rpmbuild'] + cmd.extend(self.rpmdefines) + if self.quiet: + cmd.append('--quiet') + # This may need to get updated if we ever change our checksum default + if not self.hashtype == 'sha256': + cmd.extend(["--define '_source_filedigest_algorithm %s'" + % self.hashtype, + "--define '_binary_filedigest_algorithm %s'" + % self.hashtype]) + if outdir: + cmd.extend(["--define '_srcrpmdir %s'" % outdir]) + + cmd.extend(['--nodeps', '-bs', os.path.join(self.path, self.spec)]) + self._run_command(cmd, shell=True) + + def is_unpacked(self, dirpath, rpm_sources): + """ + Decide, whether we are dealing with "unpacked" + or "packed" type of source content at the + given dirpath. + + "packed": does not contain anything else + except ignored files or contains + at least one source referenced + by the given specfile + + "unpacked": is not "packed", meaning that + it contains at least one non-ignored + file and contains no file referenced + by the given specfile as a source + + "source" in these definitions is a filename + specified in a Source or Patch .spec directive + + NOTE: + + This criterion needs source list parsed from + a specfile to make the decision, which makes + it potentially dependant on the given environment + where it is executed. You can try to avoid this + dependency e.g. by not using Patch and Source + spec directives inside conditionals and not + using environment-dependant rpmmacros in Patch + and Source definitions. + + :param str dirpath: filesystem path to a directory + with the source content + :param list rpm_sources: list of tuples describing + rpm sources + + :returns True if the directory content is of the + unpacked type, False otherwise + """ + for (filepath, num, flags) in rpm_sources: + filename = os.path.basename(filepath) + + local_filepath = os.path.join(dirpath, filename) + if os.path.isfile(local_filepath): + return False + + ignore_file_regex = '(^README|.spec$|^\.|^tito.props$|^sources$)' + ignored_file_filter = lambda f: not re.search( + ignore_file_regex, f, re.IGNORECASE) + + if not list(filter(ignored_file_filter, os.listdir(dirpath))): + return False + + return True + + def make_source(self, destdir=None): + """ + Create source mentioned according to Source0 spec + directive from an unpacked repository. Does nothing + on a packed repo. + + NOTE: + + This method calls rpm's parseSpec, evaluation + of which is heavily dependant on the environment + (e.g. currently defined macros under /usr/lib/rpm + and ~/.rpmmacros), where it is being executed. + + Therefore, it should be called in a clean environment + of the target rpm distribution and architecture. + + Note that if you invoke this method directly on your + host system, you need to trust the provided spec file. + When specfile is parsed, %() constructs get evaluated + and any system command can be executed from there. + The behaviour is the same as if you called rpmbuild + directly in your system. + + :param str destdir: where to put the generated sources + + :returns path to the packed archive (alias Source0) + """ + spec_path = os.path.join(self.path, self.spec) + + ts = rpm.ts() + try: + rpm.addMacro("_sourcedir", self.path) + rpm_spec = ts.parseSpec(spec_path) + except ValueError as e: + raise RpmSpecParseException(str(e)) + + if not self.is_unpacked(self.path, rpm_spec.sources): + raise NotUnpackedException("Not an unpacked content.") + + source_zero_name = utils.find_source_zero(rpm_spec.sources) + if not source_zero_name: + raise NoSourceZeroException("Source zero not found") + + target_source_path = os.path.join( + destdir or self.path, source_zero_name) + + name = rpm.expandMacro("%{name}") + version = rpm.expandMacro("%{version}") + rpm.reloadConfig() + + packed_dir_name = name + '-' + version + utils.pack_sources( + self.path, + target_source_path, + packed_dir_name + ) + self.log.info('Wrote: {}'.format(target_source_path)) + return target_source_path diff --git a/rpkg-client/rpkglib/cli.py b/rpkg-client/rpkglib/cli.py new file mode 100644 index 0000000..49674b0 --- /dev/null +++ b/rpkg-client/rpkglib/cli.py @@ -0,0 +1,245 @@ +import argparse +import os +import rpm + +from pyrpkg.cli import cliClient +from pyrpkg import utils + +from exceptions import NotUnpackedException, RpmSpecParseException + +class rpkgClient(cliClient): + def __init__(self, config, name=None): + self.DEFAULT_CLI_NAME = 'rpkg' + super(rpkgClient, self).__init__(config, name) + + def setup_argparser(self): + """Setup the argument parser and register some basic commands.""" + + self.parser = argparse.ArgumentParser( + prog=self.name, + epilog='For detailed help pass --help to a target') + + # Add some basic arguments that should be used by all. + # Add a config file + self.parser.add_argument('--config', '-C', + default=None, + help='Specify a config file to use') + # Allow forcing the package name + self.parser.add_argument('--module-name', + help='Override the module name. Otherwise' + ' it is discovered from: Git push URL' + ' or Git URL. ') + # Override the discovered user name + self.parser.add_argument('--user', default=None, + help='Override the discovered user name') + # Let the user define a path to work in rather than cwd + self.parser.add_argument('--path', default=None, + type=utils.u, + help='Define the directory to work in ' + '(defaults to cwd)') + # Verbosity + self.parser.add_argument('--verbose', '-v', dest='v', + action='store_true', + help='Run with verbose debug output') + self.parser.add_argument('--debug', '-d', dest='debug', + action='store_true', + help='Run with debug output') + self.parser.add_argument('-q', action='store_true', + help='Run quietly only displaying errors') + + def setup_subparsers(self): + """Setup basic subparsers that all clients should use""" + + # Setup some basic shared subparsers + + # help command + self.register_help() + + # Add a common parsers + self.register_rpm_common() + + # Other targets + self.register_make_source() + self.register_clean() + self.register_clog() + self.register_clone() + self.register_copr_build() + self.register_commit() + self.register_compile() + self.register_diff() + self.register_gimmespec() + self.register_giturl() + self.register_import_srpm() + self.register_install() + self.register_is_packed() + self.register_lint() + self.register_local() + self.register_new() + self.register_new_sources() + self.register_patch() + self.register_prep() + self.register_pull() + self.register_push() + self.register_sources() + self.register_srpm() + self.register_switch_branch() + self.register_tag() + self.register_unused_patches() + self.register_upload() + self.register_verify_files() + self.register_verrel() + + def load_cmd(self): + """This sets up the cmd object""" + + # load items from the config file + items = dict(self.config.items(self.name, raw=True)) + + # Read comma separated list of kerberos realms + realms = [realm + for realm in items.get("kerberos_realms", '').split(',') + if realm] + + # Create the cmd object + self._cmd = self.site.Commands(self.args.path, + items.get('lookaside'), + items.get('lookasidehash', 'sha512'), + items.get('lookaside_cgi'), + items.get('gitbaseurl', ''), + items.get('anongiturl', ''), + branchre='.*', + kojiconfig='', + build_client=None, + user=self.args.user, + quiet=self.args.q, + realms=realms + ) + + self._cmd.module_name = self.args.module_name + self._cmd.debug = self.args.debug + self._cmd.verbose = self.args.v + self._cmd.clone_config = items.get('clone_config') + + def register_make_source(self): + make_source_parser = self.subparsers.add_parser( + 'make-source', help='Create Source0 from the ' + 'content of the current working directory ' + 'after downloading any external sources. ' + 'The content must be of unpacked type.', + description='Puts content of the current ' + 'working directory into a gzip-compressed archive named ' + 'according to Source0 filename as specfied in the .spec file. ' + 'The content must be of unpacked type, otherwise no action is taken. ' + 'Unpacked content is such that it contains a .spec file ' + 'that references no present source or patch ' + '(typically it contains only Source0 being ' + 'generated automatically) and there is at least ' + 'one file not in the list of ignored content (README, ' + 'README.md, sources, tito.props, hidden files, ' + '.spec file). Note that by invoking this command ' + 'with --outdir ., the directory content becomes ' + '"packed".') + make_source_parser.add_argument( + '--spec', action='store', default=None, + help='Path to the spec file. By default .spec file ' + 'is autodiscovered.') + make_source_parser.add_argument( + '--outdir', default=os.getcwd(), + help='Where to put the generated source. ' + 'By default cwd.') + make_source_parser.set_defaults(command=self.make_source) + + def tag(self): + self.cmd._rpmdefines = self.cmd.rpmdefines + ["--define 'dist %nil'"] + super(rpkgClient, self).tag() + + def make_source(self): + self.cmd.sources() + self.cmd._spec = self.args.spec + self.cmd.make_source(self.args.outdir) + + def srpm(self): + self.cmd.sources() + self.cmd._spec = self.args.spec + try: + self.cmd.make_source() + except NotUnpackedException: + pass + self.cmd.srpm(self.args.outdir) + + def copr_build(self): + self.args.outdir = None + super(rpkgClient, self).copr_build() + + def is_packed(self): + self.cmd._spec = self.args.spec + ts = rpm.ts() + try: + rpm.addMacro("_sourcedir", self.cmd.path) + rpm_spec = ts.parseSpec(self.cmd.spec) + except ValueError as e: + raise RpmSpecParseException(str(e)) + + if self.cmd.is_unpacked(self.cmd.path, rpm_spec.sources): + self.log.info('No') + else: + self.log.info('Yes') + + def register_srpm(self): + """Register the srpm target""" + srpm_parser = self.subparsers.add_parser( + 'srpm', help='Create a source rpm', + description='Create a source rpm out of ' + 'packed or unpacked content. See ' + 'make-sources for the description of the ' + 'two content types and their recognition.') + srpm_parser.add_argument( + '--spec', action='store', default=None, + help='Path to the spec file. By default .spec file ' + 'is autodiscovered.') + srpm_parser.add_argument( + '--outdir', default=os.getcwd(), + help='Where to put the generated srpm.') + srpm_parser.set_defaults(command=self.srpm) + + def register_is_packed(self): + """Determine whether the package content is packed or not""" + is_packed_parser = self.subparsers.add_parser( + 'is-packed', help='Tell user whether content is packed', + description='Determine whether the package content ' + 'in the working directory is packed or unpacked ' + 'and print that information to the screen.') + is_packed_parser.add_argument( + '--spec', action='store', default=None, + help='Path to an alternative spec file. Note that ' + 'whether the content is packed on unpacked depends ' + 'also on Source and Patch definitions in the spec ' + 'file as well as on the actual content in the ' + 'working directory.') + is_packed_parser.set_defaults(command=self.is_packed) + + def register_copr_build(self): + """Register the copr-build target""" + copr_parser = self.subparsers.add_parser( + 'copr-build', help='Build package in COPR', + formatter_class=argparse.RawDescriptionHelpFormatter, + description=""" + Build package in COPR. + + Note: you need to have set up correct api key. For more information + see API KEY section of copr-cli(1) man page. + """) + copr_parser.add_argument( + '--spec', action='store', default=None, + help='Path to the spec file. By default .spec file ' + 'is autodiscovered.') + copr_parser.add_argument( + '--config', required=False, + metavar='CONFIG', dest='copr_config', + help="Path to an alternative Copr configuration file") + copr_parser.add_argument( + '--nowait', action='store_true', default=False, + help="Don't wait on build") + copr_parser.add_argument( + 'project', nargs=1, help='Name of the project in format USER/PROJECT') + copr_parser.set_defaults(command=self.copr_build) diff --git a/rpkg-client/rpkglib/exceptions.py b/rpkg-client/rpkglib/exceptions.py new file mode 100644 index 0000000..7cc15d0 --- /dev/null +++ b/rpkg-client/rpkglib/exceptions.py @@ -0,0 +1,12 @@ + +class RpmSpecParseException(Exception): + pass + +class NotUnpackedException(Exception): + pass + +class NoSourceZeroException(Exception): + pass + +class SourceArchiveAlreadyExists(Exception): + pass diff --git a/rpkg-client/rpkglib/lookaside.py b/rpkg-client/rpkglib/lookaside.py new file mode 100644 index 0000000..a1ccb6b --- /dev/null +++ b/rpkg-client/rpkglib/lookaside.py @@ -0,0 +1,34 @@ +import requests +import pyrpkg.lookaside + + +class CGILookasideCache(pyrpkg.lookaside.CGILookasideCache): + """A class to interact with a CGI-based lookaside cache""" + def __init__(self, hashtype, download_url, upload_url, + client_cert=None, ca_cert=None): + super(CGILookasideCache, self).__init__(hashtype, download_url, upload_url, + client_cert=client_cert,ca_cert=ca_cert) + + self.old_download_path = '%(name)s/%(filename)s/%(hash)s/%(filename)s' + self.new_download_path = '%(name)s/%(filename)s/%(hashtype)s/%(hash)s/%(filename)s' + + def download(self, name, filename, hash, outfile, hashtype=None, **kwargs): + original_download_path = self.download_path + urled_file = filename.replace(' ', '%20') + path_dict = {'name': name, 'filename': urled_file, 'hash': hash, + 'hashtype': hashtype} + path_dict.update(kwargs) + + for download_path in [self.old_download_path, self.new_download_path]: + path = download_path % path_dict + url = '%s/%s' % (self.download_url, path) + response = requests.head(url) + self.log.debug("URL %s returned status %s" % (url, response.status_code)) + if response.status_code == 200: + self.log.debug("This URL seems to be correct, using it") + self.download_path = download_path + break + + result = super(CGILookasideCache, self).download(name, filename, hash, outfile, hashtype=hashtype, **kwargs) + self.download_path = original_download_path + return result diff --git a/rpkg-client/rpkglib/utils.py b/rpkg-client/rpkglib/utils.py new file mode 100644 index 0000000..f456b46 --- /dev/null +++ b/rpkg-client/rpkglib/utils.py @@ -0,0 +1,51 @@ +import logging +import os +import re +import shutil +import tarfile + +from exceptions import SourceArchiveAlreadyExists + +log = logging.getLogger("__main__") + + +def pack_sources(dir_to_pack, target_path, pack_dir_as): + """ + Create a gzipped tar archive from the given directory. + + :param str dir_to_pack: directory to be packed + :param str target_path: path to the resulting archive + :param str pack_dir_as: packed directory name inside the archive + """ + if os.path.exists(target_path): + raise SourceArchiveAlreadyExists("{} already exists" + .format(target_path)) + + log.debug("Packing {} as {} into {}...".format( + dir_to_pack, pack_dir_as, target_path)) + + def exclude(tar_info): + exclude_git_pattern = r'(/.git$|/.git/|/.gitignore$)' + if re.search(exclude_git_pattern, tar_info.name): + log.debug("Excluding {}".format(tar_info.name)) + return None + return tar_info + + tarball = tarfile.open(target_path, 'w:gz') + tarball.add(dir_to_pack, pack_dir_as, filter=exclude) + tarball.close() + + +def find_source_zero(rpm_sources): + """ + For the given list of rpm_sources, + return filename of the Source0. + + :param list rpm_sources + + :returns str: filename of Source0 or None + """ + for (filepath, num, flags) in rpm_sources: + if num == 0 and flags == 1: + return os.path.basename(filepath) + return None diff --git a/rpkg-client/run_tests.sh b/rpkg-client/run_tests.sh new file mode 100755 index 0000000..363eee0 --- /dev/null +++ b/rpkg-client/run_tests.sh @@ -0,0 +1,2 @@ +#!/bin/sh +PYTHONPATH=.:$PYTHONPATH python2 -m pytest tests -s $@ diff --git a/rpkg-client/setup.py b/rpkg-client/setup.py new file mode 100755 index 0000000..dd26058 --- /dev/null +++ b/rpkg-client/setup.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +import rpm +from setuptools import setup, find_packages + +spec_file = rpm.ts().parseSpec('rpkg-client.spec') + +setup( + name=spec_file.sourceHeader.name.decode("utf-8"), + version=spec_file.sourceHeader.version.decode("utf-8"), + description=spec_file.sourceHeader.summary.decode("utf-8"), + long_description=spec_file.sourceHeader.description.decode("utf-8"), + author='clime', + author_email='clime@redhat.com', + download_url='https://pagure.io/rpkg-client.git', + license='GPLv2+', + classifiers=[ + "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Build Tools", + ], + packages=find_packages(), + scripts=['rpkg'], + include_package_data=True, +) diff --git a/rpkg-client/tests/__init__.py b/rpkg-client/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/rpkg-client/tests/__init__.py diff --git a/rpkg-client/tests/base.py b/rpkg-client/tests/base.py new file mode 100644 index 0000000..44f37f6 --- /dev/null +++ b/rpkg-client/tests/base.py @@ -0,0 +1,46 @@ +import os +import unittest +import shutil +import rpm +import tempfile + +from spec_templates import SPEC_TEMPLATE + +class TestCase(unittest.TestCase): + def setUp(self): + self.tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.tmpdir) + + def dump_spec(self, template, pkgname='testpkg', **kwargs): + spec_content = template.substitute(kwargs, pkgname=pkgname) + spec_path = os.path.join( + self.tmpdir, '{}.spec'.format(pkgname)) + spec_file = open(spec_path, 'w') + spec_file.write(spec_content) + spec_file.close() + return spec_path + + def touch_file(self, filename, subdir=None): + if subdir: + dirpath = os.path.join(self.tmpdir, subdir) + os.makedirs(dirpath) + else: + dirpath = self.tmpdir + filepath = os.path.join(dirpath, filename) + open(filepath, 'w').close() + return filepath + + def get_parsed_spec(self, spec_path): + ts = rpm.ts() + rpm.addMacro("_sourcedir", self.tmpdir) + return ts.parseSpec(spec_path) + + def make_packed_content(self): + spec_path = self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + self.touch_file('source0.tar.gz') + + def make_unpacked_content(self): + spec_path = self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + self.touch_file('foobar.py') diff --git a/rpkg-client/tests/spec_templates.py b/rpkg-client/tests/spec_templates.py new file mode 100644 index 0000000..da3de2a --- /dev/null +++ b/rpkg-client/tests/spec_templates.py @@ -0,0 +1,51 @@ +from string import Template + +SPEC_TEMPLATE = Template(""" +Name: $pkgname +Version: 1 +Release: 1 +Summary: This is a test package. + +License: GPLv2+ +URL: https://someurl.org +Source0: $source0 + +%description +""") + +SPEC_WITH_PATCH_TEMPLATE = Template(""" +Name: $pkgname +Version: 1 +Release: 1 +Summary: This is a test package. + +License: GPLv2+ +URL: https://someurl.org +Source0: $source0 + +Patch0: $patch0 + +%description +""") + +INVALID_SPEC_TEMPLATE = Template(""" +Name: $pkgname +Version: 1 +Release: 1 +Summary: This is a test package. + +License: GPLv2+ +URL: https://someurl.org +""") + +NO_SOURCE_ZERO_SPEC_TEMPLATE = Template(""" +Name: $pkgname +Version: 1 +Release: 1 +Summary: This is a test package. + +License: GPLv2+ +URL: https://someurl.org + +%description +""") diff --git a/rpkg-client/tests/test_cli.py b/rpkg-client/tests/test_cli.py new file mode 100644 index 0000000..746aefd --- /dev/null +++ b/rpkg-client/tests/test_cli.py @@ -0,0 +1,131 @@ +import unittest +import rpkglib +import six +import base +import os +import glob +import tempfile + +from six.moves import configparser +from rpkglib.cli import rpkgClient +from rpkglib.exceptions import NotUnpackedException + +from spec_templates import SPEC_TEMPLATE + +if six.PY3: + from unittest import mock + from unittest.mock import MagicMock +else: + import mock + from mock import MagicMock + +RPKG_CONFIG = """ +[rpkg] +lookaside = http://localhost/repo/pkgs +lookaside_cgi = https://localhost/repo/pkgs/upload.cgi +gitbaseurl = ssh://%(user)s@localhost/%(module)s +anongiturl = git://localhost/%(module)s +""" + +class TestCli(base.TestCase): + def setUp(self): + super(TestCli, self).setUp() + config_fd, self.config_path = tempfile.mkstemp() + config_file = os.fdopen(config_fd, 'w+') + config_file.write(RPKG_CONFIG) + config_file.close() + + config = configparser.SafeConfigParser() + config.read(self.config_path) + + self.client = rpkgClient(config, name='rpkg') + self.client.do_imports('rpkglib') + self.client.args = MagicMock(user='user', q='q', path=self.tmpdir) + + def tearDown(self): + os.unlink(self.config_path) + super(TestCli, self).tearDown() + + @mock.patch('rpkglib.Commands') + def test_load_cmd(self, cmds): + self.client.load_cmd() + cmds.assert_called_with( + self.tmpdir, + 'http://localhost/repo/pkgs', + 'sha512', + 'https://localhost/repo/pkgs/upload.cgi', + 'ssh://%(user)s@localhost/%(module)s', + 'git://localhost/%(module)s', + branchre='.*', + kojiconfig='', + build_client=None, + user='user', + quiet='q', + realms=[], + ) + + def test_make_source_from_packed_raises(self): + self.make_packed_content() + self.client.args.spec = '' + with self.assertRaises(NotUnpackedException): + self.client.make_source() + + def test_make_source_from_unpacked(self): + self.make_unpacked_content() + self.client.args.outdir = self.tmpdir + self.client.args.spec = '' + self.client.make_source() + self.assertTrue(glob.glob('{}/{}'.format(self.tmpdir, '*.tar.gz'))) + + def test_unpacked_changes_into_packed_by_make_sources(self): + self.make_unpacked_content() + self.client.args.outdir = self.tmpdir + self.client.args.spec = '' + self.client.make_source() + self.assertTrue(glob.glob('{}/{}'.format(self.tmpdir, '*.tar.gz'))) + with self.assertRaises(NotUnpackedException): + self.client.make_source() + + def test_make_srpm_from_packed(self): + self.make_packed_content() + dircontent = os.listdir(self.tmpdir) + self.client.args.spec = '' + self.client.args.outdir = self.tmpdir + self.client.srpm() + self.assertItemsEqual( + os.listdir(self.tmpdir), + dircontent+['testpkg-1-1.src.rpm']) + + def test_make_srpm_from_unpacked(self): + self.make_unpacked_content() + dircontent = os.listdir(self.tmpdir) + self.client.args.spec = '' + self.client.args.outdir = self.tmpdir + self.client.srpm() + self.assertItemsEqual( + os.listdir(self.tmpdir), + dircontent+['testpkg-1-1.src.rpm', 'source0.tar.gz']) + + def test_make_srpm_multiple_specs(self): + spec1_path = self.dump_spec( + SPEC_TEMPLATE, pkgname='testpkg1', source0='source0.tar.gz') + spec2_path = self.dump_spec( + SPEC_TEMPLATE, pkgname='testpkg2', source0='source0.tar.gz') + self.touch_file('source0.tar.gz') + dircontent = os.listdir(self.tmpdir) + + self.client.args.outdir = self.tmpdir + + self.client.args.spec = spec1_path + self.client.srpm() + self.assertItemsEqual( + os.listdir(self.tmpdir), + dircontent+['testpkg1-1-1.src.rpm']) + + os.unlink(os.path.join(self.tmpdir, 'testpkg1-1-1.src.rpm')) + + self.client.args.spec = spec2_path + self.client.srpm() + self.assertItemsEqual( + os.listdir(self.tmpdir), + dircontent+['testpkg2-1-1.src.rpm']) diff --git a/rpkg-client/tests/test_cmd.py b/rpkg-client/tests/test_cmd.py new file mode 100644 index 0000000..99d5334 --- /dev/null +++ b/rpkg-client/tests/test_cmd.py @@ -0,0 +1,189 @@ +import unittest +import os +import tarfile +import six +import git + +import base +import rpkglib +from rpkglib.exceptions import NotUnpackedException, RpmSpecParseException,\ + NoSourceZeroException +from rpkglib.utils import find_source_zero +from spec_templates import SPEC_TEMPLATE, SPEC_WITH_PATCH_TEMPLATE,\ + INVALID_SPEC_TEMPLATE, NO_SOURCE_ZERO_SPEC_TEMPLATE + +if six.PY3: + from unittest import mock + from unittest.mock import MagicMock +else: + import mock + from mock import MagicMock + +class TestCommands(base.TestCase): + def setUp(self): + super(TestCommands, self).setUp() + self.cmd = rpkglib.Commands(self.tmpdir, + 'lookaside', + 'lookasidehash', + 'lookaside_cgi', + 'ssh://someuser@copr-dist-git.fedorainfracloud.org/%(module)s', + 'http://copr-dist-git.fedorainfracloud.org/git/%(module)s', + branchre='.*', + kojiconfig='', + build_client=None) + + def tearDown(self): + super(TestCommands, self).tearDown() + + def test_load_ns_module_name_on_basegiturl(self): + repo = git.Repo.init(self.tmpdir) + repo.create_remote('origin', 'ssh://someuser@copr-dist-git.fedorainfracloud.org/a/b/c.git') + self.cmd.load_ns_module_name() + self.assertEquals(self.cmd.ns_module_name, 'a/b/c') + + def test_load_ns_module_name_on_anongiturl(self): + repo = git.Repo.init(self.tmpdir) + repo.create_remote('origin', 'http://copr-dist-git.fedorainfracloud.org/git/a/b/c') + self.cmd.load_ns_module_name() + self.assertEquals(self.cmd.ns_module_name, 'a/b/c') + + @mock.patch("rpkglib.SourcesFile") + def test_sources_empty_sources(self, sources_file): + self.touch_file('sources') + self.cmd.lookasidecache.download = MagicMock() + self.cmd.sources() + sources_file.assert_called_with('{}/{}'.format(self.tmpdir, 'sources'), 'bsd') + self.cmd.lookasidecache.download.assert_not_called() + + def test_sources_new_format(self): + sources_path = os.path.join(self.tmpdir, 'sources') + sources = open(sources_path, 'w') + sources.write('SHA512 (copr-rpmbuild-0.6.tar.gz) = ' + '7f6239543c2104443b6409fc3033f939f77e64e297399ef9831e77156ac42f6545b680fa35e99a575ca812e5ba0f17c999bfba6d0b783471fa4515182c4ba313') + sources.close() + + # create repo for ns_module_name determining + repo = git.Repo.init(self.tmpdir) + repo.create_remote('origin', 'http://copr-dist-git.fedorainfracloud.org/git/testpkg') + + self.cmd.lookasidecache.download = MagicMock() + self.cmd.sources() + self.cmd.lookasidecache.download.assert_called_once_with( + 'testpkg', + 'copr-rpmbuild-0.6.tar.gz', + '7f6239543c2104443b6409fc3033f939f77e64e297399ef9831e77156ac42f6545b680fa35e99a575ca812e5ba0f17c999bfba6d0b783471fa4515182c4ba313', + '{}/{}'.format(self.tmpdir, 'copr-rpmbuild-0.6.tar.gz'), + hashtype='sha512') + + def test_sources_old_format(self): + sources_path = os.path.join(self.tmpdir, 'sources') + sources = open(sources_path, 'w') + sources.write('70e17e21942515952b4050b370fc2141 tendrl-gluster-integration-1.5.2.tar.gz') + sources.close() + + # create repo for ns_module_name determining + repo = git.Repo.init(self.tmpdir) + repo.create_remote('origin', 'http://copr-dist-git.fedorainfracloud.org/git/ns/testpkg') + + self.cmd.lookasidecache.download = MagicMock() + self.cmd.sources() + self.cmd.lookasidecache.download.assert_called_once_with( + 'ns/testpkg', + 'tendrl-gluster-integration-1.5.2.tar.gz', + '70e17e21942515952b4050b370fc2141', + '{}/{}'.format(self.tmpdir, 'tendrl-gluster-integration-1.5.2.tar.gz'), + hashtype='md5') + + def test_srpm(self): + spec_path = self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + self.touch_file('source0.tar.gz') + self.cmd._run_command = MagicMock() + self.cmd.srpm() + cmd_templated = ['rpmbuild', "--define '_sourcedir {path}'", "--define '_specdir {path}'", + "--define '_builddir {path}'", "--define '_srcrpmdir {path}'", + "--define '_rpmdir {path}'", '--nodeps', '-bs', '{path}/testpkg.spec'] + cmd = [part.format(path=self.tmpdir) for part in cmd_templated] + self.cmd._run_command.assert_called_with(cmd, shell=True) + + def test_is_unpacked_source_is_present(self): + spec_path = self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + self.touch_file('source0.tar.gz') + parsed_spec = self.get_parsed_spec(spec_path) + is_unpacked = self.cmd.is_unpacked(self.tmpdir, parsed_spec.sources) + self.assertFalse(is_unpacked) + + def test_is_unpacked_source_not_present(self): + spec_path = self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + self.touch_file('source1.tar.gz') + parsed_spec = self.get_parsed_spec(spec_path) + is_unpacked = self.cmd.is_unpacked(self.tmpdir, parsed_spec.sources) + self.assertTrue(is_unpacked) + + def test_is_unpacked_patch_present(self): + spec_path = self.dump_spec( + SPEC_WITH_PATCH_TEMPLATE, source0='source0.tar.gz', patch0='patch.txt') + self.touch_file('patch.txt') + parsed_spec = self.get_parsed_spec(spec_path) + is_unpacked = self.cmd.is_unpacked(self.tmpdir, parsed_spec.sources) + self.assertFalse(is_unpacked) + + def test_is_unpacked_ignored_files(self): + spec_path = self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + parsed_spec = self.get_parsed_spec(spec_path) + is_unpacked = self.cmd.is_unpacked(self.tmpdir, parsed_spec.sources) + self.assertFalse(is_unpacked) + + ignored_filenames = [ + 'README', 'readme', 'README.md', + 'tito.props', 'x.spec', 'sources', '.hiddden' + ] + + for filename in ignored_filenames: + self.touch_file(filename) + + is_unpacked = self.cmd.is_unpacked(self.tmpdir, parsed_spec.sources) + self.assertFalse(is_unpacked) + + self.touch_file('non-ignored-filename') + is_unpacked = self.cmd.is_unpacked(self.tmpdir, parsed_spec.sources) + self.assertTrue(is_unpacked) + + self.touch_file('source0.tar.gz') + is_unpacked = self.cmd.is_unpacked(self.tmpdir, parsed_spec.sources) + self.assertFalse(is_unpacked) + + def test_make_source_raises_on_packed(self): + self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + with self.assertRaises(NotUnpackedException): + self.cmd.make_source(self.tmpdir) + + def test_make_source_packs_on_unpacked(self): + spec_path = self.dump_spec(SPEC_TEMPLATE, source0='source0.tar.gz') + self.touch_file('patch.txt', subdir='dir') + archive_path = self.cmd.make_source(self.tmpdir) + self.assertTrue(os.path.exists(archive_path)) + + parsed_spec = self.get_parsed_spec(spec_path) + source0_filename = find_source_zero(parsed_spec.sources) + self.assertEquals(os.path.basename(archive_path), source0_filename) + + parsed_spec = self.get_parsed_spec(spec_path) + source0_filename = find_source_zero(parsed_spec.sources) + self.assertEquals(os.path.basename(archive_path), source0_filename) + + expected_names = sorted([ + 'testpkg-1', 'testpkg-1/dir', 'testpkg-1/dir/patch.txt', 'testpkg-1/testpkg.spec' + ]) + tarball = tarfile.open(archive_path, 'r:gz') + self.assertEquals(expected_names, sorted(tarball.getnames())) + + def test_make_source_raises_on_invalid_spec(self): + self.dump_spec(INVALID_SPEC_TEMPLATE) + with self.assertRaises(RpmSpecParseException): + self.cmd.make_source(self.tmpdir) + + def test_make_source_raises_on_no_source_zero(self): + self.dump_spec(NO_SOURCE_ZERO_SPEC_TEMPLATE) + self.touch_file('patch.txt') + with self.assertRaises(NoSourceZeroException): + self.cmd.make_source(self.tmpdir) diff --git a/rpkg-util b/rpkg-util deleted file mode 160000 index d7a0bd8..0000000 --- a/rpkg-util +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d7a0bd89abf4acc451a18637dad54a226c03286e