From 57e28b2782eda171616a7fdfb7bf03cb03e68aa3 Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Sat, 1 Aug 2015 12:44:20 +0100 Subject: [PATCH] Moved to GitHub: https://github.com/0install/0release --- 0release | 100 -------- 0release.xml | 66 ----- COPYING | 510 ------------------------------------- README | 39 +-- compile.py | 151 ----------- release.py | 608 -------------------------------------------- scm.py | 175 ------------- setup.py | 113 -------- support.py | 247 ------------------ tests/c-prog.tgz | Bin 11886 -> 0 bytes tests/gpg.tgz | Bin 2041 -> 0 bytes tests/test-repo-actions.tgz | Bin 7502 -> 0 bytes tests/test-repo.tgz | Bin 8834 -> 0 bytes tests/testall.py | 50 ---- tests/testdocs.py | 20 -- tests/testrelease.py | 228 ----------------- 16 files changed, 3 insertions(+), 2304 deletions(-) delete mode 100755 0release delete mode 100644 0release.xml delete mode 100644 COPYING rewrite README (99%) delete mode 100644 compile.py delete mode 100644 release.py delete mode 100644 scm.py delete mode 100644 setup.py delete mode 100644 support.py delete mode 100644 tests/c-prog.tgz delete mode 100644 tests/gpg.tgz delete mode 100644 tests/test-repo-actions.tgz delete mode 100644 tests/test-repo.tgz delete mode 100755 tests/testall.py delete mode 100755 tests/testdocs.py delete mode 100755 tests/testrelease.py diff --git a/0release b/0release deleted file mode 100755 index 91d5af9..0000000 --- a/0release +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python -# Copyright (C) 2009, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -from optparse import OptionParser -import os, sys - -zi = os.environ.get("0RELEASE_ZEROINSTALL", None) -if zi is not None: - # NOT the first element... that's us! - # (and we want our setup.py, not 0install's) - sys.path.insert(1, zi) -from zeroinstall import SafeException -from zeroinstall.injector import qdom - -version = '0.15' - -parser = OptionParser(usage = """usage: %prog [options] LOCAL-FEED - -Run this command from a new empty directory to set things up.""") - -parser.add_option("", "--builders", help="comma-separated list of builders for binaries", metavar='LIST') -parser.add_option("", "--build-slave", help="compile a binary a source release candidate", action='store_true') -parser.add_option("-k", "--key", help="GPG key to use for signing", action='store', metavar='KEYID') -parser.add_option("-v", "--verbose", help="more verbose output", action='count') -parser.add_option("-r", "--release", help="make a new release", action='store_true') -parser.add_option("", "--archive-dir-public-url", help="remote directory for releases", metavar='URL') -parser.add_option("", "--master-feed-file", help="local file to extend with new releases", metavar='PATH') -parser.add_option("", "--archive-upload-command", help="shell command to upload releases", metavar='COMMAND') -parser.add_option("", "--master-feed-upload-command", help="shell command to upload feed", metavar='COMMAND') -parser.add_option("", "--public-scm-repository", help="the name of the repository to push to", metavar='REPOS') -parser.add_option("", "--release-version", help="explicitly set the version of this release", metavar='VERSION') -parser.add_option("-V", "--version", help="display version information", action='store_true') - -(options, args) = parser.parse_args() - -if options.version: - print "0release (zero-install) " + version - print "Copyright (C) 2009 Thomas Leonard" - print "This program comes with ABSOLUTELY NO WARRANTY," - print "to the extent permitted by law." - print "You may redistribute copies of this program" - print "under the terms of the GNU General Public License." - print "For more information about these matters, see the file named COPYING." - sys.exit(0) - -if options.verbose: - import logging - logger = logging.getLogger() - if options.verbose == 1: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.DEBUG) - -if options.build_slave: - if len(args) != 4: - parser.print_help() - sys.exit(1) - src_feed, archive_file, archive_dir_public_url, target_feed = args - import compile - compile.build_slave(src_feed, archive_file, archive_dir_public_url, target_feed) - sys.exit(0) - -if len(args) != 1: - parser.print_help() - sys.exit(1) - -local_feed_path = os.path.abspath(args[0]) - -try: - if not os.path.exists(local_feed_path): - raise SafeException("Local feed file '%s' does not exist" % local_feed_path) - - with open(local_feed_path, 'rb') as stream: - root = qdom.parse(stream) - - import support - feed = support.load_feed(local_feed_path) - - if options.release: - import release - release.do_release(feed, options) - else: - import setup - setup.init_releases_directory(feed) -except KeyboardInterrupt, ex: - print >>sys.stderr, "Interrupted" - sys.exit(1) -except OSError, ex: - if options.verbose: raise - print >>sys.stderr, str(ex) - sys.exit(1) -except IOError, ex: - if options.verbose: raise - print >>sys.stderr, str(ex) - sys.exit(1) -except SafeException, ex: - if options.verbose: raise - print >>sys.stderr, str(ex) - sys.exit(1) diff --git a/0release.xml b/0release.xml deleted file mode 100644 index 68c61b8..0000000 --- a/0release.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - 0release - manage new releases with Zero Install - - If you have a local feed in the development version of your program, - 0release can help you to create new releases easily. - - - http://0install.net/0release.xml - - - - - - sed -i "s/^version = '.*'$/version = '$RELEASE_VERSION'/" 0release - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/COPYING b/COPYING deleted file mode 100644 index cf9b6b9..0000000 --- a/COPYING +++ /dev/null @@ -1,510 +0,0 @@ - - GNU LESSER GENERAL PUBLIC LICENSE - Version 2.1, February 1999 - - Copyright (C) 1991, 1999 Free Software Foundation, Inc. - 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - -[This is the first released version of the Lesser GPL. It also counts - as the successor of the GNU Library Public License, version 2, hence - the version number 2.1.] - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -Licenses are intended to guarantee your freedom to share and change -free software--to make sure the software is free for all its users. - - This license, the Lesser General Public License, applies to some -specially designated software packages--typically libraries--of the -Free Software Foundation and other authors who decide to use it. You -can use it too, but we suggest you first think carefully about whether -this license or the ordinary General Public License is the better -strategy to use in any particular case, based on the explanations -below. - - When we speak of free software, we are referring to freedom of use, -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 and use pieces of -it in new free programs; and that you are informed that you can do -these things. - - To protect your rights, we need to make restrictions that forbid -distributors to deny you these rights or to ask you to surrender these -rights. These restrictions translate to certain responsibilities for -you if you distribute copies of the library or if you modify it. - - For example, if you distribute copies of the library, whether gratis -or for a fee, you must give the recipients all the rights that we gave -you. You must make sure that they, too, receive or can get the source -code. If you link other code with the library, you must provide -complete object files to the recipients, so that they can relink them -with the library after making changes to the library and recompiling -it. And you must show them these terms so they know their rights. - - We protect your rights with a two-step method: (1) we copyright the -library, and (2) we offer you this license, which gives you legal -permission to copy, distribute and/or modify the library. - - To protect each distributor, we want to make it very clear that -there is no warranty for the free library. Also, if the library is -modified by someone else and passed on, the recipients should know -that what they have is not the original version, so that the original -author's reputation will not be affected by problems that might be -introduced by others. -^L - Finally, software patents pose a constant threat to the existence of -any free program. We wish to make sure that a company cannot -effectively restrict the users of a free program by obtaining a -restrictive license from a patent holder. Therefore, we insist that -any patent license obtained for a version of the library must be -consistent with the full freedom of use specified in this license. - - Most GNU software, including some libraries, is covered by the -ordinary GNU General Public License. This license, the GNU Lesser -General Public License, applies to certain designated libraries, and -is quite different from the ordinary General Public License. We use -this license for certain libraries in order to permit linking those -libraries into non-free programs. - - When a program is linked with a library, whether statically or using -a shared library, the combination of the two is legally speaking a -combined work, a derivative of the original library. The ordinary -General Public License therefore permits such linking only if the -entire combination fits its criteria of freedom. The Lesser General -Public License permits more lax criteria for linking other code with -the library. - - We call this license the "Lesser" General Public License because it -does Less to protect the user's freedom than the ordinary General -Public License. It also provides other free software developers Less -of an advantage over competing non-free programs. These disadvantages -are the reason we use the ordinary General Public License for many -libraries. However, the Lesser license provides advantages in certain -special circumstances. - - For example, on rare occasions, there may be a special need to -encourage the widest possible use of a certain library, so that it -becomes a de-facto standard. To achieve this, non-free programs must -be allowed to use the library. A more frequent case is that a free -library does the same job as widely used non-free libraries. In this -case, there is little to gain by limiting the free library to free -software only, so we use the Lesser General Public License. - - In other cases, permission to use a particular library in non-free -programs enables a greater number of people to use a large body of -free software. For example, permission to use the GNU C Library in -non-free programs enables many more people to use the whole GNU -operating system, as well as its variant, the GNU/Linux operating -system. - - Although the Lesser General Public License is Less protective of the -users' freedom, it does ensure that the user of a program that is -linked with the Library has the freedom and the wherewithal to run -that program using a modified version of the Library. - - The precise terms and conditions for copying, distribution and -modification follow. Pay close attention to the difference between a -"work based on the library" and a "work that uses the library". The -former contains code derived from the library, whereas the latter must -be combined with the library in order to run. -^L - GNU LESSER GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License Agreement applies to any software library or other -program which contains a notice placed by the copyright holder or -other authorized party saying it may be distributed under the terms of -this Lesser General Public License (also called "this License"). -Each licensee is addressed as "you". - - A "library" means a collection of software functions and/or data -prepared so as to be conveniently linked with application programs -(which use some of those functions and data) to form executables. - - The "Library", below, refers to any such software library or work -which has been distributed under these terms. A "work based on the -Library" means either the Library or any derivative work under -copyright law: that is to say, a work containing the Library or a -portion of it, either verbatim or with modifications and/or translated -straightforwardly into another language. (Hereinafter, translation is -included without limitation in the term "modification".) - - "Source code" for a work means the preferred form of the work for -making modifications to it. For a library, 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 library. - - Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running a program using the Library is not restricted, and output from -such a program is covered only if its contents constitute a work based -on the Library (independent of the use of the Library in a tool for -writing it). Whether that is true depends on what the Library does -and what the program that uses the Library does. - - 1. You may copy and distribute verbatim copies of the Library's -complete 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 distribute a copy of this License along with the -Library. - - 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 Library or any portion -of it, thus forming a work based on the Library, 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) The modified work must itself be a software library. - - b) You must cause the files modified to carry prominent notices - stating that you changed the files and the date of any change. - - c) You must cause the whole of the work to be licensed at no - charge to all third parties under the terms of this License. - - d) If a facility in the modified Library refers to a function or a - table of data to be supplied by an application program that uses - the facility, other than as an argument passed when the facility - is invoked, then you must make a good faith effort to ensure that, - in the event an application does not supply such function or - table, the facility still operates, and performs whatever part of - its purpose remains meaningful. - - (For example, a function in a library to compute square roots has - a purpose that is entirely well-defined independent of the - application. Therefore, Subsection 2d requires that any - application-supplied function or table used by this function must - be optional: if the application does not supply it, the square - root function must still compute square roots.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Library, -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 Library, 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 Library. - -In addition, mere aggregation of another work not based on the Library -with the Library (or with a work based on the Library) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may opt to apply the terms of the ordinary GNU General Public -License instead of this License to a given copy of the Library. To do -this, you must alter all the notices that refer to this License, so -that they refer to the ordinary GNU General Public License, version 2, -instead of to this License. (If a newer version than version 2 of the -ordinary GNU General Public License has appeared, then you can specify -that version instead if you wish.) Do not make any other change in -these notices. -^L - Once this change is made in a given copy, it is irreversible for -that copy, so the ordinary GNU General Public License applies to all -subsequent copies and derivative works made from that copy. - - This option is useful when you wish to copy part of the code of -the Library into a program that is not a library. - - 4. You may copy and distribute the Library (or a portion or -derivative of it, under Section 2) in object code or executable form -under the terms of Sections 1 and 2 above provided that you 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. - - If distribution of 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 satisfies the requirement to -distribute the source code, even though third parties are not -compelled to copy the source along with the object code. - - 5. A program that contains no derivative of any portion of the -Library, but is designed to work with the Library by being compiled or -linked with it, is called a "work that uses the Library". Such a -work, in isolation, is not a derivative work of the Library, and -therefore falls outside the scope of this License. - - However, linking a "work that uses the Library" with the Library -creates an executable that is a derivative of the Library (because it -contains portions of the Library), rather than a "work that uses the -library". The executable is therefore covered by this License. -Section 6 states terms for distribution of such executables. - - When a "work that uses the Library" uses material from a header file -that is part of the Library, the object code for the work may be a -derivative work of the Library even though the source code is not. -Whether this is true is especially significant if the work can be -linked without the Library, or if the work is itself a library. The -threshold for this to be true is not precisely defined by law. - - If such an object file uses only numerical parameters, data -structure layouts and accessors, and small macros and small inline -functions (ten lines or less in length), then the use of the object -file is unrestricted, regardless of whether it is legally a derivative -work. (Executables containing this object code plus portions of the -Library will still fall under Section 6.) - - Otherwise, if the work is a derivative of the Library, you may -distribute the object code for the work under the terms of Section 6. -Any executables containing that work also fall under Section 6, -whether or not they are linked directly with the Library itself. -^L - 6. As an exception to the Sections above, you may also combine or -link a "work that uses the Library" with the Library to produce a -work containing portions of the Library, and distribute that work -under terms of your choice, provided that the terms permit -modification of the work for the customer's own use and reverse -engineering for debugging such modifications. - - You must give prominent notice with each copy of the work that the -Library is used in it and that the Library and its use are covered by -this License. You must supply a copy of this License. If the work -during execution displays copyright notices, you must include the -copyright notice for the Library among them, as well as a reference -directing the user to the copy of this License. Also, you must do one -of these things: - - a) Accompany the work with the complete corresponding - machine-readable source code for the Library including whatever - changes were used in the work (which must be distributed under - Sections 1 and 2 above); and, if the work is an executable linked - with the Library, with the complete machine-readable "work that - uses the Library", as object code and/or source code, so that the - user can modify the Library and then relink to produce a modified - executable containing the modified Library. (It is understood - that the user who changes the contents of definitions files in the - Library will not necessarily be able to recompile the application - to use the modified definitions.) - - b) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (1) uses at run time a - copy of the library already present on the user's computer system, - rather than copying library functions into the executable, and (2) - will operate properly with a modified version of the library, if - the user installs one, as long as the modified version is - interface-compatible with the version that the work was made with. - - c) Accompany the work with a written offer, valid for at least - three years, to give the same user the materials specified in - Subsection 6a, above, for a charge no more than the cost of - performing this distribution. - - d) If distribution of the work is made by offering access to copy - from a designated place, offer equivalent access to copy the above - specified materials from the same place. - - e) Verify that the user has already received a copy of these - materials or that you have already sent this user a copy. - - For an executable, the required form of the "work that uses the -Library" must include any data and utility programs needed for -reproducing the executable from it. However, as a special exception, -the materials to be 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. - - It may happen that this requirement contradicts the license -restrictions of other proprietary libraries that do not normally -accompany the operating system. Such a contradiction means you cannot -use both them and the Library together in an executable that you -distribute. -^L - 7. You may place library facilities that are a work based on the -Library side-by-side in a single library together with other library -facilities not covered by this License, and distribute such a combined -library, provided that the separate distribution of the work based on -the Library and of the other library facilities is otherwise -permitted, and provided that you do these two things: - - a) Accompany the combined library with a copy of the same work - based on the Library, uncombined with any other library - facilities. This must be distributed under the terms of the - Sections above. - - b) Give prominent notice with the combined library of the fact - that part of it is a work based on the Library, and explaining - where to find the accompanying uncombined form of the same work. - - 8. You may not copy, modify, sublicense, link with, or distribute -the Library except as expressly provided under this License. Any -attempt otherwise to copy, modify, sublicense, link with, or -distribute the Library 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. - - 9. 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 Library or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Library (or any work based on the -Library), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Library or works based on it. - - 10. Each time you redistribute the Library (or any work based on the -Library), the recipient automatically receives a license from the -original licensor to copy, distribute, link with or modify the Library -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 with -this License. -^L - 11. 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 Library at all. For example, if a patent -license would not permit royalty-free redistribution of the Library 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 Library. - -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. - - 12. If the distribution and/or use of the Library is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Library 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. - - 13. The Free Software Foundation may publish revised and/or new -versions of the Lesser 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 Library -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 Library does not specify a -license version number, you may choose any version ever published by -the Free Software Foundation. -^L - 14. If you wish to incorporate parts of the Library into other free -programs whose distribution conditions are incompatible with these, -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 - - 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO -WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. -EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR -OTHER PARTIES PROVIDE THE LIBRARY "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 -LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME -THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. 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 LIBRARY 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 -LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF -SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - - END OF TERMS AND CONDITIONS -^L - How to Apply These Terms to Your New Libraries - - If you develop a new library, and you want it to be of the greatest -possible use to the public, we recommend making it free software that -everyone can redistribute and change. You can do so by permitting -redistribution under these terms (or, alternatively, under the terms -of the ordinary General Public License). - - To apply these terms, attach the following notices to the library. -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 library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -Also add information on how to contact you by electronic and paper mail. - -You should also get your employer (if you work as a programmer) or -your school, if any, to sign a "copyright disclaimer" for the library, -if necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the - library `Frob' (a library for tweaking knobs) written by James - Random Hacker. - - , 1 April 1990 - Ty Coon, President of Vice - -That's all there is to it! - - diff --git a/README b/README dissimilarity index 99% index dbea5eb..5b4b66f 100644 --- a/README +++ b/README @@ -1,36 +1,3 @@ -0release - -(c) Copyright Thomas Leonard, 2009 - - -INTRODUCTION - -0release can be used to make new releases of your software. It handles details -such as setting the version number and release date, tagging the release in -your version control system and updating your Zero Install feed. - -See http://0install.net/0release.html for details, including a tutorial. - - -CONDITIONS - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library 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 -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - - -BUG REPORTS - -Please report any bugs to the mailing list: - - http://0install.net/support.html +Moved to GitHub: + +https://github.com/0install/0release diff --git a/compile.py b/compile.py deleted file mode 100644 index 7823c55..0000000 --- a/compile.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright (C) 2009, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import tempfile, shutil, os, sys -import ConfigParser -from logging import info -from zeroinstall.support import basedir, portable_rename - -import support - -class Compiler: - def __init__(self, options, src_feed_name, release_version): - self.src_feed_name = src_feed_name - self.src_feed = support.load_feed(src_feed_name) - self.archive_dir_public_url = support.get_archive_url(options, release_version, '') - - self.config = ConfigParser.RawConfigParser() - - # Start with a default configuration - self.config.add_section('global') - self.config.set('global', 'builders', 'host') - - self.config.add_section('builder-host') - #self.config.set('builder-host', 'build', '0launch --not-before 0.10 http://0install.net/2007/interfaces/0release.xml --build-slave "$@"') - self.config.set('builder-host', 'build', '') - - self.src_impl = support.get_singleton_impl(self.src_feed) - if self.src_impl.arch and self.src_impl.arch.endswith('-src'): - path = basedir.load_first_config('0install.net', '0release', 'builders.conf') - if path: - info("Loading configuration file '%s'", path) - self.config.read(path) - else: - info("No builders.conf configuration; will build a binary for this host only") - - if options.builders is not None: - builders = options.builders - else: - builders = self.config.get('global', 'builders').strip() - if builders: - self.targets = [x.strip() for x in builders.split(',')] - info("%d build targets configured: %s", len(self.targets), self.targets) - else: - self.targets = [] - info("No builders set; no binaries will be built") - else: - self.targets = [] - - # We run the build in a sub-process. The idea is that the build may need to run - # on a different machine. - def build_binaries(self): - if not self.targets: return - - print "Source package, so generating binaries..." - - archive_file = support.get_archive_basename(self.src_impl) - - for target in self.targets: - start = self.get('builder-' + target, 'start', None) - command = self.config.get('builder-' + target, 'build') - stop = self.get('builder-' + target, 'stop', None) - - binary_feed = 'binary-' + target + '.xml' - if os.path.exists(binary_feed): - print "Feed %s already exists; not rebuilding" % binary_feed - else: - print "\nBuilding binary with builder '%s' ...\n" % target - - if start: support.show_and_run(start, []) - try: - args = [os.path.basename(self.src_feed_name), archive_file, self.archive_dir_public_url, binary_feed + '.new'] - if not command: - assert target == 'host', 'Missing build command' - support.check_call([sys.executable, sys.argv[0], '--build-slave'] + args) - else: - support.show_and_run(command, args) - finally: - if stop: support.show_and_run(stop, []) - - bin_feed = support.load_feed(binary_feed + '.new') - bin_impl = support.get_singleton_impl(bin_feed) - bin_archive_file = support.get_archive_basename(bin_impl) - bin_size = bin_impl.download_sources[0].size - - assert os.path.exists(bin_archive_file), "Compiled binary '%s' not found!" % os.path.abspath(bin_archive_file) - assert os.path.getsize(bin_archive_file) == bin_size, "Compiled binary '%s' has wrong size!" % os.path.abspath(bin_archive_file) - - portable_rename(binary_feed + '.new', binary_feed) - - def get_binary_feeds(self): - return ['binary-%s.xml' % target for target in self.targets] - - def get(self, section, option, default): - try: - return self.config.get(section, option) - except ConfigParser.NoOptionError: - return default - -# This is the actual build process, running on the build machine -def build_slave(src_feed, archive_file, archive_dir_public_url, target_feed): - try: - COMPILE = [os.environ['0COMPILE']] - except KeyError: - # (build slave has an old 0release) - COMPILE = ['0launch', '--not-before=0.30', 'http://0install.net/2006/interfaces/0compile.xml'] - - feed = support.load_feed(src_feed) - - src_feed = os.path.abspath(src_feed) - archive_file = os.path.abspath(archive_file) - target_feed = os.path.abspath(target_feed) - - impl, = feed.implementations.values() - - tmpdir = tempfile.mkdtemp(prefix = '0release-') - try: - os.chdir(tmpdir) - depdir = os.path.join(tmpdir, 'dependencies') - os.mkdir(depdir) - - support.unpack_tarball(archive_file) - portable_rename(impl.download_sources[0].extract, os.path.join(depdir, impl.id)) - - config = ConfigParser.RawConfigParser() - config.add_section('compile') - config.set('compile', 'download-base-url', archive_dir_public_url) - config.set('compile', 'version-modifier', '') - config.set('compile', 'interface', src_feed) - config.set('compile', 'selections', '') - config.set('compile', 'metadir', '0install') - stream = open(os.path.join(tmpdir, '0compile.properties'), 'w') - try: - config.write(stream) - finally: - stream.close() - - support.check_call(COMPILE + ['build'], cwd = tmpdir) - support.check_call(COMPILE + ['publish', '--target-feed', target_feed], cwd = tmpdir) - - # TODO: run unit-tests - - feed = support.load_feed(target_feed) - impl = support.get_singleton_impl(feed) - archive_file = support.get_archive_basename(impl) - - shutil.move(archive_file, os.path.join(os.path.dirname(target_feed), archive_file)) - except: - print "\nLeaving temporary directory %s for inspection...\n" % tmpdir - raise - else: - shutil.rmtree(tmpdir) diff --git a/release.py b/release.py deleted file mode 100644 index 9f4eaaa..0000000 --- a/release.py +++ /dev/null @@ -1,608 +0,0 @@ -# Copyright (C) 2009, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import os, subprocess, shutil, sys, re -from xml.dom import minidom -from zeroinstall import SafeException -from zeroinstall.injector import model -from zeroinstall.support import ro_rmtree -from logging import info, warn - -sys.path.insert(0, os.environ['RELEASE_0REPO']) -from repo import registry, merge - -import support, compile -from scm import get_scm - -XMLNS_RELEASE = 'http://zero-install.sourceforge.net/2007/namespaces/0release' - -valid_phases = ['commit-release', 'generate-archive'] - -TMP_BRANCH_NAME = '0release-tmp' - -test_command = os.environ['0TEST'] - -def run_unit_tests(local_feed): - print "Running self-tests..." - exitstatus = subprocess.call([test_command, '--', local_feed]) - if exitstatus == 2: - print "SKIPPED unit tests for %s (no 'test' command)" % local_feed - return - if exitstatus: - raise SafeException("Self-test failed with exit status %d" % exitstatus) - -def upload_archives(options, status, uploads): - # For each binary or source archive in uploads, ensure it is available - # from options.archive_dir_public_url - - # We try to do all the uploads together first, and then verify them all - # afterwards. This is because we may have to wait for them to be moved - # from an incoming queue before we can test them. - - def url(archive): - return support.get_archive_url(options, status.release_version, archive) - - # Check that url exists and has the given size - def is_uploaded(url, size): - if url.startswith('http://TESTING/releases'): - return True - - print "Testing URL %s..." % url - try: - actual_size = int(support.get_size(url)) - except Exception, ex: - print "Can't get size of '%s': %s" % (url, ex) - return False - else: - if actual_size == size: - return True - print "WARNING: %s exists, but size is %d, not %d!" % (url, actual_size, size) - return False - - # status.verified_uploads is an array of status flags: - description = { - 'N': 'Upload required', - 'A': 'Upload has been attempted, but we need to check whether it worked', - 'V': 'Upload has been checked (exists and has correct size)', - } - - if status.verified_uploads is None: - # First time around; no point checking for existing uploads - status.verified_uploads = 'N' * len(uploads) - status.save() - - while True: - print "\nUpload status:" - for i, stat in enumerate(status.verified_uploads): - print "- %s : %s" % (uploads[i], description[stat]) - print - - # Break if finished - if status.verified_uploads == 'V' * len(uploads): - break - - # Find all New archives - to_upload = [] - for i, stat in enumerate(status.verified_uploads): - assert stat in 'NAV' - if stat == 'N': - to_upload.append(uploads[i]) - print "Upload %s/%s as %s" % (status.release_version, uploads[i], url(uploads[i])) - - cmd = options.archive_upload_command.strip() - - if to_upload: - # Mark all New items as Attempted - status.verified_uploads = status.verified_uploads.replace('N', 'A') - status.save() - - # Upload them... - if cmd: - support.show_and_run(cmd, to_upload) - else: - if len(to_upload) == 1: - print "No upload command is set => please upload the archive manually now" - raw_input('Press Return once the archive is uploaded.') - else: - print "No upload command is set => please upload the archives manually now" - raw_input('Press Return once the %d archives are uploaded.' % len(to_upload)) - - # Verify all Attempted uploads - new_stat = '' - for i, stat in enumerate(status.verified_uploads): - assert stat in 'AV', status.verified_uploads - if stat == 'A' : - if not is_uploaded(url(uploads[i]), os.path.getsize(uploads[i])): - print "** Archive '%s' still not uploaded! Try again..." % uploads[i] - stat = 'N' - else: - stat = 'V' - new_stat += stat - - status.verified_uploads = new_stat - status.save() - - if 'N' in new_stat and cmd: - raw_input('Press Return to try again.') - -legacy_warning = """*** Note: the upload functions of 0release -*** (--archive-dir-public-url, --master-feed-file, --archive-upload-command -*** and --master-feed-upload-command) are being replaced by 0repo. They may -*** go away in future. If 0repo is not suitable for your needs, please -*** contact the mailing list to let us know. -*** -*** http://www.0install.net/0repo.html -*** http://www.0install.net/support.html#lists -""" - -def do_version_substitutions(impl_dir, version_substitutions, new_version): - for (rel_path, subst) in version_substitutions: - assert not os.path.isabs(rel_path), rel_path - path = os.path.join(impl_dir, rel_path) - with open(path, 'rt') as stream: - data = stream.read() - - match = subst.search(data) - if match: - orig = match.group(0) - span = match.span(1) - if match.lastindex != 1: - raise SafeException("Regex '%s' must have exactly one matching () group" % subst.pattern) - assert span[0] >= 0, "Version match group did not match (regexp=%s; match=%s)" % (subst.pattern, orig) - new_data = data[:span[0]] + new_version + data[span[1]:] - else: - raise SafeException("No matches for regex '%s' in '%s'" % (subst.pattern, path)) - - with open(path, 'wt') as stream: - stream.write(new_data) - -def do_release(local_feed, options): - if options.master_feed_file or options.archive_dir_public_url or options.archive_upload_command or options.master_feed_upload_command: - print(legacy_warning) - - if options.master_feed_file: - options.master_feed_file = os.path.abspath(options.master_feed_file) - - if not local_feed.feed_for: - raise SafeException("Feed %s missing a element" % local_feed.local_path) - - status = support.Status() - local_impl = support.get_singleton_impl(local_feed) - - local_impl_dir = local_impl.id - assert os.path.isabs(local_impl_dir) - local_impl_dir = os.path.realpath(local_impl_dir) - assert os.path.isdir(local_impl_dir) - assert local_feed.local_path.startswith(local_impl_dir + os.sep) - - # From the impl directory to the feed - # NOT relative to the archive root (in general) - local_iface_rel_path = local_feed.local_path[len(local_impl_dir) + 1:] - assert not local_iface_rel_path.startswith('/') - assert os.path.isfile(os.path.join(local_impl_dir, local_iface_rel_path)) - - phase_actions = {} - for phase in valid_phases: - phase_actions[phase] = [] # List of elements - - version_substitutions = [] - - add_toplevel_dir = None - release_management = local_feed.get_metadata(XMLNS_RELEASE, 'management') - if len(release_management) == 1: - info("Found element.") - release_management = release_management[0] - for x in release_management.childNodes: - if x.uri == XMLNS_RELEASE and x.name == 'action': - phase = x.getAttribute('phase') - if phase not in valid_phases: - raise SafeException("Invalid action phase '%s' in local feed %s. Valid actions are:\n%s" % (phase, local_feed.local_path, '\n'.join(valid_phases))) - phase_actions[phase].append(x.content) - elif x.uri == XMLNS_RELEASE and x.name == 'update-version': - version_substitutions.append((x.getAttribute('path'), re.compile(x.content, re.MULTILINE))) - elif x.uri == XMLNS_RELEASE and x.name == 'add-toplevel-directory': - add_toplevel_dir = local_feed.get_name() - else: - warn("Unknown element: %s", x) - elif len(release_management) > 1: - raise SafeException("Multiple sections in %s!" % local_feed) - else: - info("No element found in local feed.") - - scm = get_scm(local_feed, options) - - # Path relative to the archive / SCM root - local_iface_rel_root_path = local_feed.local_path[len(scm.root_dir) + 1:] - - def run_hooks(phase, cwd, env): - info("Running hooks for phase '%s'" % phase) - full_env = os.environ.copy() - full_env.update(env) - for x in phase_actions[phase]: - print "[%s]: %s" % (phase, x) - support.check_call(x, shell = True, cwd = cwd, env = full_env) - - def set_to_release(): - print "Snapshot version is " + local_impl.get_version() - release_version = options.release_version - if release_version is None: - suggested = support.suggest_release_version(local_impl.get_version()) - release_version = raw_input("Version number for new release [%s]: " % suggested) - if not release_version: - release_version = suggested - - scm.ensure_no_tag(release_version) - - status.head_before_release = scm.get_head_revision() - status.save() - - working_copy = local_impl.id - do_version_substitutions(local_impl_dir, version_substitutions, release_version) - run_hooks('commit-release', cwd = working_copy, env = {'RELEASE_VERSION': release_version}) - - print "Releasing version", release_version - support.publish(local_feed.local_path, set_released = 'today', set_version = release_version) - - support.backup_if_exists(release_version) - os.mkdir(release_version) - os.chdir(release_version) - - status.old_snapshot_version = local_impl.get_version() - status.release_version = release_version - status.head_at_release = scm.commit('Release %s' % release_version, branch = TMP_BRANCH_NAME, parent = 'HEAD') - status.save() - - def set_to_snapshot(snapshot_version): - assert snapshot_version.endswith('-post') - support.publish(local_feed.local_path, set_released = '', set_version = snapshot_version) - do_version_substitutions(local_impl_dir, version_substitutions, snapshot_version) - scm.commit('Start development series %s' % snapshot_version, branch = TMP_BRANCH_NAME, parent = TMP_BRANCH_NAME) - status.new_snapshot_version = scm.get_head_revision() - status.save() - - def ensure_ready_to_release(): - #if not options.master_feed_file: - # raise SafeException("Master feed file not set! Check your configuration") - - scm.ensure_committed() - scm.ensure_versioned(os.path.abspath(local_feed.local_path)) - info("No uncommitted changes. Good.") - # Not needed for GIT. For SCMs where tagging is expensive (e.g. svn) this might be useful. - #run_unit_tests(local_impl) - - scm.grep('\(^\\|[^=]\)\<\\(TODO\\|XXX\\|FIXME\\)\>') - - def create_feed(target_feed, local_iface_path, archive_file, archive_name, main): - shutil.copyfile(local_iface_path, target_feed) - - support.publish(target_feed, - set_main = main, - archive_url = support.get_archive_url(options, status.release_version, os.path.basename(archive_file)), - archive_file = archive_file, - archive_extract = archive_name) - - def get_previous_release(this_version): - """Return the highest numbered verison in the master feed before this_version. - @return: version, or None if there wasn't one""" - parsed_release_version = model.parse_version(this_version) - - versions = [model.parse_version(version) for version in scm.get_tagged_versions()] - versions = [version for version in versions if version < parsed_release_version] - - if versions: - return model.format_version(max(versions)) - return None - - def export_changelog(previous_release): - changelog = file('changelog-%s' % status.release_version, 'w') - try: - try: - scm.export_changelog(previous_release, status.head_before_release, changelog) - except SafeException, ex: - print "WARNING: Failed to generate changelog: " + str(ex) - else: - print "Wrote changelog from %s to here as %s" % (previous_release or 'start', changelog.name) - finally: - changelog.close() - - def fail_candidate(): - cwd = os.getcwd() - assert cwd.endswith(status.release_version) - support.backup_if_exists(cwd) - scm.delete_branch(TMP_BRANCH_NAME) - os.unlink(support.release_status_file) - print "Restored to state before starting release. Make your fixes and try again..." - - def release_via_0repo(new_impls_feed): - import repo.cmd - support.make_archives_relative(new_impls_feed) - oldcwd = os.getcwd() - try: - repo.cmd.main(['0repo', 'add', '--', new_impls_feed]) - finally: - os.chdir(oldcwd) - - def release_without_0repo(archive_file, new_impls_feed): - assert options.master_feed_file - - if not options.archive_dir_public_url: - raise SafeException("Archive directory public URL is not set! Edit configuration and try again.") - - if status.updated_master_feed: - print "Already added to master feed. Not changing." - else: - publish_opts = {} - if os.path.exists(options.master_feed_file): - # Check we haven't already released this version - master = support.load_feed(os.path.realpath(options.master_feed_file)) - existing_releases = [impl for impl in master.implementations.values() if impl.get_version() == status.release_version] - if len(existing_releases): - raise SafeException("Master feed %s already contains an implementation with version number %s!" % (options.master_feed_file, status.release_version)) - - previous_release = get_previous_release(status.release_version) - previous_testing_releases = [impl for impl in master.implementations.values() if impl.get_version() == previous_release - and impl.upstream_stability == model.stability_levels["testing"]] - if previous_testing_releases: - print "The previous release, version %s, is still marked as 'testing'. Set to stable?" % previous_release - if support.get_choice(['Yes', 'No']) == 'Yes': - publish_opts['select_version'] = previous_release - publish_opts['set_stability'] = "stable" - - support.publish(options.master_feed_file, local = new_impls_feed, xmlsign = True, key = options.key, **publish_opts) - - status.updated_master_feed = 'true' - status.save() - - # Copy files... - uploads = [os.path.basename(archive_file)] - for b in compiler.get_binary_feeds(): - binary_feed = support.load_feed(b) - impl, = binary_feed.implementations.values() - uploads.append(os.path.basename(impl.download_sources[0].url)) - - upload_archives(options, status, uploads) - - feed_base = os.path.dirname(list(local_feed.feed_for)[0]) - feed_files = [options.master_feed_file] - print "Upload %s into %s" % (', '.join(feed_files), feed_base) - cmd = options.master_feed_upload_command.strip() - if cmd: - support.show_and_run(cmd, feed_files) - else: - print "NOTE: No feed upload command set => you'll have to upload them yourself!" - - def accept_and_publish(archive_file, src_feed_name): - if status.tagged: - print "Already tagged in SCM. Not re-tagging." - else: - scm.ensure_committed() - head = scm.get_head_revision() - if head != status.head_before_release: - raise SafeException("Changes committed since we started!\n" + - "HEAD was " + status.head_before_release + "\n" - "HEAD now " + head) - - scm.tag(status.release_version, status.head_at_release) - scm.reset_hard(TMP_BRANCH_NAME) - scm.delete_branch(TMP_BRANCH_NAME) - - status.tagged = 'true' - status.save() - - assert len(local_feed.feed_for) == 1 - - # Merge the source and binary feeds together first, so - # that we update the master feed atomically and only - # have to sign it once. - with open(src_feed_name, 'rb') as stream: - doc = minidom.parse(stream) - for b in compiler.get_binary_feeds(): - with open(b, 'rb') as stream: - bin_doc = minidom.parse(b) - merge.merge(doc, bin_doc) - new_impls_feed = 'merged.xml' - with open(new_impls_feed, 'wb') as stream: - doc.writexml(stream) - - # TODO: support uploading to a sub-feed (requires support in 0repo too) - master_feed, = local_feed.feed_for - repository = registry.lookup(master_feed, missing_ok = True) - if repository: - release_via_0repo(new_impls_feed) - else: - release_without_0repo(archive_file, new_impls_feed) - - os.unlink(new_impls_feed) - - print "Push changes to public SCM repository..." - public_repos = options.public_scm_repository - if public_repos: - scm.push_head_and_release(status.release_version) - else: - print "NOTE: No public repository set => you'll have to push the tag and trunk yourself." - - os.unlink(support.release_status_file) - - if status.head_before_release: - head = scm.get_head_revision() - if status.release_version: - print "RESUMING release of %s %s" % (local_feed.get_name(), status.release_version) - if options.release_version and options.release_version != status.release_version: - raise SafeException("Can't start release of version %s; we are currently releasing %s.\nDelete the release-status file to abort the previous release." % (options.release_version, status.release_version)) - elif head == status.head_before_release: - print "Restarting release of %s (HEAD revision has not changed)" % local_feed.get_name() - else: - raise SafeException("Something went wrong with the last run:\n" + - "HEAD revision for last run was " + status.head_before_release + "\n" + - "HEAD revision now is " + head + "\n" + - "You should revert your working copy to the previous head and try again.\n" + - "If you're sure you want to release from the current head, delete '" + support.release_status_file + "'") - else: - print "Releasing", local_feed.get_name() - - ensure_ready_to_release() - - if status.release_version: - if not os.path.isdir(status.release_version): - raise SafeException("Can't resume; directory %s missing. Try deleting '%s'." % (status.release_version, support.release_status_file)) - os.chdir(status.release_version) - need_set_snapshot = False - if status.tagged: - print "Already tagged. Resuming the publishing process..." - elif status.new_snapshot_version: - head = scm.get_head_revision() - if head != status.head_before_release: - raise SafeException("There are more commits since we started!\n" - "HEAD was " + status.head_before_release + "\n" - "HEAD now " + head + "\n" - "To include them, delete '" + support.release_status_file + "' and try again.\n" - "To leave them out, put them on a new branch and reset HEAD to the release version.") - else: - raise SafeException("Something went wrong previously when setting the new snapshot version.\n" + - "Suggest you reset to the original HEAD of\n%s and delete '%s'." % (status.head_before_release, support.release_status_file)) - else: - set_to_release() # Changes directory - assert status.release_version - need_set_snapshot = True - - # May be needed by the upload command - os.environ['RELEASE_VERSION'] = status.release_version - - archive_name = support.make_archive_name(local_feed.get_name(), status.release_version) - archive_file = archive_name + '.tar.bz2' - - export_prefix = archive_name - if add_toplevel_dir is not None: - export_prefix += os.sep + add_toplevel_dir - - if status.created_archive and os.path.isfile(archive_file): - print "Archive already created" - else: - support.backup_if_exists(archive_file) - scm.export(export_prefix, archive_file, status.head_at_release) - - has_submodules = scm.has_submodules() - - if phase_actions['generate-archive'] or has_submodules: - try: - support.unpack_tarball(archive_file) - if has_submodules: - scm.export_submodules(archive_name) - run_hooks('generate-archive', cwd = archive_name, env = {'RELEASE_VERSION': status.release_version}) - info("Regenerating archive (may have been modified by generate-archive hooks...") - support.check_call(['tar', 'cjf', archive_file, archive_name]) - except SafeException: - scm.reset_hard(scm.get_current_branch()) - fail_candidate() - raise - - status.created_archive = 'true' - status.save() - - if need_set_snapshot: - set_to_snapshot(status.release_version + '-post') - # Revert back to the original revision, so that any fixes the user makes - # will get applied before the tag - scm.reset_hard(scm.get_current_branch()) - - #backup_if_exists(archive_name) - support.unpack_tarball(archive_file) - - extracted_feed_path = os.path.abspath(os.path.join(export_prefix, local_iface_rel_root_path)) - assert os.path.isfile(extracted_feed_path), "Local feed not in archive! Is it under version control?" - extracted_feed = support.load_feed(extracted_feed_path) - extracted_impl = support.get_singleton_impl(extracted_feed) - - if extracted_impl.main: - # Find main executable, relative to the archive root - abs_main = os.path.join(os.path.dirname(extracted_feed_path), extracted_impl.id, extracted_impl.main) - main = os.path.relpath(abs_main, archive_name + os.sep) - if main != extracted_impl.main: - print "(adjusting main: '%s' for the feed inside the archive, '%s' externally)" % (extracted_impl.main, main) - # XXX: this is going to fail if the feed uses the new syntax - if not os.path.exists(abs_main): - raise SafeException("Main executable '%s' not found after unpacking archive!" % abs_main) - if main == extracted_impl.main: - main = None # Don't change the main attribute - else: - main = None - - try: - if status.src_tests_passed: - print "Unit-tests already passed - not running again" - else: - # Make directories read-only (checks tests don't write) - support.make_readonly_recursive(archive_name) - - run_unit_tests(extracted_feed_path) - status.src_tests_passed = True - status.save() - except SafeException: - print "(leaving extracted directory for examination)" - fail_candidate() - raise - # Unpack it again in case the unit-tests changed anything - ro_rmtree(archive_name) - support.unpack_tarball(archive_file) - - # Generate feed for source - src_feed_name = '%s.xml' % archive_name - create_feed(src_feed_name, extracted_feed_path, archive_file, archive_name, main) - print "Wrote source feed as %s" % src_feed_name - - # If it's a source package, compile the binaries now... - compiler = compile.Compiler(options, os.path.abspath(src_feed_name), release_version = status.release_version) - compiler.build_binaries() - - previous_release = get_previous_release(status.release_version) - export_changelog(previous_release) - - if status.tagged: - raw_input('Already tagged. Press Return to resume publishing process...') - choice = 'Publish' - else: - print "\nCandidate release archive:", archive_file - print "(extracted to %s for inspection)" % os.path.abspath(archive_name) - - print "\nPlease check candidate and select an action:" - print "P) Publish candidate (accept)" - print "F) Fail candidate (delete release-status file)" - if previous_release: - print "D) Diff against release archive for %s" % previous_release - maybe_diff = ['Diff'] - else: - maybe_diff = [] - print "(you can also hit CTRL-C and resume this script when done)" - - while True: - choice = support.get_choice(['Publish', 'Fail'] + maybe_diff) - if choice == 'Diff': - previous_archive_name = support.make_archive_name(local_feed.get_name(), previous_release) - previous_archive_file = '..' + os.sep + previous_release + os.sep + previous_archive_name + '.tar.bz2' - - # For archives created by older versions of 0release - if not os.path.isfile(previous_archive_file): - old_previous_archive_file = '..' + os.sep + previous_archive_name + '.tar.bz2' - if os.path.isfile(old_previous_archive_file): - previous_archive_file = old_previous_archive_file - - if os.path.isfile(previous_archive_file): - support.unpack_tarball(previous_archive_file) - try: - support.show_diff(previous_archive_name, archive_name) - finally: - shutil.rmtree(previous_archive_name) - else: - # TODO: download it? - print "Sorry, archive file %s not found! Can't show diff." % previous_archive_file - else: - break - - info("Deleting extracted archive %s", archive_name) - shutil.rmtree(archive_name) - - if choice == 'Publish': - accept_and_publish(archive_file, src_feed_name) - else: - assert choice == 'Fail' - fail_candidate() diff --git a/scm.py b/scm.py deleted file mode 100644 index f413bd1..0000000 --- a/scm.py +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright (C) 2007, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import os, subprocess, tempfile -from zeroinstall import SafeException -from logging import info, warn -from support import unpack_tarball - -class SCM: - def __init__(self, root_dir, options): - self.options = options - self.root_dir = root_dir - assert type(root_dir) == str, root_dir - -class GIT(SCM): - def _run(self, args, **kwargs): - info("Running git %s (in %s)", ' '.join(args), self.root_dir) - return subprocess.Popen(["git"] + args, cwd = self.root_dir, **kwargs) - - def _run_check(self, args, **kwargs): - child = self._run(args, **kwargs) - code = child.wait() - if code: - raise SafeException("Git %s failed with exit code %d" % (repr(args), code)) - - def _run_stdout(self, args, **kwargs): - child = self._run(args, stdout = subprocess.PIPE, **kwargs) - stdout, unused = child.communicate() - if child.returncode: - raise SafeException('Failed to get current branch! Exit code %d: %s' % (child.returncode, stdout)) - return stdout - - def ensure_versioned(self, path): - """Ensure path is a file tracked by the version control system. - @raise SafeException: if file is not tracked""" - out = self._run_stdout(['ls-tree', 'HEAD', path]).strip() - if not out: - raise SafeException("File '%s' is not under version control, according to git-ls-tree" % path) - - def reset_hard(self, revision): - self._run_check(['reset', '--hard', revision]) - - def ensure_committed(self): - child = self._run(["status", "--porcelain", "-uno"], stdout = subprocess.PIPE) - stdout, unused = child.communicate() - if child.returncode == 0: - # Git >= 1.7 - if stdout.strip(): - raise SafeException('Uncommitted changes! Use "git-commit -a" to commit them. Changes are:\n' + stdout) - return - else: - # Old Git - child = self._run(["status", "-a"], stdout = subprocess.PIPE) - stdout, unused = child.communicate() - if not child.returncode: - raise SafeException('Uncommitted changes! Use "git-commit -a" to commit them. Changes are:\n' + stdout) - for scm in self._submodules(): - scm.ensure_committed() - - def _submodules(self): - for line in self._run_stdout(['submodule', 'status']).split('\n'): - if not line: continue - r, subdir = line.strip().split(' ')[:2] - scm = GIT(os.path.join(self.root_dir, subdir), self.options) - scm.rev = r - scm.rel_path = subdir - yield scm - - def make_tag(self, version): - return 'v' + version - - def tag(self, version, revision): - tag = self.make_tag(version) - if self.options.key: - key_opts = ['-u', self.options.key] - else: - key_opts = [] - self._run_check(['tag', '-s'] + key_opts + ['-m', 'Release %s' % version, tag, revision]) - print "Tagged as %s" % tag - - def get_current_branch(self): - current_branch = self._run_stdout(['symbolic-ref', 'HEAD']).strip() - info("Current branch is %s", current_branch) - return current_branch - - def get_tagged_versions(self): - child = self._run(['tag', '-l', 'v*'], stdout = subprocess.PIPE) - stdout, unused = child.communicate() - status = child.wait() - if status: - raise SafeException("git tag failed with exit code %d" % status) - return [v[1:] for v in stdout.split('\n') if v] - - def delete_branch(self, branch): - self._run_check(['branch', '-D', branch]) - - def push_head_and_release(self, version): - self._run_check(['push', self.options.public_scm_repository, self.make_tag(version), self.get_current_branch()]) - - def ensure_no_tag(self, version): - tag = self.make_tag(version) - child = self._run(['tag', '-l', tag], stdout = subprocess.PIPE) - stdout, unused = child.communicate() - if tag in stdout.split('\n'): - raise SafeException(("Release %s is already tagged! If you want to replace it, do\n" + - "git tag -d %s") % (version, tag)) - - def export(self, prefix, archive_file, revision): - child = self._run(['archive', '--format=tar', '--prefix=' + prefix + os.sep, revision], stdout = subprocess.PIPE) - subprocess.check_call(['bzip2', '-'], stdin = child.stdout, stdout = file(archive_file, 'w')) - status = child.wait() - if status: - if os.path.exists(archive_file): - os.unlink(archive_file) - raise SafeException("git-archive failed with exit code %d" % status) - - def export_submodules(self, target): - # Export all sub-modules under target - cwd = os.getcwd() - target = os.path.abspath(target) - for scm in self._submodules(): - tmp = tempfile.NamedTemporaryFile(prefix = '0release-') - try: - scm.export(prefix = '.', archive_file = tmp.name, revision = scm.rev) - os.chdir(os.path.join(target, scm.rel_path)) - unpack_tarball(tmp.name) - finally: - tmp.close() - os.chdir(cwd) - - def commit(self, message, branch, parent): - self._run_check(['add', '-u']) # Commit all changed tracked files to index - tree = self._run_stdout(['write-tree']).strip() - child = self._run(['commit-tree', tree, '-p', parent], stdin = subprocess.PIPE, stdout = subprocess.PIPE) - stdout, unused = child.communicate(message) - commit = stdout.strip() - info("Committed as %s", commit) - self._run_check(['branch', '-f', branch, commit]) - return commit - - def get_head_revision(self): - proc = self._run(['rev-parse', 'HEAD'], stdout = subprocess.PIPE) - stdout, unused = proc.communicate() - if proc.returncode: - raise Exception("git rev-parse failed with exit code %d" % proc.returncode) - head = stdout.strip() - assert head - return head - - def export_changelog(self, last_release_version, head, stream): - if last_release_version: - self._run_check(['log', 'refs/tags/v' + last_release_version + '..' + head], stdout = stream) - else: - self._run_check(['log', head], stdout = stream) - - def grep(self, pattern): - child = self._run(['grep', pattern]) - child.wait() - if child.returncode in [0, 1]: - return - warn("git grep returned exit code %d", child.returncode) - - def has_submodules(self): - return os.path.isfile(os.path.join(self.root_dir, '.gitmodules')) - -def get_scm(local_feed, options): - start_dir = os.path.dirname(os.path.abspath(local_feed.local_path)) - current = start_dir - while True: - if os.path.exists(os.path.join(current, '.git')): - return GIT(current, options) - parent = os.path.dirname(current) - if parent == current: - raise SafeException("Unable to determine which version control system is being used. Couldn't find .git in %s or any parent directory." % start_dir) - current = parent diff --git a/setup.py b/setup.py deleted file mode 100644 index 5570df0..0000000 --- a/setup.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (C) 2007, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import os -from zeroinstall import SafeException - -release_uri = 'http://0install.net/2007/interfaces/0release.xml' - -umask = os.umask(0) -os.umask(umask) - -def init_releases_directory(feed): - files = os.listdir('.') - if files: - raise SafeException("This command must be run from an empty directory!\n(this one contains %s)" % (', '.join(files[:5]))) - - print "Setting up releases directory for %s" % feed.get_name() - - master_feed_name = feed.get_name().replace(' ', '-') + '.xml' - - if os.name == 'nt': - make_release = file('make-release.bat', 'w') - make_release.write("""@echo off - -:: The directory people will download the releases from. -:: This will appear in the remote feed file. -::set ARCHIVE_DIR_PUBLIC_URL=http://placeholder.org/releases/%%RELEASE_VERSION%% -set ARCHIVE_DIR_PUBLIC_URL= - -:: The path to the main feed. -:: The new version is added here when you publish a release. -::set MASTER_FEED_FILE="$HOME/public_html/feeds/MyProg.xml" -set MASTER_FEED_FILE=%s - -:: A shell command to upload the generated archive file to the -:: public server (corresponds to %%ARCHIVE_DIR_PUBLIC_URL%%, which is -:: used to download it again). -:: If unset, you'll have to upload it yourself. -::set ARCHIVE_UPLOAD_COMMAND=scp %%* me@myhost:/var/www/releases/%%RELEASE_VERSION%%/ -set ARCHIVE_UPLOAD_COMMAND= - -:: A shell command to upload the master feed (%%MASTER_FEED_FILE%%) and -:: related files to your web server. It will be downloaded using the -:: feed's URL. If unset, you'll have to upload it yourself. -::set MASTER_FEED_UPLOAD_COMMAND=scp %%* me@myhost:/var/www/feeds/ -set MASTER_FEED_UPLOAD_COMMAND= - -:: Your public version control repository. When publishing, the new -:: HEAD and the release tag will be pushed to this using a command -:: such as "git-push main master v0.1" -:: If unset, you'll have to update it yourself. -::set PUBLIC_SCM_REPOSITORY=origin -set PUBLIC_SCM_REPOSITORY= - -cd /d "%%~dp0" -0launch %s --release %s ^ - --archive-dir-public-url="%%ARCHIVE_DIR_PUBLIC_URL%%" ^ - --master-feed-file="%%MASTER_FEED_FILE%%" ^ - --archive-upload-command="%%ARCHIVE_UPLOAD_COMMAND%%" ^ - --master-feed-upload-command="%%MASTER_FEED_UPLOAD_COMMAND%%" ^ - --public-scm-repository="%%PUBLIC_SCM_REPOSITORY%%" ^ - %%* -""" % (master_feed_name, release_uri, feed.local_path)) - make_release.close() - print "Success - created script:\n %s" % os.path.abspath('make-release.bat') - else: - make_release = file('make-release', 'w') - make_release.write("""#!/bin/sh - -# The directory people will download the releases from. -# This will appear in the remote feed file. -#ARCHIVE_DIR_PUBLIC_URL='http://placeholder.org/releases/$RELEASE_VERSION' -ARCHIVE_DIR_PUBLIC_URL= - -# The path to the main feed. -# The new version is added here when you publish a release. -#MASTER_FEED_FILE="$HOME/public_html/feeds/MyProg.xml" -MASTER_FEED_FILE="%s" - -# A shell command to upload the generated archive file to the -# public server (corresponds to $ARCHIVE_DIR_PUBLIC_URL, which is -# used to download it again). -# If unset, you'll have to upload it yourself. -#ARCHIVE_UPLOAD_COMMAND='scp "$@" me@myhost:/var/www/releases/$RELEASE_VERSION/' -ARCHIVE_UPLOAD_COMMAND= - -# A shell command to upload the master feed ($MASTER_FEED_FILE) and -# related files to your web server. It will be downloaded using the -# feed's URL. If unset, you'll have to upload it yourself. -#MASTER_FEED_UPLOAD_COMMAND='scp "$@" me@myhost:/var/www/feeds/' -MASTER_FEED_UPLOAD_COMMAND= - -# Your public version control repository. When publishing, the new -# HEAD and the release tag will be pushed to this using a command -# such as "git-push main master v0.1" -# If unset, you'll have to update it yourself. -#PUBLIC_SCM_REPOSITORY=origin -PUBLIC_SCM_REPOSITORY= - -cd `dirname "$0"` -exec 0launch %s --release %s \\ - --archive-dir-public-url="$ARCHIVE_DIR_PUBLIC_URL" \\ - --master-feed-file="$MASTER_FEED_FILE" \\ - --archive-upload-command="$ARCHIVE_UPLOAD_COMMAND" \\ - --master-feed-upload-command="$MASTER_FEED_UPLOAD_COMMAND" \\ - --public-scm-repository="$PUBLIC_SCM_REPOSITORY" \\ - "$@" -""" % (master_feed_name, release_uri, feed.local_path)) - make_release.close() - os.chmod('make-release', 0775 & ~umask) - print "Success - created script:\n %s" % os.path.abspath('make-release') - print "Now edit it with your local settings." - print "Then, create new releases by running it." diff --git a/support.py b/support.py deleted file mode 100644 index a567fee..0000000 --- a/support.py +++ /dev/null @@ -1,247 +0,0 @@ -# Copyright (C) 2007, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import copy -import os, subprocess, tarfile, platform -import urlparse, ftplib, httplib -from xml.dom import minidom - -from zeroinstall import SafeException -from zeroinstall.injector import model, qdom, namespaces -from zeroinstall.support import ro_rmtree, portable_rename -from logging import info - -release_status_file = os.path.abspath('release-status') - -def check_call(*args, **kwargs): - exitstatus = subprocess.call(*args, **kwargs) - if exitstatus != 0: - if type(args[0]) in (str, unicode): - cmd = args[0] - else: - cmd = ' '.join(args[0]) - raise SafeException("Command failed with exit code %d:\n%s" % (exitstatus, cmd)) - -def show_and_run(cmd, args): - print "Executing: %s %s" % (cmd, ' '.join("[%s]" % x for x in args)) - check_call(['sh', '-c', cmd, '-'] + args) - -def suggest_release_version(snapshot_version): - """Given a snapshot version, suggest a suitable release version. - >>> suggest_release_version('1.0-pre') - '1.0' - >>> suggest_release_version('0.9-post') - '0.10' - >>> suggest_release_version('3') - Traceback (most recent call last): - ... - SafeException: Version '3' is not a snapshot version (should end in -pre or -post) - """ - version = model.parse_version(snapshot_version) - mod = version[-1] - if mod == 0: - raise SafeException("Version '%s' is not a snapshot version (should end in -pre or -post)" % snapshot_version) - if mod > 0: - # -post, so increment the number - version[-2][-1] += 1 - version[-1] = 0 # Remove the modifier - return model.format_version(version) - -def publish(feed_path, **kwargs): - args = [os.environ['0PUBLISH']] - for k in kwargs: - value = kwargs[k] - if value is True: - args += ['--' + k.replace('_', '-')] - elif value is not None: - if platform.system() == 'Windows': - args += ['--' + k.replace('_', '-') + "='" + value + "'"] - else: - args += ['--' + k.replace('_', '-'), value] - args.append(feed_path) - info("Executing %s", args) - check_call(args) - -def get_singleton_impl(feed): - impls = feed.implementations - if len(impls) != 1: - raise SafeException("Local feed '%s' contains %d versions! I need exactly one!" % (feed.url, len(impls))) - return impls.values()[0] - -def backup_if_exists(name): - if not os.path.exists(name): - return - backup = name + '~' - if os.path.exists(backup): - print "(deleting old backup %s)" % backup - if os.path.isdir(backup): - ro_rmtree(backup) - else: - os.unlink(backup) - portable_rename(name, backup) - print "(renamed old %s as %s; will delete on next run)" % (name, backup) - -def get_choice(options): - while True: - choice = raw_input('/'.join(options) + ': ').lower() - if not choice: continue - for o in options: - if o.lower().startswith(choice): - return o - -def make_archive_name(feed_name, version): - return feed_name.lower().replace(' ', '-') + '-' + version - -def in_PATH(prog): - for x in os.environ['PATH'].split(':'): - if os.path.isfile(os.path.join(x, prog)): - return True - return False - -def show_diff(from_dir, to_dir): - for cmd in [['meld'], ['xxdiff'], ['diff', '-ur']]: - if in_PATH(cmd[0]): - code = os.spawnvp(os.P_WAIT, cmd[0], cmd + [from_dir, to_dir]) - if code: - print "WARNING: command %s failed with exit code %d" % (cmd, code) - return - -class Status(object): - __slots__ = ['old_snapshot_version', 'release_version', 'head_before_release', 'new_snapshot_version', - 'head_at_release', 'created_archive', 'src_tests_passed', 'tagged', 'verified_uploads', 'updated_master_feed'] - def __init__(self): - for name in self.__slots__: - setattr(self, name, None) - - if os.path.isfile(release_status_file): - for line in file(release_status_file): - assert line.endswith('\n') - line = line[:-1] - name, value = line.split('=') - setattr(self, name, value) - info("Loaded status %s=%s", name, value) - - def save(self): - tmp_name = release_status_file + '.new' - tmp = file(tmp_name, 'w') - try: - lines = ["%s=%s\n" % (name, getattr(self, name)) for name in self.__slots__ if getattr(self, name)] - tmp.write(''.join(lines)) - tmp.close() - portable_rename(tmp_name, release_status_file) - info("Wrote status to %s", release_status_file) - except: - os.unlink(tmp_name) - raise - -def host(address): - if hasattr(address, 'hostname'): - return address.hostname - else: - return address[1].split(':', 1)[0] - -def port(address): - if hasattr(address, 'port'): - return address.port - else: - port = address[1].split(':', 1)[1:] - if port: - return int(port[0]) - else: - return None - -def get_http_size(url, ttl = 1): - assert url.lower().startswith('http://') - - address = urlparse.urlparse(url) - http = httplib.HTTPConnection(host(address), port(address) or 80) - - parts = url.split('/', 3) - if len(parts) == 4: - path = parts[3] - else: - path = '' - - http.request('HEAD', '/' + path, headers = {'Host': host(address)}) - response = http.getresponse() - try: - if response.status == 200: - return response.getheader('Content-Length') - elif response.status in (301, 302): - new_url_rel = response.getheader('Location') or response.getheader('URI') - new_url = urlparse.urljoin(url, new_url_rel) - else: - raise SafeException("HTTP error: got status code %s" % response.status) - finally: - response.close() - - if ttl: - info("Resource moved! Checking new URL %s" % new_url) - assert new_url - return get_http_size(new_url, ttl - 1) - else: - raise SafeException('Too many redirections.') - -def get_ftp_size(url): - address = urlparse.urlparse(url) - ftp = ftplib.FTP(host(address)) - try: - ftp.login() - return ftp.size(url.split('/', 3)[3]) - finally: - ftp.close() - -def get_size(url): - scheme = urlparse.urlparse(url)[0].lower() - if scheme.startswith('http'): - return get_http_size(url) - elif scheme.startswith('ftp'): - return get_ftp_size(url) - else: - raise SafeException("Unknown scheme '%s' in '%s'" % (scheme, url)) - -def unpack_tarball(archive_file): - tar = tarfile.open(archive_file, 'r:bz2') - members = [m for m in tar.getmembers() if m.name != 'pax_global_header'] - #tar.extractall('.', members = members) # Python >= 2.5 only - for tarinfo in members: - tarinfo = copy.copy(tarinfo) - tarinfo.mode |= 0600 - tarinfo.mode &= 0755 - tar.extract(tarinfo, '.') - -def load_feed(path): - with open(path, 'rb') as stream: - return model.ZeroInstallFeed(qdom.parse(stream), local_path = path) - -def get_archive_basename(impl): - # "2" means "path" (for Python 2.4) - return os.path.basename(urlparse.urlparse(impl.download_sources[0].url)[2]) - -def make_readonly_recursive(path): - for root, dirs, files in os.walk(path): - for d in dirs + files: - full = os.path.join(root, d) - mode = os.stat(full).st_mode - os.chmod(full, mode & 0o555) - -def get_archive_url(options, release_version, archive): - if not options.archive_dir_public_url: - return archive # Not needed with 0repo - - archive_dir_public_url = options.archive_dir_public_url.replace('$RELEASE_VERSION', release_version) - if not archive_dir_public_url.endswith('/'): - archive_dir_public_url += '/' - return archive_dir_public_url + archive - -def make_archives_relative(feed): - with open(feed, 'rb') as stream: - doc = minidom.parse(stream) - for elem in doc.getElementsByTagNameNS(namespaces.XMLNS_IFACE, 'archive') + doc.getElementsByTagNameNS(namespaces.XMLNS_IFACE, 'file'): - href = elem.getAttribute('href') - assert href, 'Missing href on %r' % elem - if '/' in href: - elem.setAttribute('href', href.rsplit('/', 1)[1]) - with open(feed, 'wb') as stream: - doc.writexml(stream) - stream.write(b'\n') diff --git a/tests/c-prog.tgz b/tests/c-prog.tgz deleted file mode 100644 index ae1f06864c70f65fdef5a00065fb82eec809d6dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 11886 zcwPZ+E|Jk6iwFQE<^4$j1MEFZj2uZ;MnbGAg;v06Z^UF5uI`@7%F62c^vB&^|8~ag z%oy389Y*6{RAyvVj-;2n!ZA5W6GB3T()`m9d_3+ag^xD#YPEHC^+2P`Mv_i_k&J~% zSvrlxej^pnQ}cP^H`q89?ta4yQW3k{7A?GpjxTR zSMbKcyTbRwkHXk@0E%(r3U#O3Nj91AczQ7L0&l{7=7>oM^Ta#_>M#KKl#c|1$yhrU zzTkhUW*BNn zju^Kkm&b$*h|R=HjRHK7gc+XVS7JU|bA;;!f_?AS-Fvt1e8BY9rd~JauRhT0cW=FS z>)O3rkJY6Ix+!6>F*3bnUomG9AVD%0sU@lC3gH+ifQ6P6bemxXusTgDW?eVs_)#2Y z5mP#`D;lkZ}cW=N=^L@DBqT)3aoxMrq(@LZ^dCtDx(q~MM5rs*Lm?8xTQ?MwdJvh+b zw6*;DHRb$5tgY@U{=0})<^MPgpC+$C3Qo`ePN(1BqWBNGulRof zZIS^|nO+V`1

N{$1Df~19LzIoJlj7%Z2zKCQUNT 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 XYc5>4aN)v*%cuDrGO|DR04M+eR-gEB diff --git a/tests/test-repo-actions.tgz b/tests/test-repo-actions.tgz deleted file mode 100644 index 3efa4b6f3b082ffb2c4b4dad2a4d38701ff9376a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 7502 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 diff --git a/tests/test-repo.tgz b/tests/test-repo.tgz deleted file mode 100644 index f96493b1248f799ff096244345437b846f76494d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcwPel00001 literal 8834 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 1: - alltests = testLoader.loadTestsFromNames(sys.argv[1:]) -else: - alltests = unittest.TestSuite() - - suite_names = [f[:-3] for f in os.listdir(my_dir) - if f.startswith('test') and f.endswith('.py')] - suite_names.remove('testall') - suite_names.sort() - - for name in suite_names: - m = __import__(name, globals(), locals(), []) - t = testLoader.loadTestsFromModule(m) - alltests.addTest(t) - -a = unittest.TextTestRunner(verbosity=2).run(alltests) - -if coverage: - coverage.stop() -else: - print "Coverage module not found. Skipping coverage report." - -print "\nResult", a -if not a.wasSuccessful(): - sys.exit(1) - -if coverage: - all_sources = [] - def incl(d): - for x in os.listdir(d): - if x.endswith('.py'): - all_sources.append(os.path.join(d, x)) - incl('..') - coverage.report(all_sources + ['../0publish']) diff --git a/tests/testdocs.py b/tests/testdocs.py deleted file mode 100755 index 01dc3cb..0000000 --- a/tests/testdocs.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -# Copyright (C) 2007, Thomas Leonard -# See the README file for details, or visit http://0install.net. - -import unittest, os, sys -import doctest - -sys.path.insert(0, '..') - -import support - -main_dir = os.path.join(os.path.dirname(__file__), '..') - -suite = unittest.TestSuite() -for x in [support]: - suite.addTest(doctest.DocTestSuite(x)) - -if __name__ == '__main__': - runner = unittest.TextTestRunner(verbosity = 2) - runner.run(suite) diff --git a/tests/testrelease.py b/tests/testrelease.py deleted file mode 100755 index 787216b..0000000 --- a/tests/testrelease.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python -# Copyright (C) 2007, Thomas Leonard -# See the README file for details, or visit http://0install.net. -import sys, os, shutil, tempfile, subprocess, imp -from StringIO import StringIO -import unittest - -from zeroinstall.injector import model, qdom, writer -from zeroinstall.injector.config import load_config -from zeroinstall.support import basedir, ro_rmtree - -sys.path.insert(0, '..') -os.environ['http_proxy'] = 'localhost:1111' # Prevent accidental network access - -import support -import release # (sets sys.path for 0repo) -import repo.cmd - -mydir = os.path.realpath(os.path.dirname(__file__)) -release_feed = mydir + '/../0release.xml' -test_repo = mydir + '/test-repo.tgz' -test_repo_actions = mydir + '/test-repo-actions.tgz' -test_repo_c = mydir + '/c-prog.tgz' -test_gpg = mydir + '/gpg.tgz' - -test_config = """ -[global] -freshness = 0 -auto_approve_keys = True -help_with_testing = True -network_use = full -""" - -CUSTOM_REPO_CONFIG = """ -REPOSITORY_BASE_URL = "http://0install.net/tests/" -ARCHIVES_BASE_URL = "http://TESTING/releases" - -def upload_archives(archives): - for dir_rel_url, files in paths.group_by_target_url_dir(archives): - target_dir = join('..', 'releases', 'archives') # hack: skip dir_rel_url - if not os.path.isdir(target_dir): - os.makedirs(target_dir) - subprocess.check_call(["cp"] + files + [target_dir]) - -def check_new_impl(impl): - pass - -def get_archive_rel_url(archive_basename, impl): - return "{version}/{archive}".format( - version = impl.get_version(), - archive = archive_basename) - -def check_uploaded_archive(archive, url): - pass -""" - -def call_with_output_suppressed(cmd, stdin, expect_failure = False, **kwargs): - #cmd = [cmd[0], '-v'] + cmd[1:] - if stdin: - child = subprocess.Popen(cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE, **kwargs) - else: - child = subprocess.Popen(cmd, stdout = subprocess.PIPE, **kwargs) - stdout, stderr = child.communicate(stdin) - if (child.returncode != 0) == expect_failure: - return stdout, stderr - #print stdout, stderr - raise Exception("Return code %d from %s\nstdout: %s\nstderr: %s" % (child.returncode, cmd, stdout, stderr)) - -def make_releases_dir(src_feed = '../hello/HelloWorld.xml', auto_upload = False): - os.chdir('releases') - call_with_output_suppressed(['0release', src_feed], None) - assert os.path.isfile('make-release') - - lines = file('make-release').readlines() - lines[lines.index('ARCHIVE_DIR_PUBLIC_URL=\n')] = 'ARCHIVE_DIR_PUBLIC_URL=http://TESTING/releases/\\$RELEASE_VERSION\n' - - # Force us to test against this version of 0release - for i, line in enumerate(lines): - if line.startswith('exec 0launch http://0install.net/2007/interfaces/0release.xml --release'): - lines[i] = '0release --release ' + line.split('--release ', 1)[1] - break - else: - assert 0 - - if auto_upload: - os.mkdir('archives') - lines[lines.index('ARCHIVE_UPLOAD_COMMAND=\n')] = 'ARCHIVE_UPLOAD_COMMAND=\'cp "$@" ../archives/\'\n' - - s = file('make-release', 'w') - s.write(''.join(lines)) - s.close() - -class TestRelease(unittest.TestCase): - def setUp(self): - self.tmp = tempfile.mkdtemp(prefix = '0release-') - os.chdir(self.tmp) - support.check_call(['tar', 'xzf', test_gpg]) - os.mkdir('releases') - os.environ['GNUPGHOME'] = self.tmp + '/gpg' - os.chmod(os.environ['GNUPGHOME'], 0700) - - config_dir = os.path.join(self.tmp, 'config') - injector_config = os.path.join(config_dir, '0install.net', 'injector') - os.makedirs(injector_config) - s = open(os.path.join(injector_config, 'global'), 'w') - s.write(test_config) - s.close() - - if 'ZEROINSTALL_PORTABLE_BASE' in os.environ: - del os.environ['ZEROINSTALL_PORTABLE_BASE'] - os.environ['XDG_CONFIG_HOME'] = config_dir - imp.reload(basedir) - assert basedir.xdg_config_home == config_dir - - def tearDown(self): - os.chdir(mydir) - ro_rmtree(self.tmp) - - def testSimple(self): - support.check_call(['tar', 'xzf', test_repo]) - make_releases_dir() - - call_with_output_suppressed(['./make-release', '-k', 'Testing'], '\nP\n\n') - - call_with_output_suppressed(['./make-release', '-k', 'Testing'], '\nP\nY\n\n') - - assert 'Prints "Hello World"' in file('0.1/changelog-0.1').read() - assert 'Prints "Hello World"' not in file('0.2/changelog-0.2').read() - new_v = file('../hello/hello.py').read() - assert '0.2-post' in new_v, new_v - - def testUncommitted(self): - support.check_call(['tar', 'xzf', test_repo_actions]) - make_releases_dir() - - unused, stderr = call_with_output_suppressed(['./make-release', '-k', 'Testing'], None, - expect_failure = True, stderr = subprocess.PIPE) - assert "Uncommitted changes!" in stderr - - def testActions(self): - support.check_call(['tar', 'xzf', test_repo_actions]) - os.chdir('hello') - support.check_call(['git', 'commit', '-a', '-m', 'Added release instructions'], stdout = subprocess.PIPE) - os.chdir('..') - make_releases_dir() - - assert "version = '0.2'\n" not in file('../hello/hello.py').read() - - child = subprocess.Popen(['./make-release', '-k', 'Testing'], stdin = subprocess.PIPE, stdout = subprocess.PIPE) - unused, unused = child.communicate('\nP\n\n') - assert child.returncode == 0 - - assert "version = '0.2'\n" in file('../hello/hello.py').read() - - def testBinaryRelease(self): - support.check_call(['tar', 'xzf', test_repo_c]) - make_releases_dir(src_feed = '../c-prog/c-prog.xml', auto_upload = True) - - call_with_output_suppressed(['./make-release', '-k', 'Testing', '--builders=host'], '\nP\n\n') - - feed = self.get_public_feed('HelloWorld-in-C.xml', 'c-prog.xml') - - assert len(feed.implementations) == 2 - src_impl, = [x for x in feed.implementations.values() if x.arch == '*-src'] - host_impl, = [x for x in feed.implementations.values() if x.arch != '*-src'] - - assert src_impl.main == None - assert host_impl.main == 'hello' - - archives = os.listdir('archives') - assert os.path.basename(src_impl.download_sources[0].url) in archives, src_impl.download_sources[0].url - - host_download = host_impl.download_sources[0] - self.assertEqual('http://TESTING/releases/1.1/helloworld-in-c-linux-x86_64-1.1.tar.bz2', - host_download.url) - host_archive = os.path.basename(host_download.url) - assert host_archive in archives - support.check_call(['tar', 'xjf', os.path.join('archives', host_archive)]) - c = subprocess.Popen(['0launch', os.path.join(host_download.extract, '0install', 'feed.xml')], stdout = subprocess.PIPE) - output, _ = c.communicate() - - self.assertEquals("Hello from C! (version 1.1)\n", output) - - def get_public_feed(self, name, uri_basename): - with open(name, 'rb') as stream: - return model.ZeroInstallFeed(qdom.parse(stream)) - -def run_repo(args): - oldcwd = os.getcwd() - - old_stdout = sys.stdout - sys.stdout = StringIO() - try: - sys.stdin = StringIO('\n') # (simulate a press of Return if needed) - repo.cmd.main(['0repo'] + args) - return sys.stdout.getvalue() - finally: - os.chdir(oldcwd) - sys.stdout = old_stdout - -class TestRepoRelease(TestRelease): - def setUp(self): - TestRelease.setUp(self) - - # Let GPG initialise (it's a bit verbose) - child = subprocess.Popen(['gpg', '-q', '--list-secret-keys'], stdout = subprocess.PIPE, stderr = subprocess.PIPE) - unused, unused = child.communicate() - child.wait() - - run_repo(['create', 'my-repo', 'Testing ']) - os.chdir('my-repo') - - if '0repo-config' in sys.modules: - del sys.modules['0repo-config'] - - with open('0repo-config.py', 'at') as stream: - stream.write(CUSTOM_REPO_CONFIG) - run_repo(['register']) - os.chdir('..') - - def get_public_feed(self, name, uri_basename): - with open(os.path.join(self.tmp, 'my-repo', 'public', uri_basename), 'rb') as stream: - return model.ZeroInstallFeed(qdom.parse(stream)) - -unittest.makeSuite(TestRelease) -unittest.makeSuite(TestRepoRelease) -if __name__ == '__main__': - unittest.main() -- 2.11.4.GIT