bzr-builder-0.7.3/.testr.conf0000644000000000000000000000022411740346202014153 0ustar 00000000000000[DEFAULT] test_command=BZR_PLUGINS_AT=builder@`pwd` bzr selftest -s bp.builder --subunit test_id_option=--load-list $IDFILE test_list_option=--list bzr-builder-0.7.3/COPYING0000644000000000000000000010451311740346202013126 0ustar 00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . bzr-builder-0.7.3/TODO0000644000000000000000000000042711740346202012562 0ustar 00000000000000- Documentation. - Work out how to get tarballs for non-native packages if we want that. - Decide on error on existing target directory vs. re-use and pull changes. - Decide what should happen if you nest in to a directory that exists in the branch that you are nesting in to. bzr-builder-0.7.3/__init__.py0000644000000000000000000001771411740346202014212 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . """The bzr-builder plugin allows you to construct a branch from a 'recipe'. The recipe is a series of pointers to branches and instructions for how they should be combined. There are two ways to combine branches, by merging, and by nesting, allowing much flexibility. A recipe is just a text file that starts with a line such as:: # bzr-builder format 0.3 deb-version 1.0+{revno}-{revno:packaging} The format specifier is there to allow the syntax to be changed in later versions, and the meaning of "deb-version" will be explained later. The next step is the define the base branch, this is the branch that will be places at the root, e.g. just put:: lp:foo to use the trunk of "foo" hosted on Launchpad. Next comes any number of lines of other branches to be merged in, but using a slightly different format. To merge a branch in to the base specify something like:: merge packaging lp:~foo-dev/foo/packaging which specifies we are merging a branch we will refer to as "packaging", which can be found at the given URI. The name you give to the branch as the second item doesn't have to match anything else, it's just an identifier specific to the recipe. If you wish to nest a branch then you use a similar line:: nest artwork lp:foo-images images This specifies that we are nesting the branch at lp:foo-images, which we will call "artwork", and we will place it locally in to the "images" directory. You can then continue in this fashion for as many branches as you like. It is also possible to nest and merge branches into nested branches. For example to merge a branch in to the "artwork" branch we put the following on the line below that one, indented by two spaces:: merge artwork-fixes lp:~bob/foo-images/fix-12345 which will merge Bob's fixes branch into the "artwork" branch which we nested at "images". It is also possible to specify a particular revision of a branch by appending a revisionspec to the line. For instance:: nest docs lp:foo-docs doc tag:1.0 will nest the revision pointed to by the "1.0" tag of that branch. The format for the revisionspec is identical to that taken by the "--revision" argument to many bzr commands. See "bzr help revisionspec" for details. You can also merge specific subdirectories from a branch with a "nest-part" line like nest-part packaging lp:~foo-dev/foo/packaging debian which specifies that the only the debian/ subdirectory should be merged. This works even if the branches share no revision history. You can optionally specify the subdirectory and revision in the target with a line like nest-part libfoo lp:libfoo src lib/foo tag:release-1.2 which will put the "src" directory of libfoo in "lib/foo", using the revision of libfoo tagged "release-1.2". It is also possible to run an arbitrary command at a particular point in the construction process. For example:: run autoreconf -i will run autotools at a particular point. Doing things with branches is usually preferred, but sometimes it is the easier or only way to achieve something. Note that you usually shouldn't rely on having general Internet access when assembling the recipe, so commands that require it should be avoided. You can then build this branch by running:: bzr build foo.recipe working-dir (assuming you saved it as foo.recipe in your current directory). Once the command finished it will have placed the result in "working-dir". It is also possible to produce Debian source packages from a recipe, assuming that one of the branches in the recipe contains some appropriate packaging. You can do this using the "bzr dailydeb" command, which takes the same arguments as "build". Only this time in the working dir you will find a source package and a directory containing the code that the packages was built from once it is done. Also take a look at the "--key-id" and "--dput" arguments to have "bzr dailydeb" sign and upload the source package somewhere. To build Debian source package that you desire you should make sure that "deb-version" is set to an appropriate value on the first line of your recipe. This will be used as the version number of the package. The value you put there also allows for substitution of values in to it based on various things when the recipe is processed: * {time} will be substituted with the current date and time, such as 200908191512. * {date} will be substituted with just the current date, such as 20090819. * {revno} will be the revno of the base branch (the first specified). * {revno:} will be substituted with the revno for the branch named in the recipe. * {debupstream}/{debupstream:} will be replaced by the upstream portion of the version number taken from debian/changelog in the branch. For example, if debian/changelog has a version number of "1.0-1" then this would evaluate to "1.0". * {debupstream-base}/{debupstream-base:} will be replaced by the upstream portion of the version number taken from debian/changelog in the branch, with any VCS markers stripped. For example, if debian/changelog has a version number of "1.0~bzr43-1" then this would evaluate to "1.0~". For any upstream versions without a VCS marker, a "+" is added to the version ("1.0-1" becomes "1.0+"). * {debversion}/{debversion:} will be substituted with the exact version string from debian/changelog in the branch. * {revtime}/{revtime:} will be substituted with the date and time of the revision that was built, such as 201108191512. * {revdate}/{revdate:} will be substituted with the date of the revision that was built, such as 20111222. * {latest-tag}/{latest-tag:} will be replaced with the name of the tag found on the most recent revision in the branch mainline that has a tag. * {git-commit}/{git-commit:} will be substituted with the last 7 characters of the SHA1 checksum of the revision that was built, if the revision was imported from a Git repository. * {svn-revno}/{svn-revno:} will be substituted with the Subversion revision number of the revision that was built, if the revision was imported from a Subversion repository. Instruction syntax summary: * nest NAME BRANCH TARGET-DIR [REVISION] * merge NAME BRANCH [REVISION] * nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]] * run COMMAND Format versions: 0.1 - original format. 0.2 - added "run" instruction. 0.3 - added "nest-part" instruction. 0.4 - made "deb-version" optional, added several new substitution variables. {debupstream} now only looks for changelog in the root branch, not the resulting tree """ from __future__ import absolute_import from bzrlib.plugins.builder.info import ( bzr_plugin_version as version_info, ) if version_info[3] == 'final': version_string = '%d.%d.%d' % version_info[:3] else: version_string = '%d.%d.%d%s%d' % version_info __version__ = version_string from bzrlib.commands import plugin_cmds plugin_cmds.register_lazy("cmd_build", [], "bzrlib.plugins.builder.cmds") plugin_cmds.register_lazy("cmd_dailydeb", [], "bzrlib.plugins.builder.cmds") def test_suite(): from unittest import TestSuite from bzrlib.plugins.builder import tests result = TestSuite() result.addTest(tests.test_suite()) return result bzr-builder-0.7.3/backports.py0000644000000000000000000002673311740346202014444 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2010 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . """Copies/backports features from more recent versions of packages This allows bzr-builder to continue to work with older bzrlib and python-debian versions while using features from newer versions. """ # NOTE FOR DEVELOPERS: with each backport please include a comment saying which # version of what you are backporting from, to make it easier to copy bugfixes # made in later version (and so that we know which things we can drop from this # module if bzr-builder ever raises which package versions it depends on). import os import pwd import re import socket from bzrlib.merge import ( Merger, Merge3Merger, ) from bzrlib import ( errors, generate_ids, osutils, revision as _mod_revision, transform, ui, ) # Backport of bzrlib.merge.MergeIntoMerger, introduced in bzr 2.2rc1. # (Also backports PathNotInTree, _MergeTypeParameterizer, MergeIntoMergeType) class PathNotInTree(errors.BzrError): _fmt = """Merge-into failed because %(tree)s does not contain %(path)s.""" def __init__(self, path, tree): errors.BzrError.__init__(self, path=path, tree=tree) class MergeIntoMerger(Merger): """Merger that understands other_tree will be merged into a subdir. This also changes the Merger api so that it uses real Branch, revision_id, and RevisonTree objects, rather than using revision specs. """ def __init__(self, this_tree, other_branch, other_tree, target_subdir, source_subpath, other_rev_id=None): """Create a new MergeIntoMerger object. source_subpath in other_tree will be effectively copied to target_subdir in this_tree. :param this_tree: The tree that we will be merging into. :param other_branch: The Branch we will be merging from. :param other_tree: The RevisionTree object we want to merge. :param target_subdir: The relative path where we want to merge other_tree into this_tree :param source_subpath: The relative path specifying the subtree of other_tree to merge into this_tree. """ # It is assumed that we are merging a tree that is not in our current # ancestry, which means we are using the "EmptyTree" as our basis. null_ancestor_tree = this_tree.branch.repository.revision_tree( _mod_revision.NULL_REVISION) super(MergeIntoMerger, self).__init__( this_branch=this_tree.branch, this_tree=this_tree, other_tree=other_tree, base_tree=null_ancestor_tree, ) self._target_subdir = target_subdir self._source_subpath = source_subpath self.other_branch = other_branch if other_rev_id is None: other_rev_id = other_tree.get_revision_id() self.other_rev_id = self.other_basis = other_rev_id self.base_is_ancestor = True self.backup_files = True self.merge_type = Merge3Merger self.show_base = False self.reprocess = False self.interesting_ids = None self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType, target_subdir=self._target_subdir, source_subpath=self._source_subpath) if self._source_subpath != '': # If this isn't a partial merge make sure the revisions will be # present. self._maybe_fetch(self.other_branch, self.this_branch, self.other_basis) def set_pending(self): if self._source_subpath != '': return Merger.set_pending(self) class _MergeTypeParameterizer(object): """Wrap a merge-type class to provide extra parameters. This is hack used by MergeIntoMerger to pass some extra parameters to its merge_type. Merger.do_merge() sets up its own set of parameters to pass to the 'merge_type' member. It is difficult override do_merge without re-writing the whole thing, so instead we create a wrapper which will pass the extra parameters. """ def __init__(self, merge_type, **kwargs): self._extra_kwargs = kwargs self._merge_type = merge_type def __call__(self, *args, **kwargs): kwargs.update(self._extra_kwargs) return self._merge_type(*args, **kwargs) def __getattr__(self, name): return getattr(self._merge_type, name) class MergeIntoMergeType(Merge3Merger): """Merger that incorporates a tree (or part of a tree) into another.""" # Backport note: the definition of _finish_computing_transform is copied # from Merge3Merger in bzr 2.2 (it is supposed to be inherited from # Merge3Merger, but was only introduced in 2.2). def _finish_computing_transform(self): """Finalize the transform and report the changes. This is the second half of _compute_transform. """ child_pb = ui.ui_factory.nested_progress_bar() try: fs_conflicts = transform.resolve_conflicts(self.tt, child_pb, lambda t, c: transform.conflict_pass(t, c, self.other_tree)) finally: child_pb.finished() if self.change_reporter is not None: from bzrlib import delta delta.report_changes( self.tt.iter_changes(), self.change_reporter) self.cook_conflicts(fs_conflicts) from bzrlib import trace for conflict in self.cooked_conflicts: trace.warning(conflict) def __init__(self, *args, **kwargs): """Initialize the merger object. :param args: See Merge3Merger.__init__'s args. :param kwargs: See Merge3Merger.__init__'s keyword args, except for source_subpath and target_subdir. :keyword source_subpath: The relative path specifying the subtree of other_tree to merge into this_tree. :keyword target_subdir: The relative path where we want to merge other_tree into this_tree """ # All of the interesting work happens during Merge3Merger.__init__(), # so we have have to hack in to get our extra parameters set. self._source_subpath = kwargs.pop('source_subpath') self._target_subdir = kwargs.pop('target_subdir') super(MergeIntoMergeType, self).__init__(*args, **kwargs) def _compute_transform(self): child_pb = ui.ui_factory.nested_progress_bar() try: entries = self._entries_to_incorporate() entries = list(entries) for num, (entry, parent_id) in enumerate(entries): child_pb.update('Preparing file merge', num, len(entries)) parent_trans_id = self.tt.trans_id_file_id(parent_id) trans_id = transform.new_by_entry(self.tt, entry, parent_trans_id, self.other_tree) finally: child_pb.finished() self._finish_computing_transform() def _entries_to_incorporate(self): """Yields pairs of (inventory_entry, new_parent).""" other_inv = self.other_tree.inventory subdir_id = other_inv.path2id(self._source_subpath) if subdir_id is None: # XXX: The error would be clearer if it gave the URL of the source # branch, but we don't have a reference to that here. raise PathNotInTree(self._source_subpath, "Source tree") subdir = other_inv[subdir_id] parent_in_target = osutils.dirname(self._target_subdir) target_id = self.this_tree.inventory.path2id(parent_in_target) if target_id is None: raise PathNotInTree(self._target_subdir, "Target tree") name_in_target = osutils.basename(self._target_subdir) merge_into_root = subdir.copy() merge_into_root.name = name_in_target if merge_into_root.file_id in self.this_tree.inventory: # Give the root a new file-id. # This can happen fairly easily if the directory we are # incorporating is the root, and both trees have 'TREE_ROOT' as # their root_id. Users will expect this to Just Work, so we # change the file-id here. # Non-root file-ids could potentially conflict too. That's really # an edge case, so we don't do anything special for those. We let # them cause conflicts. merge_into_root.file_id = generate_ids.gen_file_id(name_in_target) yield (merge_into_root, target_id) if subdir.kind != 'directory': # No children, so we are done. return for ignored_path, entry in other_inv.iter_entries_by_dir(subdir_id): parent_id = entry.parent_id if parent_id == subdir.file_id: # The root's parent ID has changed, so make sure children of # the root refer to the new ID. parent_id = merge_into_root.file_id yield (entry, parent_id) def get_maintainer(): """Create maintainer string using the same algorithm as in dch. From version 0.1.19 python-debian has this function in debian.changelog """ env = os.environ regex = re.compile(r"^(.*)\s+<(.*)>$") # Split email and name if 'DEBEMAIL' in env: match_obj = regex.match(env['DEBEMAIL']) if match_obj: if not 'DEBFULLNAME' in env: env['DEBFULLNAME'] = match_obj.group(1) env['DEBEMAIL'] = match_obj.group(2) if 'DEBEMAIL' not in env or 'DEBFULLNAME' not in env: if 'EMAIL' in env: match_obj = regex.match(env['EMAIL']) if match_obj: if not 'DEBFULLNAME' in env: env['DEBFULLNAME'] = match_obj.group(1) env['EMAIL'] = match_obj.group(2) # Get maintainer's name if 'DEBFULLNAME' in env: maintainer = env['DEBFULLNAME'] elif 'NAME' in env: maintainer = env['NAME'] else: # Use password database if no data in environment variables try: maintainer = re.sub(r',.*', '', pwd.getpwuid(os.getuid()).pw_gecos) except (KeyError, AttributeError): # TBD: Use last changelog entry value maintainer = "bzr-builder" # Get maintainer's mail address if 'DEBEMAIL' in env: email = env['DEBEMAIL'] elif 'EMAIL' in env: email = env['EMAIL'] else: addr = None if os.path.exists('/etc/mailname'): f = open('/etc/mailname') try: addr = f.readline().strip() finally: f.close() if not addr: addr = socket.getfqdn() if addr: user = pwd.getpwuid(os.getuid()).pw_name if not user: addr = None else: addr = "%s@%s" % (user, addr) if addr: email = addr else: # TBD: Use last changelog entry value email = "none@example.org" return (maintainer, email) bzr-builder-0.7.3/cmds.py0000644000000000000000000004337011740346202013376 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . """Subcommands provided by bzr-builder.""" from StringIO import StringIO import datetime import os import shutil import tempfile from bzrlib import ( errors, lazy_regex, trace, transport as _mod_transport, urlutils, ) from bzrlib.branch import Branch from bzrlib.commands import Command from bzrlib.option import Option from bzrlib.plugins.builder.recipe import ( BaseRecipeBranch, build_tree, RecipeParser, resolve_revisions, SAFE_INSTRUCTIONS, ) def write_manifest_to_transport(location, base_branch, possible_transports=None): """Write a manifest to disk. :param location: Location to write to :param base_branch: Recipe base branch """ child_transport = _mod_transport.get_transport(location, possible_transports=possible_transports) base_transport = child_transport.clone('..') base_transport.create_prefix() basename = base_transport.relpath(child_transport.base) base_transport.put_bytes(basename, str(base_branch)) def get_branch_from_recipe_location(recipe_location, safe=False, possible_transports=None): """Return the base branch for the specified recipe. :param recipe_location: The URL of the recipe file to retrieve. :param safe: if True, reject recipes that would cause arbitrary code execution. """ if safe: permitted_instructions = SAFE_INSTRUCTIONS else: permitted_instructions = None try: (basename, f) = get_recipe_from_location(recipe_location, possible_transports) except errors.NoSuchFile: raise errors.BzrCommandError("Specified recipe does not exist: " "%s" % recipe_location) try: parser = RecipeParser(f, filename=recipe_location) finally: f.close() return parser.parse(permitted_instructions=permitted_instructions) def get_branch_from_branch_location(branch_location, possible_transports=None, revspec=None): """Return the base branch for the branch location. :param branch_location: The URL of the branch to retrieve. """ # Make sure it's actually a branch Branch.open(branch_location) return BaseRecipeBranch(branch_location, None, RecipeParser.NEWEST_VERSION, revspec=revspec) def get_old_recipe(if_changed_from, possible_transports=None): try: (basename, f) = get_recipe_from_location(if_changed_from, possible_transports) except errors.NoSuchFile: return None try: old_recipe = RecipeParser(f, filename=if_changed_from).parse() finally: f.close() return old_recipe launchpad_recipe_re = lazy_regex.lazy_compile( r'^https://code.launchpad.net/~(.*)/\+recipe/(.*)$') def get_recipe_from_launchpad(username, recipe_name, location): """Load a recipe from Launchpad. :param username: The launchpad user name :param recipe_name: Recipe name :param location: Original location (used for error reporting) :return: Text of the recipe """ from launchpadlib.launchpad import Launchpad lp = Launchpad.login_with("bzr-builder", "production") try: person = lp.people[username] except KeyError: raise errors.NoSuchFile(location, "No such Launchpad user %s" % username) recipe = person.getRecipe(name=recipe_name) if recipe is None: raise errors.NoSuchFile(location, "Launchpad user %s has no recipe %s" % ( username, recipe_name)) return recipe.recipe_text def get_recipe_from_location(location, possible_transports=None): """Open a recipe as a file-like object from a URL. :param location: The recipe location :param possible_transports: Possible transports to use :return: Tuple with basename and file-like object """ m = launchpad_recipe_re.match(location) if m: (username, recipe_name) = m.groups() text = get_recipe_from_launchpad(username, recipe_name, location) return (recipe_name, StringIO(text)) child_transport = _mod_transport.get_transport(location, possible_transports=possible_transports) recipe_transport = child_transport.clone('..') basename = recipe_transport.relpath(child_transport.base) return basename, recipe_transport.get(basename) def get_prepared_branch_from_location(location, safe=False, possible_transports=None, revspec=None): """Common code to prepare a branch and do substitutions. :param location: a path to a recipe file or branch to work from. :param if_changed_from: an optional location of a manifest to compare the recipe against. :param safe: if True, reject recipes that would cause arbitrary code execution. :return: A tuple with (retcode, base_branch). If retcode is None then the command execution should continue. """ try: base_branch = get_branch_from_recipe_location(location, safe=safe, possible_transports=possible_transports) except (_mod_transport.LateReadError, errors.ReadError): # Presume unable to read means location is a directory rather than a file base_branch = get_branch_from_branch_location(location, possible_transports=possible_transports) else: if revspec is not None: raise errors.BzrCommandError("--revision only supported when " "building from branch") return base_branch class cmd_build(Command): """Build a tree based on a branch or a recipe. Pass the path of a recipe file or a branch to build and the directory to work in. See "bzr help builder" for more information on what a recipe is. """ takes_args = ["location", "working_directory"] takes_options = [ Option('manifest', type=str, argname="path", help="Path to write the manifest to."), Option('if-changed-from', type=str, argname="path", help="Only build if the outcome would be different " "to that specified in the specified manifest."), 'revision', ] def run(self, location, working_directory, manifest=None, if_changed_from=None, revision=None): if revision is not None and len(revision) > 0: if len(revision) != 1: raise errors.BzrCommandError("only a single revision can be " "specified") revspec = revision[0] else: revspec = None possible_transports = [] base_branch = get_prepared_branch_from_location(location, possible_transports=possible_transports, revspec=revspec) if if_changed_from is not None: old_recipe = get_old_recipe(if_changed_from, possible_transports) else: old_recipe = None changed = resolve_revisions(base_branch, if_changed_from=old_recipe) if not changed: trace.note("Unchanged") return 0 manifest_path = manifest or os.path.join(working_directory, "bzr-builder.manifest") build_tree(base_branch, working_directory) write_manifest_to_transport(manifest_path, base_branch, possible_transports) class cmd_dailydeb(Command): """Build a deb based on a 'recipe' or from a branch. See "bzr help builder" for more information on what a recipe is. If you do not specify a working directory then a temporary directory will be used and it will be removed when the command finishes. """ takes_options = cmd_build.takes_options + [ Option("package", type=str, help="The package name to use in the changelog entry. " "If not specified then the package from the " "previous changelog entry will be used, so it " "must be specified if there is no changelog."), Option("distribution", type=str, help="The distribution to target. If not specified " "then the same distribution as the last entry " "in debian/changelog will be used."), Option("dput", type=str, argname="target", help="dput the built package to the specified " "dput target."), Option("key-id", type=str, short_name="k", help="Sign the packages with the specified GnuPG key. " "Must be specified if you use --dput."), Option("no-build", help="Just ready the source package and don't " "actually build it."), Option("watch-ppa", help="Watch the PPA the package was " "dput to and exit with 0 only if it builds and " "publishes successfully."), Option("append-version", type=str, help="Append the " "specified string to the end of the version used " "in debian/changelog."), Option("safe", help="Error if the recipe would cause" " arbitrary code execution."), Option("allow-fallback-to-native", help="Allow falling back to a native package if the upstream " "tarball can not be found."), ] takes_args = ["location", "working_basedir?"] def run(self, location, working_basedir=None, manifest=None, if_changed_from=None, package=None, distribution=None, dput=None, key_id=None, no_build=None, watch_ppa=False, append_version=None, safe=False, allow_fallback_to_native=False): try: try: import debian except ImportError: # In older versions of python-debian the main package was named # debian_bundle import debian_bundle except ImportError: raise errors.BzrCommandError("The 'debian' python module " "is required for 'bzr dailydeb'. Install the " "python-debian package.") from bzrlib.plugins.builder.deb_util import ( add_autobuild_changelog_entry, build_source_package, calculate_package_dir, changelog, debian_source_package_name, dput_source_package, extract_upstream_tarball, force_native_format, get_source_format, sign_source_package, target_from_dput, ) from bzrlib.plugins.builder.deb_version import ( check_expanded_deb_version, substitute_branch_vars, substitute_time, ) if dput is not None and key_id is None: raise errors.BzrCommandError("You must specify --key-id if you " "specify --dput.") if watch_ppa: if not dput: raise errors.BzrCommandError( "cannot watch a ppa without doing dput.") else: # Check we can calculate a PPA url. target_from_dput(dput) possible_transports = [] base_branch = get_prepared_branch_from_location(location, safe=safe, possible_transports=possible_transports) # Save the unsubstituted version template_version = base_branch.deb_version if if_changed_from is not None: old_recipe = get_old_recipe(if_changed_from, possible_transports) else: old_recipe = None if base_branch.deb_version is not None: time = datetime.datetime.utcnow() substitute_time(base_branch, time) changed = resolve_revisions(base_branch, if_changed_from=old_recipe, substitute_branch_vars=substitute_branch_vars) check_expanded_deb_version(base_branch) else: changed = resolve_revisions(base_branch, if_changed_from=old_recipe) if not changed: trace.note("Unchanged") return 0 if working_basedir is None: temp_dir = tempfile.mkdtemp(prefix="bzr-builder-") working_basedir = temp_dir else: temp_dir = None if not os.path.exists(working_basedir): os.makedirs(working_basedir) package_name = self._calculate_package_name(location, package) if template_version is None: working_directory = os.path.join(working_basedir, "%s-direct" % (package_name,)) else: working_directory = os.path.join(working_basedir, "%s-%s" % (package_name, template_version)) try: # we want to use a consistent package_dir always to support # updates in place, but debuild etc want PACKAGE-UPSTREAMVERSION # on disk, so we build_tree with the unsubstituted version number # and do a final rename-to step before calling into debian build # tools. We then rename the working dir back. manifest_path = os.path.join(working_directory, "debian", "bzr-builder.manifest") build_tree(base_branch, working_directory) control_path = os.path.join(working_directory, "debian", "control") if not os.path.exists(control_path): if package is None: raise errors.BzrCommandError("No control file to " "take the package name from, and --package not " "specified.") else: package = debian_source_package_name(control_path) write_manifest_to_transport(manifest_path, base_branch, possible_transports) autobuild = (base_branch.deb_version is not None) if autobuild: # Add changelog also substitutes {debupstream}. add_autobuild_changelog_entry(base_branch, working_directory, package, distribution=distribution, append_version=append_version) else: if append_version: raise errors.BzrCommandError("--append-version only " "supported for autobuild recipes (with a 'deb-version' " "header)") with open(os.path.join(working_directory, "debian", "changelog")) as cl_f: contents = cl_f.read() cl = changelog.Changelog(file=contents) package_name = cl.package package_version = cl.version package_dir = calculate_package_dir(package_name, package_version, working_basedir) # working_directory -> package_dir: after this debian stuff works. os.rename(working_directory, package_dir) try: current_format = get_source_format(package_dir) if (package_version.debian_version is not None or current_format == "3.0 (quilt)"): # Non-native package try: extract_upstream_tarball(base_branch.branch, package_name, package_version.upstream_version, working_basedir) except errors.NoSuchTag, e: if not allow_fallback_to_native: raise errors.BzrCommandError( "Unable to find the upstream source. Import it " "as tag %s or build with " "--allow-fallback-to-native." % e.tag_name) else: force_native_format(package_dir, current_format) if not no_build: build_source_package(package_dir, tgz_check=not allow_fallback_to_native) if key_id is not None: sign_source_package(package_dir, key_id) if dput is not None: dput_source_package(package_dir, dput) finally: # package_dir -> working_directory # FIXME: may fail in error unwind, masking the original exception. os.rename(package_dir, working_directory) # Note that this may write a second manifest. if manifest is not None: write_manifest_to_transport(manifest, base_branch, possible_transports) finally: if temp_dir is not None: shutil.rmtree(temp_dir) if watch_ppa: from bzrlib.plugins.builder.ppa import watch (owner, archive) = target_from_dput(dput) if not watch(owner, archive, package_name, base_branch.deb_version): return 2 def _calculate_package_name(self, recipe_location, package): """Calculate the directory name that should be used while debuilding.""" recipe_name = urlutils.basename(recipe_location) if recipe_name.endswith(".recipe"): recipe_name = recipe_name[:-len(".recipe")] return package or recipe_name bzr-builder-0.7.3/deb_util.py0000644000000000000000000003214511740346202014235 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009-2011 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . """Debian-specific utility functions.""" from base64 import standard_b64decode from email import utils import errno import os import shutil import signal import subprocess from bzrlib import ( errors, export as _mod_export, osutils, trace, ) from bzrlib.plugins.builder.deb_version import substitute_changelog_vars from bzrlib.plugins.builder.recipe import ( SubstitutionUnavailable, ) try: from debian import changelog, deb822 except ImportError: # In older versions of python-debian the main package was named # debian_bundle from debian_bundle import changelog, deb822 try: get_maintainer = changelog.get_maintainer except AttributeError: # Implementation of get_maintainer was added after 0.1.18 so import same # function from backports module if python-debian doesn't have it. from bzrlib.plugins.builder.backports import get_maintainer # The default distribution used by add_autobuild_changelog_entry() DEFAULT_UBUNTU_DISTRIBUTION = "lucid" class MissingDependency(errors.BzrError): pass def target_from_dput(dput): """Convert a dput specification to a LP API specification. :param dput: A dput command spec like ppa:team-name. :return: A LP API target like team-name/ppa. """ ppa_prefix = 'ppa:' if not dput.startswith(ppa_prefix): raise errors.BzrCommandError('%r does not appear to be a PPA. ' 'A dput target like \'%suser[/name]\' must be used.' % (dput, ppa_prefix)) base, _, suffix = dput[len(ppa_prefix):].partition('/') if not suffix: suffix = 'ppa' return base, suffix def debian_source_package_name(control_path): """Open a debian control file and extract the package name. """ f = open(control_path, 'r') try: control = deb822.Deb822(f) # Debian policy states package names are [a-z0-9][a-z0-9.+-]+ so ascii return control["Source"].encode("ascii") finally: f.close() def reconstruct_pristine_tar(dest, delta, dest_filename): """Reconstruct a pristine tarball from a directory and a delta. :param dest: Directory to pack :param delta: pristine-tar delta :param dest_filename: Destination filename """ command = ["pristine-tar", "gentar", "-", os.path.abspath(dest_filename)] _run_command(command, dest, "Reconstructing pristine tarball", "Generating tar from delta failed", not_installed_msg="pristine-tar is not installed", indata=delta) def extract_upstream_tarball(branch, package, version, dest_dir): """Extract the upstream tarball from a branch. :param branch: Branch with the upstream pristine tar data :param package: Package name :param version: Package version :param dest_dir: Destination directory """ tag_names = ["upstream-%s" % version, "upstream/%s" % version] for tag_name in tag_names: try: revid = branch.tags.lookup_tag(tag_name) except errors.NoSuchTag: pass else: break else: raise errors.NoSuchTag(tag_names[0]) tree = branch.repository.revision_tree(revid) rev = branch.repository.get_revision(revid) if 'deb-pristine-delta' in rev.properties: uuencoded = rev.properties['deb-pristine-delta'] dest_filename = "%s_%s.orig.tar.gz" % (package, version) elif 'deb-pristine-delta-bz2' in rev.properties: uuencoded = rev.properties['deb-pristine-delta-bz2'] dest_filename = "%s_%s.orig.tar.bz2" % (package, version) else: uuencoded = None if uuencoded is not None: delta = standard_b64decode(uuencoded) dest = os.path.join(dest_dir, "orig") try: _mod_export.export(tree, dest, format='dir') reconstruct_pristine_tar(dest, delta, os.path.join(dest_dir, dest_filename)) finally: if os.path.exists(dest): shutil.rmtree(dest) else: # Default to .tar.gz dest_filename = "%s_%s.orig.tar.gz" % (package, version) _mod_export.export(tree, os.path.join(dest_dir, dest_filename), per_file_timestamps=True) def add_autobuild_changelog_entry(base_branch, basedir, package, distribution=None, author_name=None, author_email=None, append_version=None): """Add a new changelog entry for an autobuild. :param base_branch: Recipe base branch :param basedir: Base working directory :param package: package name :param distribution: Optional distribution (defaults to last entry distribution) :param author_name: Name of the build requester :param author_email: Email of the build requester :param append_version: Optional version suffix to add """ debian_dir = os.path.join(basedir, "debian") if not os.path.exists(debian_dir): os.makedirs(debian_dir) cl_path = os.path.join(debian_dir, "changelog") file_found = False if os.path.exists(cl_path): file_found = True cl_f = open(cl_path) try: contents = cl_f.read() finally: cl_f.close() cl = changelog.Changelog(file=contents) else: cl = changelog.Changelog() if len(cl._blocks) > 0: if distribution is None: distribution = cl._blocks[0].distributions.split()[0] else: if file_found: if len(contents.strip()) > 0: reason = ("debian/changelog didn't contain any " "parseable stanzas") else: reason = "debian/changelog was empty" else: reason = "debian/changelog was not present" if distribution is None: distribution = DEFAULT_UBUNTU_DISTRIBUTION if base_branch.format in (0.1, 0.2, 0.3): try: substitute_changelog_vars(base_branch, None, cl) except SubstitutionUnavailable, e: raise errors.BzrCommandError("No previous changelog to " "take the upstream version from as %s was " "used: %s: %s." % (e.name, e.reason, reason)) # Use debian packaging environment variables # or default values if they don't exist if author_name is None or author_email is None: author_name, author_email = get_maintainer() # The python-debian package breaks compatibility at version 0.1.20 by # switching to expecting (but not checking for) unicode rather than # bytestring inputs. Detect this and decode environment if needed. if getattr(changelog.Changelog, "__unicode__", None) is not None: enc = osutils.get_user_encoding() author_name = author_name.decode(enc) author_email = author_email.decode(enc) author = "%s <%s>" % (author_name, author_email) date = utils.formatdate(localtime=True) version = base_branch.deb_version if append_version is not None: version += append_version try: changelog.Version(version) except (changelog.VersionError, ValueError), e: raise errors.BzrCommandError("Invalid deb-version: %s: %s" % (version, e)) cl.new_block(package=package, version=version, distributions=distribution, urgency="low", changes=['', ' * Auto build.', ''], author=author, date=date) cl_f = open(cl_path, 'wb') try: cl.write_to_open_file(cl_f) finally: cl_f.close() def calculate_package_dir(package_name, package_version, working_basedir): """Calculate the directory name that should be used while debuilding. :param base_branch: Recipe base branch :param package_version: Version of the package :param package_name: Package name :param working_basedir: Base directory """ package_basedir = "%s-%s" % (package_name, package_version.upstream_version) package_dir = os.path.join(working_basedir, package_basedir) return package_dir def _run_command(command, basedir, msg, error_msg, not_installed_msg=None, env=None, success_exit_codes=None, indata=None): """ Run a command in a subprocess. :param command: list with command and parameters :param msg: message to display to the user :param error_msg: message to display if something fails. :param not_installed_msg: the message to display if the command isn't available. :param env: Optional environment to use rather than os.environ. :param success_exit_codes: Exit codes to consider succesfull, defaults to [0]. :param indata: Data to write to standard input """ def subprocess_setup(): signal.signal(signal.SIGPIPE, signal.SIG_DFL) trace.note(msg) # Hide output if -q is in use. quiet = trace.is_quiet() if quiet: kwargs = {"stderr": subprocess.STDOUT, "stdout": subprocess.PIPE} else: kwargs = {} if env is not None: kwargs["env"] = env trace.mutter("running: %r", command) try: proc = subprocess.Popen(command, cwd=basedir, stdin=subprocess.PIPE, preexec_fn=subprocess_setup, **kwargs) except OSError, e: if e.errno != errno.ENOENT: raise if not_installed_msg is None: raise raise MissingDependency(msg=not_installed_msg) output = proc.communicate(indata) if success_exit_codes is None: success_exit_codes = [0] if proc.returncode not in success_exit_codes: if quiet: raise errors.BzrCommandError("%s: %s" % (error_msg, output)) else: raise errors.BzrCommandError(error_msg) def build_source_package(basedir, tgz_check=True): command = ["/usr/bin/debuild"] if tgz_check: command.append("--tgz-check") else: command.append("--no-tgz-check") command.extend(["-i", "-I", "-S", "-uc", "-us"]) _run_command(command, basedir, "Building the source package", "Failed to build the source package", not_installed_msg="debuild is not installed, please install " "the devscripts package.") def get_source_format(path): """Retrieve the source format name from a package. :param path: Path to the package :return: String with package format """ source_format_path = os.path.join(path, "debian", "source", "format") if not os.path.exists(source_format_path): return "1.0" f = open(source_format_path, 'r') try: return f.read().strip() finally: f.close() def convert_3_0_quilt_to_native(path): """Convert a package in 3.0 (quilt) format to 3.0 (native). This applies all patches in the package and updates the debian/source/format file. :param path: Path to the package on disk """ path = os.path.abspath(path) patches_dir = os.path.join(path, "debian", "patches") series_file = os.path.join(patches_dir, "series") if os.path.exists(series_file): _run_command(["quilt", "push", "-a", "-v"], path, "Applying quilt patches", "Failed to apply quilt patches", not_installed_msg="quilt is not installed, please install it.", env={"QUILT_SERIES": series_file, "QUILT_PATCHES": patches_dir}, success_exit_codes=(0, 2)) if os.path.exists(patches_dir): shutil.rmtree(patches_dir) f = open(os.path.join(path, "debian", "source", "format"), 'w') try: f.write("3.0 (native)\n") finally: f.close() def force_native_format(working_tree_path, current_format): """Make sure a package is a format that supports native packages. :param working_tree_path: Path to the package """ if current_format == "3.0 (quilt)": convert_3_0_quilt_to_native(working_tree_path) elif current_format not in ("1.0", "3.0 (native)"): raise errors.BzrCommandError("Unknown source format %s" % current_format) def sign_source_package(basedir, key_id): command = ["/usr/bin/debsign", "-S", "-k%s" % key_id] _run_command(command, basedir, "Signing the source package", "Signing the package failed", not_installed_msg="debsign is not installed, please install " "the devscripts package.") def dput_source_package(basedir, target): command = ["/usr/bin/debrelease", "-S", "--dput", target] _run_command(command, basedir, "Uploading the source package", "Uploading the package failed", not_installed_msg="debrelease is not installed, please " "install the devscripts package.") bzr-builder-0.7.3/deb_version.py0000644000000000000000000001754611740346202014755 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009-2011 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . from bzrlib import ( errors, lazy_regex, ) from bzrlib.plugins.builder.recipe import ( BranchSubstitutionVariable, DateVariable, GitCommitVariable, LatestTagVariable, RevdateVariable, RevtimeVariable, RevnoVariable, SubstitutionUnavailable, SubversionRevnumVariable, TimeVariable, branch_vars, simple_vars, ) try: from debian import changelog except ImportError: # In older versions of python-debian the main package was named # debian_bundle from debian_bundle import changelog class DebUpstreamVariable(BranchSubstitutionVariable): basename = "debupstream" minimum_format = 0.1 def __init__(self, branch_name, version): super(DebUpstreamVariable, self).__init__(branch_name) self._version = version @classmethod def from_changelog(cls, branch_name, changelog): if len(changelog._blocks) > 0: return cls(branch_name, changelog._blocks[0].version) else: return cls(branch_name, None) def get(self): if self._version is None: raise SubstitutionUnavailable(self.name, "No previous changelog to take the upstream version from") # Should we include the epoch? return self._version.upstream_version class DebVersionVariable(BranchSubstitutionVariable): basename = "debversion" minimum_format = 0.4 def __init__(self, branch_name, version): super(DebVersionVariable, self).__init__(branch_name) self._version = version @classmethod def from_changelog(cls, branch_name, changelog): if len(changelog._blocks) > 0: return cls(branch_name, changelog._blocks[0].version) else: return cls(branch_name, None) def get(self): if self._version is None: raise SubstitutionUnavailable(self.name, "No previous changelog to take the version from") return str(self._version) dfsg_regex = lazy_regex.lazy_compile( r'[+.]*dfsg[.]*[0-9]+') version_regex = lazy_regex.lazy_compile( r'([~+])(svn[0-9]+|bzr[0-9]+|git[0-9a-f]+)') def version_extract_base(version): version = dfsg_regex.sub("", version) return version_regex.sub("\\1", version) class DebUpstreamBaseVariable(DebUpstreamVariable): basename = "debupstream-base" minimum_format = 0.4 def get(self): version = super(DebUpstreamBaseVariable, self).get() version = version_extract_base(version) if version[-1] not in ("~", "+"): version += "+" return version ok_to_preserve = [DebUpstreamVariable, DebUpstreamBaseVariable, DebVersionVariable] deb_branch_vars = [DebVersionVariable, DebUpstreamBaseVariable, DebUpstreamVariable] def check_expanded_deb_version(base_branch): checked_version = base_branch.deb_version if checked_version is None: return for token in ok_to_preserve: if issubclass(token, BranchSubstitutionVariable): for name in base_branch.list_branch_names(): checked_version = checked_version.replace( token.determine_name(name), "") checked_version = checked_version.replace( token.determine_name(None), "") else: checked_version = checked_version.replace( token.name, "") if "{" in checked_version: available_tokens = [var.name for var in simple_vars if var.available_in(base_branch.format)] for var_kls in branch_vars + deb_branch_vars: if not var_kls.available_in(base_branch.format): continue for name in base_branch.list_branch_names(): available_tokens.append(var_kls.determine_name(name)) available_tokens.append(var_kls.determine_name(None)) raise errors.BzrCommandError("deb-version not fully " "expanded: %s. Valid substitutions in recipe format %s are: %s" % (base_branch.deb_version, base_branch.format, available_tokens)) def substitute_branch_vars(base_branch, branch_name, branch, revid): """Substitute the branch variables for the given branch name in deb_version. Where deb_version has a place to substitute the revno for a branch this will substitute it for the given branch name. :param branch_name: the name of the RecipeBranch to substitute. :param branch: Branch object for the branch :param revid: Revision id in the branch for which to return the revno """ if base_branch.deb_version is None: return revno_var = RevnoVariable(branch_name, branch, revid) base_branch.deb_version = revno_var.replace(base_branch.deb_version) if base_branch.format < 0.4: # The other variables were introduced in recipe format 0.4 return svn_revno_var = SubversionRevnumVariable(branch_name, branch, revid) base_branch.deb_version = svn_revno_var.replace(base_branch.deb_version) git_commit_var = GitCommitVariable(branch_name, branch, revid) base_branch.deb_version = git_commit_var.replace(base_branch.deb_version) latest_tag_var = LatestTagVariable(branch_name, branch, revid) base_branch.deb_version = latest_tag_var.replace(base_branch.deb_version) revdate_var = RevdateVariable(branch_name, branch, revid) base_branch.deb_version = revdate_var.replace(base_branch.deb_version) revtime_var = RevtimeVariable(branch_name, branch, revid) base_branch.deb_version = revtime_var.replace(base_branch.deb_version) tree = branch.repository.revision_tree(revid) cl_file_id = tree.path2id("debian/changelog") if cl_file_id is not None: tree.lock_read() try: cl = changelog.Changelog(tree.get_file_text(cl_file_id)) substitute_changelog_vars(base_branch, branch_name, cl) finally: tree.unlock() def substitute_changelog_vars(base_branch, branch_name, changelog): """Substitute variables related from a changelog. :param branch_name: Branch name (None for root branch) :param changelog: Changelog object to use """ from bzrlib.plugins.builder.deb_version import DebUpstreamVariable, DebUpstreamBaseVariable, DebVersionVariable debupstream_var = DebUpstreamVariable.from_changelog(branch_name, changelog) base_branch.deb_version = debupstream_var.replace(base_branch.deb_version) if base_branch.format < 0.4: # The other variables were introduced in recipe format 0.4 return debupstreambase_var = DebUpstreamBaseVariable.from_changelog( branch_name, changelog) base_branch.deb_version = debupstreambase_var.replace(base_branch.deb_version) pkgversion_var = DebVersionVariable.from_changelog(branch_name, changelog) base_branch.deb_version = pkgversion_var.replace(base_branch.deb_version) def substitute_time(base_branch, time): """Substitute the time in to deb_version if needed. :param time: a datetime.datetime with the desired time. """ if base_branch.deb_version is None: return base_branch.deb_version = TimeVariable(time).replace(base_branch.deb_version) base_branch.deb_version = DateVariable(time).replace(base_branch.deb_version) bzr-builder-0.7.3/info.py0000644000000000000000000000134511740346202013377 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2011 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . bzr_plugin_version = (0, 7, 3, 'final', 0) bzr-builder-0.7.3/ppa.py0000644000000000000000000000743411740346202013231 0ustar 00000000000000# ppa support for bzr builder. # # Copyright: Canonical Ltd. (C) 2009 # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . import time from bzrlib import ( errors, trace, ) from launchpadlib.launchpad import Launchpad def get_lp(): return Launchpad.login_with('bzr-builder', 'production') def watch(owner_name, archive_name, package_name, version): """Watch a package build. :return: True once the package built and published, or False if it fails or there is a timeout waiting. """ version = str(version) trace.note("Logging into Launchpad") launchpad = get_lp() owner = launchpad.people[owner_name] archive = owner.getPPAByName(name=archive_name) end_states = ['FAILEDTOBUILD', 'FULLYBUILT'] important_arches = ['amd64', 'i386', 'armel'] trace.note("Waiting for version %s of %s to build." % (version, package_name)) start = time.time() while True: sourceRecords = list(archive.getPublishedSources( source_name=package_name, version=version)) if not sourceRecords: if time.time() - 900 > start: # Over 15 minutes and no source yet, upload FAIL. raise errors.BzrCommandError("No source record in %s/%s for " "package %s=%s after 15 minutes." % (owner_name, archive_name, package_name, version)) return False trace.note("Source not available yet - waiting.") time.sleep(60) continue pkg = sourceRecords[0] if pkg.status.lower() not in ('published', 'pending'): trace.note("Package status: %s" % (pkg.status,)) time.sleep(60) continue # FIXME: LP should export this as an attribute. source_id = pkg.self_link.rsplit('/', 1)[1] buildSummaries = archive.getBuildSummariesForSourceIds( source_ids=[source_id])[source_id] if buildSummaries['status'] in end_states: break if buildSummaries['status'] == 'NEEDSBUILD': # We ignore non-virtual PPA architectures that are sparsely # supplied with buildds. missing = [] for build in buildSummaries['builds']: arch = build['arch_tag'] if arch in important_arches: missing.append(arch) if not missing: break extra = ' on ' + ', '.join(missing) else: extra = '' trace.note("%s is still in %s%s" % (pkg.display_name, buildSummaries['status'], extra)) time.sleep(60) trace.note("%s is now %s" % (pkg.display_name, buildSummaries['status'])) result = True if pkg.status.lower() != 'published': result = False # should this perhaps keep waiting? if buildSummaries['status'] != 'FULLYBUILT': if buildSummaries['status'] == 'NEEDSBUILD': # We're stopping early cause the important_arches are built. builds = pkg.getBuilds() for build in builds: if build.arch_tag in important_arches: if build.buildstate != 'Successfully built': result = False else: result = False return result bzr-builder-0.7.3/recipe.py0000644000000000000000000015042411740346202013716 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . import os import signal import subprocess import time from bzrlib import ( branch as _mod_branch, bzrdir, errors, merge, revision, revisionspec, tag, trace, transport, urlutils, version_info as bzr_version_info, ) try: from bzrlib.errors import NoWhoami except ImportError: NoWhoami = object() try: MergeIntoMerger = merge.MergeIntoMerger except (AttributeError, NameError): from bzrlib.plugins.builder.backports import MergeIntoMerger def subprocess_setup(): signal.signal(signal.SIGPIPE, signal.SIG_DFL) MERGE_INSTRUCTION = "merge" NEST_PART_INSTRUCTION = "nest-part" NEST_INSTRUCTION = "nest" RUN_INSTRUCTION = "run" USAGE = { MERGE_INSTRUCTION: 'merge NAME BRANCH [REVISION]', NEST_INSTRUCTION: 'nest NAME BRANCH TARGET-DIR [REVISION]', NEST_PART_INSTRUCTION: 'nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]]', RUN_INSTRUCTION: 'run COMMAND', } SAFE_INSTRUCTIONS = [ MERGE_INSTRUCTION, NEST_PART_INSTRUCTION, NEST_INSTRUCTION] class SubstitutionUnavailable(errors.BzrError): _fmt = """Substitution for %(name)s not available: %(reason)s""" def __init__(self, name, reason): errors.BzrError.__init__(self, name=name, reason=reason) class SubstitutionVariable(object): """A substitution variable for a version string.""" def replace(self, value): """Replace name with value.""" raise NotImplementedError(self.replace) @classmethod def available_in(cls, format): """Check if this variable is available in a particular format.""" raise NotImplementedError(cls.available_in) class SimpleSubstitutionVariable(SubstitutionVariable): name = None minimum_format = None def replace(self, value): if not self.name in value: return value return value.replace(self.name, self.get()) def get(self): raise NotImplementedError(self.value) @classmethod def available_in(self, format): return (format >= self.minimum_format) class BranchSubstitutionVariable(SimpleSubstitutionVariable): basename = None def __init__(self, branch_name=None): super(BranchSubstitutionVariable, self).__init__() self.branch_name = branch_name @classmethod def determine_name(cls, branch_name): if branch_name is None: return "{%s}" % cls.basename else: return "{%s:%s}" % (cls.basename, branch_name) @property def name(self): return self.determine_name(self.branch_name) class TimeVariable(SimpleSubstitutionVariable): name = "{time}" minimum_format = 0.1 def __init__(self, time): self._time = time def get(self): return self._time.strftime("%Y%m%d%H%M") class DateVariable(SimpleSubstitutionVariable): name = "{date}" minimum_format = 0.1 def __init__(self, time): self._time = time def get(self): return self._time.strftime("%Y%m%d") class RevisionVariable(BranchSubstitutionVariable): minimum_format = 0.1 def __init__(self, branch_name, branch, revid): super(RevisionVariable, self).__init__(branch_name) self.branch = branch self.revid = revid class RevnoVariable(RevisionVariable): basename = "revno" minimum_format = 0.1 def get_revno(self): try: revno = self.branch.revision_id_to_revno(self.revid) return str(revno) except errors.NoSuchRevision: # We need to load and use the full revno map after all result = self.branch.get_revision_id_to_revno_map().get( self.revid) if result is None: return result return ".".join(result) def get(self): revno = self.get_revno() if revno is None: raise errors.BzrCommandError("Can't substitute revno of " "branch %s in deb-version, as it's revno can't be " "determined" % revno) return revno class RevtimeVariable(RevisionVariable): basename = "revtime" minimum_format = 0.4 def get(self): rev = self.branch.repository.get_revision(self.revid) return time.strftime("%Y%m%d%H%M", time.gmtime(rev.timestamp)) class RevdateVariable(RevisionVariable): basename = "revdate" minimum_format = 0.4 def get(self): rev = self.branch.repository.get_revision(self.revid) return time.strftime("%Y%m%d", time.gmtime(rev.timestamp)) def extract_svn_revnum(rev): try: foreign_revid = rev.foreign_revid except AttributeError: try: (mapping_name, uuid, bp, srevnum) = rev.revision_id.split(":", 3) except ValueError: raise errors.InvalidRevisionId(rev.revision_id, None) if not mapping_name.startswith("svn-"): raise errors.InvalidRevisionId(rev.revision_id, None) return int(srevnum) else: if rev.mapping.vcs.abbreviation == "svn": return foreign_revid[2] else: raise errors.InvalidRevisionId(rev.revision_id, None) class SubversionRevnumVariable(RevisionVariable): basename = "svn-revno" minimum_format = 0.4 def get(self): rev = self.branch.repository.get_revision(self.revid) try: revno = extract_svn_revnum(rev) except errors.InvalidRevisionId: raise errors.BzrCommandError("unable to expand %s for %r in %r: " "not a Subversion revision" % ( self.name, self.revid, self.branch)) return str(revno) def extract_git_foreign_revid(rev): try: foreign_revid = rev.foreign_revid except AttributeError: try: (mapping_name, foreign_revid) = rev.revision_id.split(":", 1) except ValueError: raise errors.InvalidRevisionId(rev.revision_id, None) if not mapping_name.startswith("git-"): raise errors.InvalidRevisionId(rev.revision_id, None) return foreign_revid else: if rev.mapping.vcs.abbreviation == "git": return foreign_revid else: raise errors.InvalidRevisionId(rev.revision_id, None) class GitCommitVariable(RevisionVariable): basename = "git-commit" minimum_format = 0.4 def get(self): rev = self.branch.repository.get_revision(self.revid) try: commit_sha = extract_git_foreign_revid(rev) except errors.InvalidRevisionId: raise errors.BzrCommandError("unable to expand %s for %r in %r: " "not a Git revision" % ( self.name, self.revid, self.branch)) return commit_sha[:7] class LatestTagVariable(RevisionVariable): basename = "latest-tag" minimum_format = 0.4 def get(self): reverse_tag_dict = self.branch.tags.get_reverse_tag_dict() self.branch.lock_read() try: graph = self.branch.repository.get_graph() for revid in graph.iter_lefthand_ancestry(self.revid): if revid in reverse_tag_dict: return reverse_tag_dict[revid][0] else: raise errors.BzrCommandError("No tags set on branch %s mainline" % self.branch_name) finally: self.branch.unlock() branch_vars = [RevnoVariable, SubversionRevnumVariable, GitCommitVariable, LatestTagVariable, RevdateVariable, RevtimeVariable] simple_vars = [TimeVariable, DateVariable] class CommandFailedError(errors.BzrError): _fmt = "The command \"%(command)s\" failed." def __init__(self, command): super(CommandFailedError, self).__init__() self.command = command def ensure_basedir(to_transport): """Ensure that the basedir of to_transport exists. It is allowed to already exist currently, to reuse directories. :param to_transport: The Transport to ensure that the basedir of exists. """ try: to_transport.mkdir('.') except errors.FileExists: pass except errors.NoSuchFile: raise errors.BzrCommandError('Parent of "%s" does not exist.' % to_transport.base) def pull_or_branch(tree_to, br_to, br_from, to_transport, revision_id, accelerator_tree=None, possible_transports=None): """Either pull or branch from a branch. Depending on whether the target branch and tree exist already this will either pull from the source branch, or branch from it. If it returns this function will return a branch and tree for the target, after creating either if necessary. :param tree_to: The WorkingTree to pull in to, or None. If not None then br_to must not be None. :param br_to: The Branch to pull in to, or None to branch. :param br_from: The Branch to pull/branch from. :param to_transport: A Transport for the root of the target. :param revision_id: the revision id to pull/branch. :param accelerator_tree: A tree to take contents from that is faster than extracting from br_from, or None. :param possible_transports: A list of transports that can be reused, or None. :return: A tuple of (target tree, target branch) which are the updated tree and branch, created if necessary. They are locked, and you should use these instead of tree_to and br_to if they were passed in, including for unlocking. """ created_tree_to = False created_br_to = False if br_to is None: # We do a "branch" ensure_basedir(to_transport) dir = br_from.bzrdir.sprout(to_transport.base, revision_id, possible_transports=possible_transports, accelerator_tree=accelerator_tree, source_branch=br_from, stacked=(bzr_version_info >= (2, 3, 0))) try: tree_to = dir.open_workingtree() except errors.NoWorkingTree: # There's no working tree, so it's probably in a no-trees repo, # but the whole point of this is to create trees, so we should # forcibly create one. tree_to = dir.create_workingtree() br_to = tree_to.branch created_br_to = True tag._merge_tags_if_possible(br_from, br_to) created_tree_to = True else: # We do a "pull" if tree_to is not None: # FIXME: should these pulls overwrite? tree_to.pull(br_from, stop_revision=revision_id, possible_transports=possible_transports) else: br_to.pull(br_from, stop_revision=revision_id, possible_transports=possible_transports) tree_to = br_to.bzrdir.create_workingtree() # Ugh, we have to assume that the caller replaces their reference # to the branch with the one we return. br_to.unlock() br_to = tree_to.branch br_to.lock_write() created_tree_to = True if created_tree_to: tree_to.lock_write() try: if created_br_to: br_to.lock_write() try: conflicts = tree_to.conflicts() if len(conflicts) > 0: # FIXME: better reporting raise errors.BzrCommandError("Conflicts... aborting.") except: if created_br_to: br_to.unlock() raise except: if created_tree_to: tree_to.unlock() raise return tree_to, br_to def merge_branch(child_branch, tree_to, br_to, possible_transports=None): """Merge the branch specified by child_branch. :param child_branch: the RecipeBranch to retrieve the branch and revision to merge from. :param tree_to: the WorkingTree to merge in to. :param br_to: the Branch to merge in to. """ if child_branch.branch is None: child_branch.branch = _mod_branch.Branch.open(child_branch.url, possible_transports=possible_transports) child_branch.branch.lock_read() try: tag._merge_tags_if_possible(child_branch.branch, br_to) if child_branch.revspec is not None: merge_revspec = revisionspec.RevisionSpec.from_string( child_branch.revspec) try: merge_revid = merge_revspec.as_revision_id( child_branch.branch) except errors.InvalidRevisionSpec, e: # Give the user a hint if they didn't mean to speciy # a revspec. e.extra = (". Did you not mean to specify a revspec " "at the end of the merge line?") raise e else: merge_revid = child_branch.branch.last_revision() child_branch.revid = merge_revid try: merger = merge.Merger.from_revision_ids(None, tree_to, merge_revid, other_branch=child_branch.branch, tree_branch=br_to) except errors.UnrelatedBranches: # Let's just try and hope for the best. merger = merge.Merger.from_revision_ids(None, tree_to, merge_revid, other_branch=child_branch.branch, tree_branch=br_to, base=revision.NULL_REVISION) merger.merge_type = merge.Merge3Merger if (merger.base_rev_id == merger.other_rev_id and merger.other_rev_id is not None): # Nothing to do. return conflict_count = merger.do_merge() merger.set_pending() if conflict_count: # FIXME: better reporting raise errors.BzrCommandError("Conflicts from merge") config = br_to.get_config() try: committer = config.username() except NoWhoami: committer = "bzr-builder " tree_to.commit("Merge %s" % urlutils.unescape_for_display(child_branch.url, 'utf-8'), committer=committer) finally: child_branch.branch.unlock() def nest_part_branch(child_branch, tree_to, br_to, subpath, target_subdir=None): """Merge the branch subdirectory specified by child_branch. :param child_branch: the RecipeBranch to retrieve the branch and revision to merge from. :param tree_to: the WorkingTree to merge in to. :param br_to: the Branch to merge in to. :param subpath: only merge files from branch that are from this path. e.g. subpath='/debian' will only merge changes from that directory. :param target_subdir: (optional) directory in target to merge that subpath into. Defaults to basename of subpath. """ child_branch.branch = _mod_branch.Branch.open(child_branch.url) child_branch.branch.lock_read() try: child_branch.resolve_revision_id() other_tree = child_branch.branch.basis_tree() other_tree.lock_read() try: if target_subdir is None: target_subdir = os.path.basename(subpath) # Create any missing parent directories target_subdir_parent = os.path.dirname(target_subdir) missing = [] while tree_to.path2id(target_subdir_parent) is None: missing.append(target_subdir_parent) target_subdir_parent = os.path.dirname(target_subdir_parent) for path in reversed(missing): tree_to.mkdir(path) merger = MergeIntoMerger(this_tree=tree_to, other_tree=other_tree, other_branch=child_branch.branch, target_subdir=target_subdir, source_subpath=subpath, other_rev_id=child_branch.revid) merger.set_base_revision(revision.NULL_REVISION, child_branch.branch) conflict_count = merger.do_merge() merger.set_pending() finally: other_tree.unlock() finally: child_branch.branch.unlock() if conflict_count: # FIXME: better reporting raise errors.BzrCommandError("Conflicts from merge") tree_to.commit("Merge %s of %s" % (subpath, urlutils.unescape_for_display(child_branch.url, 'utf-8'))) def update_branch(base_branch, tree_to, br_to, to_transport, possible_transports=None): if base_branch.branch is None: base_branch.branch = _mod_branch.Branch.open(base_branch.url, possible_transports=possible_transports) base_branch.branch.lock_read() try: base_branch.resolve_revision_id() tree_to, br_to = pull_or_branch(tree_to, br_to, base_branch.branch, to_transport, base_branch.revid, possible_transports=possible_transports) finally: base_branch.branch.unlock() return tree_to, br_to def _resolve_revisions_recurse(new_branch, substitute_branch_vars, if_changed_from=None): changed = False new_branch.branch = _mod_branch.Branch.open(new_branch.url) new_branch.branch.lock_read() try: new_branch.resolve_revision_id() if substitute_branch_vars is not None: substitute_branch_vars(new_branch.name, new_branch.branch, new_branch.revid) if (if_changed_from is not None and (new_branch.revspec is not None or if_changed_from.revspec is not None)): if if_changed_from.revspec is not None: changed_revspec = revisionspec.RevisionSpec.from_string( if_changed_from.revspec) changed_revision_id = changed_revspec.as_revision_id( new_branch.branch) else: changed_revision_id = new_branch.branch.last_revision() if new_branch.revid != changed_revision_id: changed = True for index, instruction in enumerate(new_branch.child_branches): child_branch = instruction.recipe_branch if_changed_child = None if if_changed_from is not None: if_changed_child = if_changed_from.child_branches[index].recipe_branch if child_branch is not None: child_changed = _resolve_revisions_recurse(child_branch, substitute_branch_vars, if_changed_from=if_changed_child) if child_changed: changed = child_changed return changed finally: new_branch.branch.unlock() def resolve_revisions(base_branch, if_changed_from=None, substitute_branch_vars=None): """Resolve all the unknowns in base_branch. This walks the RecipeBranch and calls substitute_branch_vars for each child branch. If if_changed_from is not None then it should be a second RecipeBranch to compare base_branch against. If the shape, or the revision ids differ then the function will return True. :param base_branch: the RecipeBranch we plan to build. :param if_changed_from: the RecipeBranch that we want to compare against. :param substitute_branch_vars: Callable called for each branch with (name, bzr branch and last revision) :return: False if if_changed_from is not None, and the shape and revisions of the two branches don't differ. True otherwise. """ changed = False if if_changed_from is not None: changed = base_branch.different_shape_to(if_changed_from) if_changed_from_revisions = if_changed_from if changed: if_changed_from_revisions = None if substitute_branch_vars is not None: real_subsitute_branch_vars = lambda n, b, r: substitute_branch_vars(base_branch, n, b, r) else: real_subsitute_branch_vars = None changed_revisions = _resolve_revisions_recurse(base_branch, real_subsitute_branch_vars, if_changed_from=if_changed_from_revisions) if not changed: changed = changed_revisions if if_changed_from is not None and not changed: return False return True def _build_inner_tree(base_branch, target_path, possible_transports=None): revision_of = "" if base_branch.revspec is not None: revision_of = "revision '%s' of " % base_branch.revspec trace.note("Retrieving %s'%s' to put at '%s'." % (revision_of, base_branch.url, target_path)) to_transport = transport.get_transport(target_path, possible_transports=possible_transports) try: tree_to, br_to = bzrdir.BzrDir.open_tree_or_branch(target_path) # Should we commit any changes in the tree here? If we don't # then they will get folded up in to the first merge. except errors.NotBranchError: tree_to = None br_to = None if tree_to is not None: tree_to.lock_write() try: if br_to is not None: br_to.lock_write() try: tree_to, br_to = update_branch(base_branch, tree_to, br_to, to_transport, possible_transports=possible_transports) for instruction in base_branch.child_branches: instruction.apply(target_path, tree_to, br_to) finally: # Is this ok if tree_to is created by pull_or_branch? if br_to is not None: br_to.unlock() finally: if tree_to is not None: tree_to.unlock() def build_tree(base_branch, target_path, possible_transports=None): """Build the RecipeBranch at a path. Follow the instructions embodied in RecipeBranch and build a tree based on them rooted at target_path. If target_path exists and is the root of the branch then the branch will be updated based on what the RecipeBranch requires. :param base_branch: a RecipeBranch to build. :param target_path: the path to the base of the desired output. """ trace.note("Building tree.") _build_inner_tree(base_branch, target_path, possible_transports=possible_transports) class ChildBranch(object): """A child branch in a recipe. If the nest path is not None it is the path relative to the recipe branch where the child branch should be placed. If it is None then the child branch should be merged instead of nested. """ can_have_children = False def __init__(self, recipe_branch, nest_path=None): self.recipe_branch = recipe_branch self.nest_path = nest_path def apply(self, target_path, tree_to, br_to, possible_transports=None): raise NotImplementedError(self.apply) def as_tuple(self): return (self.recipe_branch, self.nest_path) def _get_revid_part(self): if self.recipe_branch.revid is not None: revid_part = " revid:%s" % self.recipe_branch.revid elif self.recipe_branch.revspec is not None: revid_part = " %s" % self.recipe_branch.revspec else: revid_part = "" return revid_part def __repr__(self): return "<%s %r>" % (self.__class__.__name__, self.nest_path) class CommandInstruction(ChildBranch): def apply(self, target_path, tree_to, br_to, possible_transports=None): # it's a command trace.note("Running '%s' in '%s'." % (self.nest_path, target_path)) proc = subprocess.Popen(self.nest_path, cwd=target_path, preexec_fn=subprocess_setup, shell=True, stdin=subprocess.PIPE) proc.communicate() if proc.returncode != 0: raise CommandFailedError(self.nest_path) def as_text(self): return "%s %s" % (RUN_INSTRUCTION, self.nest_path) class MergeInstruction(ChildBranch): def apply(self, target_path, tree_to, br_to, possible_transports=None): revision_of = "" if self.recipe_branch.revspec is not None: revision_of = "revision '%s' of " % self.recipe_branch.revspec trace.note("Merging %s'%s' in to '%s'." % (revision_of, self.recipe_branch.url, target_path)) merge_branch(self.recipe_branch, tree_to, br_to, possible_transports=possible_transports) def as_text(self): revid_part = self._get_revid_part() return "%s %s %s%s" % ( MERGE_INSTRUCTION, self.recipe_branch.name, self.recipe_branch.url, revid_part) def __repr__(self): return "<%s %r>" % (self.__class__.__name__, self.recipe_branch.name) class NestPartInstruction(ChildBranch): def __init__(self, recipe_branch, subpath, target_subdir): ChildBranch.__init__(self, recipe_branch) self.subpath = subpath self.target_subdir = target_subdir def apply(self, target_path, tree_to, br_to): nest_part_branch(self.recipe_branch, tree_to, br_to, self.subpath, self.target_subdir) def as_text(self): revid_part = self._get_revid_part() if revid_part: target_subdir = self.target_subdir if target_subdir is None: target_subdir = self.subpath target_revid_part = " %s%s" % ( target_subdir, revid_part) elif self.target_subdir is not None: target_revid_part = " %s" % self.target_subdir else: target_revid_part = "" return "%s %s %s %s%s" % ( NEST_PART_INSTRUCTION, self.recipe_branch.name, self.recipe_branch.url, self.subpath, target_revid_part) class NestInstruction(ChildBranch): can_have_children = True def apply(self, target_path, tree_to, br_to, possible_transports=None): _build_inner_tree(self.recipe_branch, target_path=os.path.join(target_path, self.nest_path), possible_transports=possible_transports) def as_text(self): revid_part = self._get_revid_part() return "%s %s %s %s%s" % ( NEST_INSTRUCTION, self.recipe_branch.name, self.recipe_branch.url, self.nest_path, revid_part) def __repr__(self): return "<%s %r>" % (self.__class__.__name__, self.recipe_branch.name) class RecipeBranch(object): """A nested structure that represents a Recipe. A RecipeBranch has a name and a url (the name can be None for the root branch), and optionally child branches that are either merged or nested. The child_branches attribute is a list of tuples of ChildBranch objects. The revid attribute records the revid that the url and revspec resolved to when the RecipeBranch was built, or None if it has not been built. :ivar revid: after this recipe branch has been built this is set to the revision ID that was merged/nested from the branch at self.url. """ def __init__(self, name, url, revspec=None): """Create a RecipeBranch. :param name: the name for the branch, or None if it is the root. :param url: the URL from which to retrieve the branch. :param revspec: a revision specifier for the revision of the branch to use, or None (the default) to use the last revision. """ self.name = name self.url = url self.revspec = revspec self.child_branches = [] self.revid = None self.branch = None def resolve_revision_id(self): """Resolve the revision id for this branch. """ if self.revspec is not None: revspec = revisionspec.RevisionSpec.from_string(self.revspec) revision_id = revspec.as_revision_id(self.branch) else: revision_id = self.branch.last_revision() self.revid = revision_id def merge_branch(self, branch): """Merge a child branch in to this one. :param branch: the RecipeBranch to merge. """ self.child_branches.append(MergeInstruction(branch)) def nest_part_branch(self, branch, subpath=None, target_subdir=None): """Merge subdir of a child branch into this one. :param branch: the RecipeBranch to merge. :param subpath: only merge files from branch that are from this path. e.g. subpath='/debian' will only merge changes from that directory. :param target_subdir: (optional) directory in target to merge that subpath into. Defaults to basename of subpath. """ self.child_branches.append( NestPartInstruction(branch, subpath, target_subdir)) def nest_branch(self, location, branch): """Nest a child branch in to this one. :param location: the relative path at which this branch should be nested. :param branch: the RecipeBranch to nest. """ assert location not in [b.nest_path for b in self.child_branches],\ "%s already has branch nested there" % location self.child_branches.append(NestInstruction(branch, location)) def run_command(self, command): """Set a command to be run. :param command: the command to be run """ self.child_branches.append(CommandInstruction(None, command)) def different_shape_to(self, other_branch): """Tests whether the name, url and child_branches are the same""" if self.name != other_branch.name: return True if self.url != other_branch.url: return True if len(self.child_branches) != len(other_branch.child_branches): return True for index, instruction in enumerate(self.child_branches): child_branch = instruction.recipe_branch nest_location = instruction.nest_path other_instruction = other_branch.child_branches[index] other_child_branch = other_instruction.recipe_branch other_nest_location = other_instruction.nest_path if nest_location != other_nest_location: return True if ((child_branch is None and other_child_branch is not None) or (child_branch is not None and other_child_branch is None)): return True # if child_branch is None then other_child_branch must be # None too, meaning that they are both run instructions, # we would compare their nest locations (commands), but # that has already been done, so just guard if (child_branch is not None and child_branch.different_shape_to(other_child_branch)): return True return False def iter_all_instructions(self): """Iter over all instructions under this branch.""" for instruction in self.child_branches: yield instruction child_branch = instruction.recipe_branch if child_branch is None: continue for instruction in child_branch.iter_all_instructions(): yield instruction def iter_all_branches(self): """Iterate over all branches.""" yield self for instruction in self.child_branches: child_branch = instruction.recipe_branch if child_branch is None: continue for subbranch in child_branch.iter_all_branches(): yield subbranch def lookup_branch(self, name): """Lookup a branch by its name.""" for branch in self.iter_all_branches(): if branch.name == name: return branch else: raise KeyError(name) def list_branch_names(self): """List all of the branch names under this one. :return: a list of the branch names. :rtype: list(str) """ return [branch.name for branch in self.iter_all_branches() if branch.name is not None] def __repr__(self): return "<%s %r>" % (self.__class__.__name__, self.name) class BaseRecipeBranch(RecipeBranch): """The RecipeBranch that is at the root of a recipe.""" def __init__(self, url, deb_version, format, revspec=None): """Create a BaseRecipeBranch. :param deb_version: the template to use for the version number. Should be None for anything except the root branch. """ super(BaseRecipeBranch, self).__init__(None, url, revspec=revspec) self.deb_version = deb_version self.format = format def _add_child_branches_to_manifest(self, child_branches, indent_level): manifest = "" for instruction in child_branches: manifest += "%s%s\n" % ( " " * indent_level, instruction.as_text()) if instruction.can_have_children: manifest += self._add_child_branches_to_manifest( instruction.recipe_branch.child_branches, indent_level+1) return manifest def __str__(self): return self.get_recipe_text(validate=True) def get_recipe_text(self, validate=False): manifest = "# bzr-builder format %s" % str(self.format) if self.deb_version is not None: # TODO: should we store the expanded version that was used? manifest += " deb-version %s" % (self.deb_version,) manifest += "\n" if self.revid is not None: manifest += "%s revid:%s\n" % (self.url, self.revid) elif self.revspec is not None: manifest += "%s %s\n" % (self.url, self.revspec) else: manifest += "%s\n" % (self.url,) manifest += self._add_child_branches_to_manifest(self.child_branches, 0) if validate: # Sanity check. # TODO: write a function that compares the result of this parse with # the branch that we built it from. RecipeParser(manifest).parse() return manifest class RecipeParseError(errors.BzrError): _fmt = "Error parsing %(filename)s:%(line)s:%(char)s: %(problem)s." def __init__(self, filename, line, char, problem): errors.BzrError.__init__(self, filename=filename, line=line, char=char, problem=problem) class InstructionParseError(RecipeParseError): _fmt = RecipeParseError._fmt + "\nUsage: %(usage)s" def __init__(self, filename, line, char, problem, instruction): RecipeParseError.__init__(self, filename, line, char, problem) self.usage = USAGE[instruction] class ForbiddenInstructionError(RecipeParseError): def __init__(self, filename, line, char, problem, instruction_name=None): RecipeParseError.__init__(self, filename, line, char, problem) self.instruction_name = instruction_name class RecipeParser(object): """Parse a recipe. The parse() method is probably the only one that interests you. """ whitespace_chars = " \t" eol_char = "\n" digit_chars = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") NEWEST_VERSION = 0.4 def __init__(self, f, filename=None): """Create a RecipeParser. :param f: either the recipe as a string, or a file like object to take it from. :param filename: the filename of the recipe if known (for error reporting). """ if getattr(f, "read", None) is not None: self.text = f.read() else: self.text = f self.filename = filename if filename is None: self.filename = "recipe" def parse(self, permitted_instructions=None): """Parse the recipe. :param permitted_instructions: a list of instructions that you want to allow. Defaults to None allowing them all. :type permitted_instructions: list(str) or None :return: a RecipeBranch representing the recipe. """ self.lines = self.text.split("\n") self.index = 0 self.line_index = 0 self.current_line = self.lines[self.line_index] self.current_indent_level = 0 self.seen_nicks = set() self.seen_paths = {".": 1} (version, deb_version) = self.parse_header() self.version = version last_instruction = None active_branches = [] last_branch = None while self.line_index < len(self.lines): if self.is_blankline(): self.new_line() continue comment = self.parse_comment_line() if comment is not None: self.new_line() continue old_indent_level = self.parse_indent() if old_indent_level is not None: if (old_indent_level < self.current_indent_level and last_instruction != NEST_INSTRUCTION): self.throw_parse_error("Not allowed to indent unless " "after a '%s' line" % NEST_INSTRUCTION) if old_indent_level < self.current_indent_level: active_branches.append(last_branch) else: unindent = self.current_indent_level - old_indent_level active_branches = active_branches[:unindent] if last_instruction is None: url = self.take_to_whitespace("branch to start from") revspec = self.parse_optional_revspec() self.new_line() last_branch = BaseRecipeBranch(url, deb_version, self.version, revspec=revspec) active_branches = [last_branch] last_instruction = "" else: instruction = self.parse_instruction( permitted_instructions=permitted_instructions) if instruction == RUN_INSTRUCTION: self.parse_whitespace("the command", instruction=instruction) command = self.take_to_newline().strip() self.new_line() active_branches[-1].run_command(command) else: branch_id = self.parse_branch_id(instruction) url = self.parse_branch_url(instruction) if instruction == NEST_INSTRUCTION: location = self.parse_branch_location(instruction) if instruction == NEST_PART_INSTRUCTION: path = self.parse_subpath(instruction) target_subdir = self.parse_optional_path() if target_subdir == '': target_subdir = None revspec = None else: revspec = self.parse_optional_revspec() else: revspec = self.parse_optional_revspec() self.new_line(instruction) last_branch = RecipeBranch(branch_id, url, revspec=revspec) if instruction == NEST_INSTRUCTION: active_branches[-1].nest_branch(location, last_branch) elif instruction == MERGE_INSTRUCTION: active_branches[-1].merge_branch(last_branch) elif instruction == NEST_PART_INSTRUCTION: active_branches[-1].nest_part_branch( last_branch, path, target_subdir) last_instruction = instruction if len(active_branches) == 0: self.throw_parse_error("Empty recipe") return active_branches[0] def parse_header(self): self.parse_char("#") self.parse_word("bzr-builder", require_whitespace=False) self.parse_word("format") version, version_str = self.peek_float("format version") if version > self.NEWEST_VERSION: self.throw_parse_error("Unknown format: '%s'" % str(version)) self.take_chars(len(version_str)) deb_version = self.parse_optional_deb_version() self.new_line() return version, deb_version def parse_instruction(self, permitted_instructions=None): if self.version < 0.2: options = (MERGE_INSTRUCTION, NEST_INSTRUCTION) options_str = "'%s' or '%s'" % options elif self.version < 0.3: options = (MERGE_INSTRUCTION, NEST_INSTRUCTION, RUN_INSTRUCTION) options_str = "'%s', '%s' or '%s'" % options else: options = (MERGE_INSTRUCTION, NEST_INSTRUCTION, NEST_PART_INSTRUCTION, RUN_INSTRUCTION) options_str = "'%s', '%s', '%s' or '%s'" % options instruction = self.peek_to_whitespace() if instruction is None: self.throw_parse_error("End of line while looking for %s" % options_str) if instruction in options: if permitted_instructions is not None: if instruction not in permitted_instructions: self.throw_parse_error("The '%s' instruction is " "forbidden" % instruction, cls=ForbiddenInstructionError, instruction_name=instruction) self.take_chars(len(instruction)) return instruction self.throw_parse_error("Expecting %s, got '%s'" % (options_str, instruction)) def parse_branch_id(self, instruction): self.parse_whitespace("the branch id", instruction=instruction) branch_id = self.peek_to_whitespace() if branch_id is None: self.throw_parse_error("End of line while looking for the " "branch id", cls=InstructionParseError, instruction=instruction) if branch_id in self.seen_nicks: self.throw_parse_error("'%s' was already used to identify " "a branch." % branch_id) self.take_chars(len(branch_id)) self.seen_nicks.add(branch_id) return branch_id def parse_branch_url(self, instruction): self.parse_whitespace("the branch url", instruction=instruction) branch_url = self.take_to_whitespace("the branch url", instruction) return branch_url def parse_branch_location(self, instruction): # FIXME: Needs a better term self.parse_whitespace("the location to nest") location = self.peek_to_whitespace() if location is None: self.throw_parse_error("End of line while looking for the " "location to nest", cls=InstructionParseError, instruction=instruction) norm_location = os.path.normpath(location) if norm_location in self.seen_paths: self.throw_parse_error("The path '%s' is a duplicate of " "the one used on line %d." % (location, self.seen_paths[norm_location]), InstructionParseError, instruction=instruction) if os.path.isabs(norm_location): self.throw_parse_error("Absolute paths are not allowed: %s" % location, InstructionParseError, instruction=instruction) if norm_location.startswith(".."): self.throw_parse_error("Paths outside the current directory " "are not allowed: %s" % location, cls=InstructionParseError, instruction=instruction) self.take_chars(len(location)) self.seen_paths[norm_location] = self.line_index + 1 return location def parse_subpath(self, instruction): self.parse_whitespace("the subpath to merge", instruction=instruction) location = self.take_to_whitespace("the subpath to merge", instruction) return location def parse_revspec(self): self.parse_whitespace("the revspec") revspec = self.take_to_whitespace("the revspec") return revspec def parse_optional_deb_version(self): self.parse_whitespace("'deb-version'", require=False) actual = self.peek_to_whitespace() if actual is None: # End of line, no version return None if actual != "deb-version": self.throw_expecting_error("deb-version", actual) self.take_chars(len("deb-version")) self.parse_whitespace("a value for 'deb-version'") return self.take_to_whitespace("a value for 'deb-version'") def parse_optional_revspec(self): self.parse_whitespace(None, require=False) revspec = self.peek_to_whitespace() if revspec is not None: self.take_chars(len(revspec)) return revspec def parse_optional_path(self): self.parse_whitespace(None, require=False) path = self.peek_to_whitespace() if path is not None: self.take_chars(len(path)) return path def throw_parse_error(self, problem, cls=None, **kwargs): if cls is None: cls = RecipeParseError raise cls(self.filename, self.line_index + 1, self.index + 1, problem, **kwargs) def throw_expecting_error(self, expected, actual): self.throw_parse_error("Expecting '%s', got '%s'" % (expected, actual)) def throw_eol(self, expected): self.throw_parse_error("End of line while looking for '%s'" % expected) def new_line(self, instruction=None): # Jump over any whitespace self.parse_whitespace(None, require=False) remaining = self.peek_to_whitespace() if remaining != None: kwargs = {} if instruction is not None: kwargs = { 'cls': InstructionParseError, 'instruction': instruction} self.throw_parse_error("Expecting the end of the line, got '%s'" % remaining, **kwargs) self.index = 0 self.line_index += 1 if self.line_index >= len(self.lines): self.current_line = None else: self.current_line = self.lines[self.line_index] def is_blankline(self): whitespace = self.peek_whitespace() if whitespace is None: return True return self.peek_char(skip=len(whitespace)) is None def take_char(self): if self.index >= len(self.current_line): return None self.index += 1 return self.current_line[self.index-1] def take_chars(self, num): ret = "" for i in range(num): char = self.take_char() if char is None: return None ret += char return ret def peek_char(self, skip=0): if self.index + skip >= len(self.current_line): return None return self.current_line[self.index + skip] def parse_char(self, char): actual = self.peek_char() if actual is None: self.throw_eol(char) if actual == char: self.take_char() return char self.throw_expecting_error(char, actual) def parse_indent(self): """Parse the indent from the start of the line.""" # FIXME: should just peek the whitespace new_indent = self.parse_whitespace(None, require=False) # FIXME: These checks should probably come after we check whether # any change in indent is legal at this point: # "Indents of 3 spaces aren't allowed" -> make it 2 spaces # -> "oh, you aren't allowed to indent at that point anyway" if "\t" in new_indent: self.throw_parse_error("Indents may not be done by tabs") if (len(new_indent) % 2 != 0): self.throw_parse_error("Indent not a multiple of two spaces") new_indent_level = len(new_indent) / 2 if new_indent_level != self.current_indent_level: old_indent_level = self.current_indent_level self.current_indent_level = new_indent_level if (new_indent_level > old_indent_level and new_indent_level - old_indent_level != 1): self.throw_parse_error("Indented by more than two spaces " "at once") return old_indent_level return None def parse_whitespace(self, looking_for, require=True, instruction=None): if require: actual = self.peek_char() if actual is None: kwargs = {} if instruction is not None: kwargs = { 'cls': InstructionParseError, 'instruction': instruction, } self.throw_parse_error("End of line while looking for " "%s" % looking_for, **kwargs) if actual not in self.whitespace_chars: self.throw_parse_error("Expecting whitespace before %s, " "got '%s'." % (looking_for, actual)) ret = "" actual = self.peek_char() while (actual is not None and actual in self.whitespace_chars): self.take_char() ret += actual actual = self.peek_char() return ret def peek_whitespace(self): ret = "" char = self.peek_char() if char is None: return char count = 0 while char is not None and char in self.whitespace_chars: ret += char count += 1 char = self.peek_char(skip=count) return ret def parse_word(self, expected, require_whitespace=True): self.parse_whitespace("'%s'" % expected, require=require_whitespace) length = len(expected) actual = self.peek_to_whitespace() if actual == expected: self.take_chars(length) return expected if actual is None: self.throw_eol(expected) self.throw_expecting_error(expected, actual) def peek_to_whitespace(self): ret = "" char = self.peek_char() if char is None: return char count = 0 while char is not None and char not in self.whitespace_chars: ret += char count += 1 char = self.peek_char(skip=count) return ret def take_to_whitespace(self, looking_for, instruction=None): text = self.peek_to_whitespace() if text is None: kwargs = {} if instruction is not None: kwargs = { 'cls': InstructionParseError, 'instruction': instruction} self.throw_parse_error("End of line while looking for %s" % looking_for, **kwargs) self.take_chars(len(text)) return text def peek_float(self, looking_for): self.parse_whitespace(looking_for) ret = self._parse_integer() conv_fn = int if ret == "": self.throw_parse_error("Expecting a float, got '%s'" % self.peek_to_whitespace()) if self.peek_char(skip=len(ret)) == ".": conv_fn = float ret2 = self._parse_integer(skip=len(ret)+1) if ret2 == "": self.throw_parse_error("Expecting a float, got '%s'" % self.peek_to_whitespace()) ret += "." + ret2 try: fl = conv_fn(ret) except ValueError: self.throw_parse_error("Expecting a float, got '%s'" % ret) return (fl, ret) def _parse_integer(self, skip=0): i = skip ret = "" while True: char = self.peek_char(skip=i) if char not in self.digit_chars: break ret += char i = i+1 return ret def parse_integer(self): ret = self._parse_integer() if ret == "": self.throw_parse_error("Expected an integer, found %s" % self.peek_to_whitespace()) self.take_chars(len(ret)) return ret def take_to_newline(self): text = self.current_line[self.index:] self.index += len(text) return text def parse_comment_line(self): whitespace = self.peek_whitespace() if whitespace is None: return "" if self.peek_char(skip=len(whitespace)) is None: return "" if self.peek_char(skip=len(whitespace)) != "#": return None self.parse_whitespace(None, require=False) comment = self.current_line[self.index:] self.index += len(comment) return comment bzr-builder-0.7.3/setup.py0000755000000000000000000000123711740346202013607 0ustar 00000000000000#!/usr/bin/python from info import * from distutils.core import setup if __name__ == '__main__': version = bzr_plugin_version[:3] version_string = ".".join([str(x) for x in version]) setup(name="bzr-builder", version=version_string, description="Turn a recipe in to a bzr branch", author="James Westby", author_email="james.westby@canonical.com", license="GNU GPL v3", url="http://launchpad.net/bzr-builder", packages=['bzrlib.plugins.builder', 'bzrlib.plugins.builder.tests', ], package_dir={'bzrlib.plugins.builder': '.'}, ) bzr-builder-0.7.3/tests/0000755000000000000000000000000011740346202013231 5ustar 00000000000000bzr-builder-0.7.3/tests/__init__.py0000644000000000000000000000302311740346202015340 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . import os from unittest import TestSuite from bzrlib.tests import ( TestUtil, ) try: from bzrlib.tests.features import ( Feature, ) except ImportError: # bzr < 2.5 from bzrlib.tests import ( Feature, ) class _PristineTarFeature(Feature): def feature_name(self): return '/usr/bin/pristine-tar' def _probe(self): return os.path.exists("/usr/bin/pristine-tar") PristineTarFeature = _PristineTarFeature() def test_suite(): loader = TestUtil.TestLoader() suite = TestSuite() testmod_names = [ 'blackbox', 'deb_util', 'deb_version', 'recipe', ] suite.addTest(loader.loadTestsFromModuleNames(["%s.test_%s" % (__name__, i) for i in testmod_names])) return suite bzr-builder-0.7.3/tests/test_blackbox.py0000644000000000000000000007563411740346202016446 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009-2010 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . from base64 import standard_b64encode import os import signal import subprocess from textwrap import dedent from bzrlib import ( export as _mod_export, osutils, workingtree, ) from bzrlib.branch import Branch from bzrlib.tests import ( TestCaseWithTransport, ) from bzrlib.plugins.builder.tests import ( Feature, PristineTarFeature, ) try: from debian import changelog except ImportError: from debian_bundle import changelog class _NotUnderFakeRootFeature(Feature): def _probe(self): return 'FAKEROOTKEY' not in os.environ def feature_name(self): return 'not running inside fakeroot' NotUnderFakeRootFeature = _NotUnderFakeRootFeature() def make_pristine_tar_delta(dest, tarball_path): """Create a pristine-tar delta for a tarball. :param dest: Directory to generate pristine tar delta for :param tarball_path: Path to the tarball :return: pristine-tarball """ def subprocess_setup(): signal.signal(signal.SIGPIPE, signal.SIG_DFL) # If tarball_path is relative, the cwd=dest parameter to Popen will make # pristine-tar faaaail. pristine-tar doesn't use the VFS either, so we # assume local paths. tarball_path = osutils.abspath(tarball_path) command = ["pristine-tar", "gendelta", tarball_path, "-"] proc = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=dest, preexec_fn=subprocess_setup, stderr=subprocess.PIPE) (stdout, stderr) = proc.communicate() if proc.returncode != 0: raise Exception("Generating delta from tar failed: %s" % stderr) return stdout class BlackboxBuilderTests(TestCaseWithTransport): if not getattr(TestCaseWithTransport, "assertPathDoesNotExist", None): # Compatibility with bzr < 2.4 def assertPathDoesNotExist(self, path): self.failIfExists(path) def assertPathExists(self, path): self.failUnlessExists(path) def setUp(self): super(BlackboxBuilderTests, self).setUp() # Replace DEBEMAIL and DEBFULLNAME so that they are known values # for the changelog checks. overrideEnv = getattr(self, "overrideEnv", None) if overrideEnv is None: # Pre-2.3 versions of Bazaar did not provide self.overrideEnv overrideEnv = self._captureVar overrideEnv("DEBEMAIL", "maint@maint.org") overrideEnv("DEBFULLNAME", "M. Maintainer") def _get_file_contents(self, filename, mode="r"): """Helper to read contents of a file Use check_file_content instead to just assert the contents match.""" self.assertPathExists(filename) f = open(filename, mode) try: return f.read() finally: f.close() def test_cmd_builder_exists(self): self.run_bzr("build --help") def test_cmd_builder_requires_recipe_file_argument(self): err = self.run_bzr("build", retcode=3)[1] self.assertEqual("bzr: ERROR: command 'build' requires argument " "LOCATION\n", err) def test_cmd_builder_requires_working_dir_argument(self): err = self.run_bzr("build recipe", retcode=3)[1] self.assertEqual("bzr: ERROR: command 'build' requires argument " "WORKING_DIRECTORY\n", err) def test_cmd_builder_nonexistant_recipe(self): err = self.run_bzr("build recipe working", retcode=3)[1] self.assertEqual("bzr: ERROR: Specified recipe does not exist: " "recipe\n", err) def test_cmd_builder_simple_recipe(self): self.build_tree_contents([("recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource\n")]) source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) revid = source.commit("one") self.run_bzr("build -q recipe working") self.assertPathExists("working/a") tree = workingtree.WorkingTree.open("working") self.assertEqual(revid, tree.last_revision()) self.assertPathExists("working/bzr-builder.manifest") self.check_file_contents("working/bzr-builder.manifest", "# bzr-builder format 0.1 deb-version 1\nsource revid:%s\n" % revid) def test_cmd_builder_simple_branch(self): source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) revid = source.commit("one") self.run_bzr("build -q source working") self.assertPathExists("working/a") tree = workingtree.WorkingTree.open("working") self.assertEqual(revid, tree.last_revision()) self.assertPathExists("working/bzr-builder.manifest") self.check_file_contents("working/bzr-builder.manifest", "# bzr-builder format 0.4\nsource revid:%s\n" % revid) def test_cmd_builder_simple_recipe_no_debversion(self): self.build_tree_contents([("recipe", "# bzr-builder format 0.1\n" "source\n")]) source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) revid = source.commit("one") self.run_bzr("build -q recipe working") self.assertPathExists("working/a") tree = workingtree.WorkingTree.open("working") self.assertEqual(revid, tree.last_revision()) self.assertPathExists("working/bzr-builder.manifest") self.check_file_contents("working/bzr-builder.manifest", "# bzr-builder format 0.1\nsource revid:%s\n" % revid) def test_cmd_builder_manifest(self): self.build_tree_contents([("recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource\n")]) source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) revid = source.commit("one") self.run_bzr("build -q recipe working --manifest manifest") self.assertPathExists("working/a") self.assertPathExists("manifest") self.check_file_contents("manifest", "# bzr-builder format 0.1 " "deb-version 1\nsource revid:%s\n" % revid) def test_cmd_builder_if_changed_does_not_exist(self): self.build_tree_contents([("recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource\n")]) source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) source.commit("one") out, err = self.run_bzr("build recipe working " "--if-changed-from manifest") def test_cmd_builder_if_changed_not_changed(self): source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) revid = source.commit("one") self.build_tree_contents([("recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) self.build_tree_contents([("old-manifest", "# bzr-builder format 0.1 " "deb-version 1\nsource revid:%s\n" % revid)]) out, err = self.run_bzr("build recipe working --manifest manifest " "--if-changed-from old-manifest") self.assertPathDoesNotExist("working") self.assertPathDoesNotExist("manifest") self.assertEqual("Unchanged\n", err) def test_cmd_builder_if_changed_changed(self): source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) revid = source.commit("one") self.build_tree_contents([("recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) self.build_tree_contents([("old-manifest", "# bzr-builder format 0.1 " "deb-version 1\nsource revid:foo\n")]) out, err = self.run_bzr("build -q recipe working --manifest manifest " "--if-changed-from old-manifest") self.assertPathExists("working/a") self.assertPathExists("manifest") self.check_file_contents("manifest", "# bzr-builder format 0.1 " "deb-version 1\nsource revid:%s\n" % revid) def test_cmd_dailydeb(self): self.requireFeature(NotUnderFakeRootFeature) #TODO: define a test feature for debuild and require it here. source = self.make_branch_and_tree("source") self.build_tree(["source/a", "source/debian/"]) self.build_tree_contents([("source/debian/rules", "#!/usr/bin/make -f\nclean:\n"), ("source/debian/control", "Source: foo\nMaintainer: maint maint@maint.org\n\n" "Package: foo\nArchitecture: all\n")]) source.add(["a", "debian/", "debian/rules", "debian/control"]) revid = source.commit("one") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) out, err = self.run_bzr("dailydeb -q test.recipe working " "--manifest manifest") self.assertPathDoesNotExist("working/a") package_root = "working/test-1/" self.assertPathExists(os.path.join(package_root, "a")) self.assertPathExists(os.path.join(package_root, "debian/bzr-builder.manifest")) self.assertPathExists("manifest") self.check_file_contents("manifest", "# bzr-builder format 0.1 " "deb-version 1\nsource revid:%s\n" % revid) self.check_file_contents(os.path.join(package_root, "debian/bzr-builder.manifest"), "# bzr-builder format 0.1 deb-version 1\nsource revid:%s\n" % revid) cl_contents = self._get_file_contents( os.path.join(package_root, "debian/changelog")) self.assertEqual("foo (1) lucid; urgency=low\n", cl_contents.splitlines(True)[0]) def test_cmd_dailydeb_no_work_dir(self): self.requireFeature(NotUnderFakeRootFeature) #TODO: define a test feature for debuild and require it here. if getattr(self, "permit_dir", None) is not None: self.permit_dir('/') # Allow the made working dir to be accessed. source = self.make_branch_and_tree("source") self.build_tree(["source/a", "source/debian/"]) self.build_tree_contents([("source/debian/rules", "#!/usr/bin/make -f\nclean:\n"), ("source/debian/control", "Source: foo\nMaintainer: maint maint@maint.org\n\n" "Package: foo\nArchitecture: all\n")]) source.add(["a", "debian/", "debian/rules", "debian/control"]) source.commit("one") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) out, err = self.run_bzr("dailydeb -q test.recipe " "--manifest manifest") def test_cmd_dailydeb_if_changed_from_non_existant(self): self.requireFeature(NotUnderFakeRootFeature) #TODO: define a test feature for debuild and require it here. if getattr(self, "permit_dir", None) is not None: self.permit_dir('/') # Allow the made working dir to be accessed. source = self.make_branch_and_tree("source") self.build_tree(["source/a", "source/debian/"]) self.build_tree_contents([("source/debian/rules", "#!/usr/bin/make -f\nclean:\n"), ("source/debian/control", "Source: foo\nMaintainer: maint maint@maint.org\n" "\nPackage: foo\nArchitecture: all\n")]) source.add(["a", "debian/", "debian/rules", "debian/control"]) source.commit("one") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) out, err = self.run_bzr("dailydeb -q test.recipe " "--manifest manifest --if-changed-from bar") def make_upstream_version(self, version, contents, pristine_tar_format=None): upstream = self.make_branch_and_tree("upstream") self.build_tree_contents(contents) upstream.smart_add([upstream.basedir]) revprops = {} if pristine_tar_format is not None: _mod_export.export(upstream, "export") if pristine_tar_format == "gz": tarfile_path = "export.tar.gz" _mod_export.export(upstream, tarfile_path, "tgz") revprops["deb-pristine-delta"] = standard_b64encode( make_pristine_tar_delta( "export", "export.tar.gz")) elif pristine_tar_format == "bz2": tarfile_path = "export.tar.bz2" _mod_export.export(upstream, tarfile_path, "tbz2") revprops["deb-pristine-delta-bz2"] = standard_b64encode( make_pristine_tar_delta( "export", "export.tar.bz2")) else: raise AssertionError("unknown pristine tar format %s" % pristine_tar_format) else: tarfile_path = "export.tar.gz" _mod_export.export(upstream, tarfile_path, "tgz") tarfile_sha1 = osutils.sha_file_by_name(tarfile_path) revid = upstream.commit("import upstream %s" % version, revprops=revprops) source = Branch.open("source") source.repository.fetch(upstream.branch.repository) source.tags.set_tag("upstream-%s" % version, revid) return tarfile_sha1 def make_simple_package(self, path): source = self.make_branch_and_tree(path) self.build_tree([os.path.join(path, "a"), os.path.join(path, "debian/")]) cl_contents = ("package (0.1-1) unstable; urgency=low\n * foo\n" " -- maint Tue, 04 Aug 2009 " "10:03:10 +0100\n") self.build_tree_contents([ (os.path.join(path, "debian/rules"), "#!/usr/bin/make -f\nclean:\n"), (os.path.join(path, "debian/control"), "Source: package\nMaintainer: maint maint@maint.org\n\n" "Package: package\nArchitecture: all\n"), (os.path.join(path, "debian/changelog"), cl_contents) ]) source.add(["a", "debian/", "debian/rules", "debian/control", "debian/changelog"]) source.commit("one") return source def test_cmd_dailydeb_no_build(self): self.make_simple_package("source") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) out, err = self.run_bzr("dailydeb -q test.recipe " "--manifest manifest --no-build working") new_cl_contents = ("package (1) unstable; urgency=low\n\n" " * Auto build.\n\n -- M. Maintainer ") actual_cl_contents = self._get_file_contents( "working/test-1/debian/changelog") self.assertStartsWith(actual_cl_contents, new_cl_contents) for fn in os.listdir("working"): self.assertFalse(fn.endswith(".changes")) def test_cmd_dailydeb_with_package_from_changelog(self): #TODO: define a test feature for debuild and require it here. self.requireFeature(NotUnderFakeRootFeature) self.make_simple_package("source") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) out, err = self.run_bzr("dailydeb -q test.recipe " "--manifest manifest --if-changed-from bar working") new_cl_contents = ("package (1) unstable; urgency=low\n\n" " * Auto build.\n\n -- M. Maintainer ") actual_cl_contents = self._get_file_contents( "working/test-1/debian/changelog") self.assertStartsWith(actual_cl_contents, new_cl_contents) def test_cmd_dailydeb_with_version_from_changelog(self): self.requireFeature(NotUnderFakeRootFeature) self.make_simple_package("source") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.4 " "deb-version {debversion}-2\nsource 1\n")]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working") new_cl_contents = ("package (0.1-2) unstable; urgency=low\n\n" " * Auto build.\n\n -- M. Maintainer ") cl = changelog.Changelog(self._get_file_contents( "working/test-{debversion}-2/debian/changelog")) self.assertEquals("0.1-1-2", str(cl._blocks[0].version)) def test_cmd_dailydeb_with_version_from_other_branch_changelog(self): self.requireFeature(NotUnderFakeRootFeature) source = self.make_simple_package("source") other = self.make_simple_package("other") cl_contents = ("package (0.4-1) unstable; urgency=low\n * foo\n" " -- maint Tue, 04 Aug 2009 " "10:03:10 +0100\n") self.build_tree_contents([ (os.path.join("other", "debian", "changelog"), cl_contents) ]) other.commit("new changelog entry") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.4 " "deb-version {debversion:other}.2\n" "source 1\n" "nest other other other\n")]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working") cl = changelog.Changelog(self._get_file_contents( "working/test-{debversion:other}.2/debian/changelog")) self.assertEquals("0.4-1.2", str(cl._blocks[0].version)) def test_cmd_dailydeb_with_upstream_version_from_changelog(self): self.requireFeature(NotUnderFakeRootFeature) self.make_simple_package("source") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version {debupstream}-2\nsource 1\n")]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working") new_cl_contents = ("package (0.1-2) unstable; urgency=low\n\n" " * Auto build.\n\n -- M. Maintainer ") actual_cl_contents = self._get_file_contents( "working/test-{debupstream}-2/debian/changelog") self.assertStartsWith(actual_cl_contents, new_cl_contents) def test_cmd_dailydeb_with_append_version(self): self.requireFeature(NotUnderFakeRootFeature) self.make_simple_package("source") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) out, err = self.run_bzr("dailydeb -q test.recipe working " "--append-version ~ppa1") new_cl_contents = ("package (1~ppa1) unstable; urgency=low\n\n" " * Auto build.\n\n -- M. Maintainer ") actual_cl_contents = self._get_file_contents( "working/test-1/debian/changelog") self.assertStartsWith(actual_cl_contents, new_cl_contents) def test_cmd_dailydeb_with_nonascii_maintainer_in_changelog(self): user_enc = osutils.get_user_encoding() try: os.environ["DEBFULLNAME"] = u"Micha\u25c8 Sawicz".encode(user_enc) except UnicodeEncodeError: self.skip("Need user encoding other than %r to test maintainer " "name from environment" % (user_enc,)) self.make_simple_package("source") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version 1\nsource 1\n")]) out, err = self.run_bzr("dailydeb -q test.recipe working") new_cl_contents = ("package (1) unstable; urgency=low\n\n" " * Auto build.\n\n" " -- Micha\xe2\x97\x88 Sawicz ") actual_cl_contents = self._get_file_contents( "working/test-1/debian/changelog") self.assertStartsWith(actual_cl_contents, new_cl_contents) def test_cmd_dailydeb_with_invalid_version(self): source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) self.build_tree_contents([ ("source/debian/", None), ("source/debian/control", "Source: foo\nMaintainer: maint maint@maint.org\n") ]) source.add(["a", "debian", "debian/control"]) revid = source.commit("one") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " "deb-version $\nsource 1\n"), ]) err = self.run_bzr("dailydeb -q test.recipe working", retcode=3)[1] self.assertContainsRe(err, "bzr: ERROR: Invalid deb-version: \\$: " "(Could not parse version: \\$|Invalid version string '\\$')\n") def test_cmd_dailydeb_with_safe(self): self.make_simple_package("source") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 1\nsource 1\nrun something bad")]) out, err = self.run_bzr("dailydeb -q test.recipe working --safe", retcode=3) self.assertContainsRe(err, "The 'run' instruction is forbidden.$") def make_simple_quilt_package(self): source = self.make_simple_package("source") self.build_tree(["source/debian/source/"]) self.build_tree_contents([ ("source/debian/source/format", "3.0 (quilt)\n")]) source.add(["debian/source", "debian/source/format"]) source.commit("set source format") return source def test_cmd_dailydeb_missing_orig_tarball(self): self.make_simple_quilt_package() self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 1-1\nsource 1\n")]) out, err = self.run_bzr( "dailydeb -q test.recipe working", retcode=3) self.assertEquals("", out) self.assertEquals( 'bzr: ERROR: Unable to find the upstream source. ' 'Import it as tag upstream-1 or build with ' '--allow-fallback-to-native.\n', err) def test_cmd_dailydeb_with_orig_tarball(self): self.requireFeature(NotUnderFakeRootFeature) self.make_simple_package("source") self.make_upstream_version("0.1", [("upstream/file", "content\n")]) self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 0.1-1\nsource\n")]) out, err = self.run_bzr( "dailydeb -q test.recipe working", retcode=0) self.assertPathExists("working/package_0.1.orig.tar.gz") self.assertPathExists("working/package_0.1-1.diff.gz") def test_cmd_dailydeb_with_pristine_orig_gz_tarball(self): self.requireFeature(NotUnderFakeRootFeature) self.requireFeature(PristineTarFeature) self.make_simple_package("source") pristine_tar_sha1 = self.make_upstream_version("0.1", [("upstream/file", "content\n")], pristine_tar_format="gz") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 0.1-1\nsource\n")]) out, err = self.run_bzr("dailydeb -q test.recipe working", retcode=0) self.assertPathExists("working/package_0.1.orig.tar.gz") self.assertPathExists("working/package_0.1-1.diff.gz") self.assertEquals( osutils.sha_file_by_name("working/package_0.1.orig.tar.gz"), pristine_tar_sha1) def test_cmd_dailydeb_with_pristine_orig_bz2_tarball(self): self.requireFeature(NotUnderFakeRootFeature) self.requireFeature(PristineTarFeature) self.make_simple_quilt_package() pristine_tar_sha1 = self.make_upstream_version("0.1", [ ("upstream/file", "content\n"), ("upstream/a", "contents of source/a\n")], pristine_tar_format="bz2") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 0.1-1\nsource\n")]) wt = workingtree.WorkingTree.open("source") self.build_tree([("source/upstream/")]) self.build_tree_contents([ ("source/upstream/file", "content\n"), ("source/upstream/a", "contents of source/a\n")]) wt.add(["upstream", "upstream/file", "upstream/a"]) out, err = self.run_bzr("dailydeb -q test.recipe working", retcode=0) self.assertPathExists("working/package_0.1.orig.tar.bz2") self.assertPathExists("working/package_0.1-1.debian.tar.gz") self.assertEquals( osutils.sha_file_by_name("working/package_0.1.orig.tar.bz2"), pristine_tar_sha1) def test_cmd_dailydeb_allow_fallback_to_native_with_orig_tarball(self): self.requireFeature(PristineTarFeature) self.make_simple_quilt_package() pristine_tar_sha1 = self.make_upstream_version("0.1", [ ("upstream/file", "content\n"), ("upstream/a", "contents of source/a\n")], pristine_tar_format="bz2") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 0.1-1\nsource\n")]) wt = workingtree.WorkingTree.open("source") self.build_tree([("source/upstream/")]) self.build_tree_contents([ ("source/upstream/file", "content\n"), ("source/upstream/a", "contents of source/a\n")]) wt.add(["upstream", "upstream/file", "upstream/a"]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working", retcode=0) self.assertPathExists("working/package_0.1.orig.tar.bz2") self.assertPathExists("working/package_0.1-1.debian.tar.gz") self.assertEquals("3.0 (quilt)\n", open("source/debian/source/format", "r").read()) self.assertEquals( osutils.sha_file_by_name("working/package_0.1.orig.tar.bz2"), pristine_tar_sha1) def test_cmd_dailydeb_force_native(self): self.requireFeature(NotUnderFakeRootFeature) self.make_simple_quilt_package() self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 1\nsource 2\n")]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working", retcode=0) self.assertFileEqual("3.0 (native)\n", "working/test-1/debian/source/format") def test_cmd_dailydeb_force_native_empty_series(self): self.requireFeature(NotUnderFakeRootFeature) source = self.make_simple_quilt_package() self.build_tree(['source/debian/patches/']) self.build_tree_contents([ ("test.recipe", "# bzr-builder format 0.3 " "deb-version 1\nsource 3\n"), ("source/debian/patches/series", "\n")]) source.add(["debian/patches", "debian/patches/series"]) source.commit("add patches") out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working", retcode=0) self.assertFileEqual("3.0 (native)\n", "working/test-1/debian/source/format") self.assertPathDoesNotExist("working/test-1/debian/patches") def test_cmd_dailydeb_force_native_apply_quilt(self): self.requireFeature(NotUnderFakeRootFeature) source = self.make_simple_quilt_package() self.build_tree(["source/debian/patches/"]) patch = dedent( """diff -ur a/thefile b/thefile --- a/thefile 2010-12-05 20:14:22.000000000 +0100 +++ b/thefile 2010-12-05 20:14:26.000000000 +0100 @@ -1 +1 @@ -old-contents +new-contents """) self.build_tree_contents([ ("source/thefile", "old-contents\n"), ("source/debian/patches/series", "01_foo.patch"), ("source/debian/patches/01_foo.patch", patch)]) source.add(["thefile", "debian/patches", "debian/patches/series", "debian/patches/01_foo.patch"]) source.commit("add patch") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 1\nsource\n")]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working", retcode=0) self.assertFileEqual("3.0 (native)\n", "working/test-1/debian/source/format") self.assertFileEqual("new-contents\n", "working/test-1/thefile") self.assertPathDoesNotExist("working/test-1/debian/patches") def test_cmd_dailydeb_force_native_apply_quilt_failure(self): source = self.make_simple_quilt_package() self.build_tree(["source/debian/patches/"]) patch = dedent( """diff -ur a/thefile b/thefile --- a/thefile 2010-12-05 20:14:22.000000000 +0100 +++ b/thefile 2010-12-05 20:14:26.000000000 +0100 @@ -1 +1 @@ -old-contents +new-contents """) self.build_tree_contents([ ("source/thefile", "contents\n"), ("source/debian/patches/series", "01_foo.patch"), ("source/debian/patches/01_foo.patch", patch)]) source.add(["thefile", "debian/patches", "debian/patches/series", "debian/patches/01_foo.patch"]) source.commit("add patch") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 1-1\nsource 3\n")]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working", retcode=3) self.assertContainsRe(err, "bzr: ERROR: Failed to apply quilt patches") def test_unknown_source_format(self): source = self.make_simple_package("source") self.build_tree(["source/debian/source/"]) self.build_tree_contents([ ("source/debian/source/format", "2.0\n")]) source.add(["debian/source", "debian/source/format"]) source.commit("set source format") self.build_tree_contents([("test.recipe", "# bzr-builder format 0.3 " "deb-version 1-1\nsource\n")]) out, err = self.run_bzr( "dailydeb --allow-fallback-to-native -q test.recipe working", retcode=3) self.assertEquals(err, "bzr: ERROR: Unknown source format 2.0\n") bzr-builder-0.7.3/tests/test_deb_util.py0000644000000000000000000000206411740346202016433 0ustar 00000000000000# bzr-builder: a bzr plugin to construct trees based on recipes # Copyright 2009 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . from bzrlib.plugins.builder.deb_util import target_from_dput from bzrlib.tests import ( TestCase, ) class TestTargetFromDPut(TestCase): def test_default_ppa(self): self.assertEqual(('team-name', 'ppa'), target_from_dput('ppa:team-name')) def test_named_ppa(self): self.assertEqual(('team', 'ppa2'), target_from_dput('ppa:team/ppa2')) bzr-builder-0.7.3/tests/test_deb_version.py0000644000000000000000000004506611740346202017154 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009-2011 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . import datetime import textwrap from bzrlib import ( errors, ) from bzrlib.tests import ( TestCase, TestCaseWithTransport, ) from bzrlib.plugins.builder.deb_version import ( DebUpstreamBaseVariable, DebUpstreamVariable, DebVersionVariable, SubstitutionUnavailable, check_expanded_deb_version, version_extract_base, substitute_branch_vars, substitute_time, ) from bzrlib.plugins.builder.recipe import ( BaseRecipeBranch, RecipeBranch, resolve_revisions, ) try: from debian import changelog except ImportError: # In older versions of python-debian the main package was named # debian_bundle from debian_bundle import changelog class ResolveRevisionsTests(TestCaseWithTransport): def test_unchanged(self): source =self.make_branch_and_tree("source") revid = source.commit("one") branch1 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="1") branch2 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="revid:%s" % revid) self.assertEqual(False, resolve_revisions(branch1, if_changed_from=branch2, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) self.assertEqual(revid, branch1.revid) self.assertEqual("1", branch1.revspec) self.assertEqual("1", branch1.deb_version) def test_unchanged_not_explicit(self): source =self.make_branch_and_tree("source") revid = source.commit("one") branch1 = BaseRecipeBranch("source", "{revno}", 0.2) branch2 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="revid:%s" % revid) self.assertEqual(False, resolve_revisions(branch1, if_changed_from=branch2, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) self.assertEqual(revid, branch1.revid) self.assertEqual(None, branch1.revspec) self.assertEqual("1", branch1.deb_version) def test_unchanged_multilevel(self): source =self.make_branch_and_tree("source") revid = source.commit("one") branch1 = BaseRecipeBranch("source", "{revno}", 0.2) branch2 = RecipeBranch("nested1", "source") branch3 = RecipeBranch("nested2", "source") branch2.nest_branch("bar", branch3) branch1.nest_branch("foo", branch2) branch4 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="revid:%s" % revid) branch5 = RecipeBranch("nested1", "source", revspec="revid:%s" % revid) branch6 = RecipeBranch("nested2", "source", revspec="revid:%s" % revid) branch5.nest_branch("bar", branch6) branch4.nest_branch("foo", branch5) self.assertEqual(False, resolve_revisions(branch1, if_changed_from=branch4, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) self.assertEqual(revid, branch1.revid) self.assertEqual(None, branch1.revspec) self.assertEqual("1", branch1.deb_version) def test_changed(self): source =self.make_branch_and_tree("source") revid = source.commit("one") branch1 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="1") branch2 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="revid:foo") self.assertEqual(True, resolve_revisions(branch1, if_changed_from=branch2, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) self.assertEqual(revid, branch1.revid) self.assertEqual("1", branch1.revspec) self.assertEqual("1", branch1.deb_version) def test_changed_shape(self): source =self.make_branch_and_tree("source") revid = source.commit("one") branch1 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="1") branch2 = BaseRecipeBranch("source", "{revno}", 0.2, revspec="revid:%s" % revid) branch3 = RecipeBranch("nested", "source") branch1.nest_branch("foo", branch3) self.assertEqual(True, resolve_revisions(branch1, if_changed_from=branch2, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) self.assertEqual(revid, branch1.revid) self.assertEqual("1", branch1.revspec) self.assertEqual("1", branch1.deb_version) def test_changed_command(self): source =self.make_branch_and_tree("source") source.commit("one") branch1 = BaseRecipeBranch("source", "{revno}", 0.2) branch2 = BaseRecipeBranch("source", "{revno}", 0.2) branch1.run_command("touch test1") branch2.run_command("touch test2") self.assertEqual(True, resolve_revisions(branch1, if_changed_from=branch2, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) def test_unchanged_command(self): source =self.make_branch_and_tree("source") source.commit("one") branch1 = BaseRecipeBranch("source", "{revno}", 0.2) branch2 = BaseRecipeBranch("source", "{revno}", 0.2) branch1.run_command("touch test1") branch2.run_command("touch test1") self.assertEqual(False, resolve_revisions(branch1, if_changed_from=branch2, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) def test_substitute(self): source =self.make_branch_and_tree("source") revid1 = source.commit("one") source.commit("two") branch1 = BaseRecipeBranch("source", "{revno}-{revno:packaging}", 0.2, revspec="1") branch2 = RecipeBranch("packaging", "source") branch1.nest_branch("debian", branch2) self.assertEqual(True, resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars)) self.assertEqual("source", branch1.url) self.assertEqual(revid1, branch1.revid) self.assertEqual("1", branch1.revspec) self.assertEqual("1-2", branch1.deb_version) def test_substitute_supports_debupstream(self): # resolve_revisions should leave debupstream parameters alone and not # complain. source =self.make_branch_and_tree("source") source.commit("one") source.commit("two") branch1 = BaseRecipeBranch("source", "{debupstream}-{revno}", 0.2) resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars) self.assertEqual("{debupstream}-2", branch1.deb_version) def test_subsitute_not_fully_expanded(self): source =self.make_branch_and_tree("source") source.commit("one") source.commit("two") branch1 = BaseRecipeBranch("source", "{revno:packaging}", 0.2) resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars) self.assertRaises(errors.BzrCommandError, check_expanded_deb_version, branch1) def test_substitute_svn_not_svn(self): br = self.make_branch("source") source = br.create_checkout("checkout") source.commit("one") source.commit("two") branch1 = BaseRecipeBranch("source", "foo-{svn-revno}", 0.4) e = self.assertRaises(errors.BzrCommandError, resolve_revisions, branch1, None, substitute_branch_vars) self.assertTrue(str(e).startswith("unable to expand {svn-revno} "), e) def test_substitute_svn(self): br = self.make_branch("source") source = br.create_checkout("checkout") source.commit("one") source.commit("two", rev_id="svn-v4:be7e6eca-30d4-0310-a8e5-ac0d63af7070:trunk:5344") branch1 = BaseRecipeBranch("source", "foo-{svn-revno}", 0.4) resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars) self.assertEqual("foo-5344", branch1.deb_version) def test_substitute_git_not_git(self): source = self.make_branch_and_tree("source") source.commit("one") source.commit("two") branch1 = BaseRecipeBranch("source", "foo-{git-commit}", 0.4) e = self.assertRaises(errors.BzrCommandError, resolve_revisions, branch1, None, substitute_branch_vars) self.assertTrue(str(e).startswith("unable to expand {git-commit} "), e) def test_substitute_git(self): source = self.make_branch_and_tree("source") source.commit("one", rev_id="git-v1:a029d7b2cc83c26a53d8b2a24fa12c340fcfac58") branch1 = BaseRecipeBranch("source", "foo-{git-commit}", 0.4) resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars) self.assertEqual("foo-a029d7b", branch1.deb_version) def test_latest_tag(self): source = self.make_branch_and_tree("source") revid = source.commit("one") source.branch.tags.set_tag("millbank", revid) source.commit("two") branch1 = BaseRecipeBranch("source", "foo-{latest-tag}", 0.4) resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars) self.assertEqual("foo-millbank", branch1.deb_version) def test_latest_tag_no_tag(self): source = self.make_branch_and_tree("source") revid = source.commit("one") source.commit("two") branch1 = BaseRecipeBranch("source", "foo-{latest-tag}", 0.4) e = self.assertRaises(errors.BzrCommandError, resolve_revisions, branch1, substitute_branch_vars=substitute_branch_vars) self.assertTrue(str(e).startswith("No tags set on branch None mainline"), e) def test_substitute_revdate(self): br = self.make_branch("source") source = br.create_checkout("checkout") source.commit("one") source.commit("two", timestamp=1307708628, timezone=0) branch1 = BaseRecipeBranch("source", "foo-{revdate}", 0.4) resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars) self.assertEqual("foo-20110610", branch1.deb_version) def test_substitute_revtime(self): br = self.make_branch("source") source = br.create_checkout("checkout") source.commit("one") source.commit("two", timestamp=1307708628, timezone=0) branch1 = BaseRecipeBranch("source", "foo-{revtime}", 0.4) resolve_revisions(branch1, substitute_branch_vars=substitute_branch_vars) self.assertEqual("foo-201106101223", branch1.deb_version) class DebUpstreamVariableTests(TestCase): def write_changelog(self, version): contents = textwrap.dedent(""" package (%s) experimental; urgency=low * Initial release. (Closes: #XXXXXX) -- Jelmer Vernooij Thu, 19 May 2011 10:07:41 +0100 """ % version)[1:] return changelog.Changelog(file=contents) def test_empty_changelog(self): var = DebUpstreamVariable.from_changelog(None, changelog.Changelog()) self.assertRaises(SubstitutionUnavailable, var.get) def test_version(self): var = DebUpstreamVariable.from_changelog(None, self.write_changelog("2.3")) self.assertEquals("2.3", var.get()) def test_epoch(self): # The epoch is (currently) ignored by {debupstream}. var = DebUpstreamVariable.from_changelog(None, self.write_changelog("2:2.3")) self.assertEquals("2.3", var.get()) def test_base_without_snapshot(self): var = DebUpstreamBaseVariable.from_changelog(None, self.write_changelog("2.4")) self.assertEquals("2.4+", var.get()) def test_base_with_svn_snapshot(self): var = DebUpstreamBaseVariable.from_changelog(None, self.write_changelog("2.4~svn4")) self.assertEquals("2.4~", var.get()) def test_base_with_bzr_snapshot(self): var = DebUpstreamBaseVariable.from_changelog(None, self.write_changelog("2.4+bzr343")) self.assertEquals("2.4+", var.get()) class VersionExtractBaseTests(TestCase): def test_simple_extract(self): self.assertEquals("2.4", version_extract_base("2.4")) self.assertEquals("2.4+foobar", version_extract_base("2.4+foobar")) def test_with_bzr(self): self.assertEquals("2.4+", version_extract_base("2.4+bzr32")) self.assertEquals("2.4~", version_extract_base("2.4~bzr32")) def test_with_git(self): self.assertEquals("2.4+", version_extract_base("2.4+git20101010")) self.assertEquals("2.4~", version_extract_base("2.4~gitaabbccdd")) def test_with_svn(self): self.assertEquals("2.4+", version_extract_base("2.4+svn45")) self.assertEquals("2.4~", version_extract_base("2.4~svn45")) def test_with_dfsg(self): self.assertEquals("2.4+", version_extract_base("2.4+bzr32+dfsg1")) self.assertEquals("2.4~", version_extract_base("2.4~bzr32+dfsg.1")) self.assertEquals("2.4~", version_extract_base("2.4~bzr32.dfsg.1")) self.assertEquals("2.4~", version_extract_base("2.4~bzr32dfsg.1")) self.assertEquals("1.6~", version_extract_base("1.6~git20120320.dfsg.1")) class DebVersionVariableTests(TestCase): def write_changelog(self, version): contents = textwrap.dedent(""" package (%s) experimental; urgency=low * Initial release. (Closes: #XXXXXX) -- Jelmer Vernooij Thu, 19 May 2011 10:07:41 +0100 """ % version)[1:] return changelog.Changelog(file=contents) def test_empty_changelog(self): var = DebVersionVariable.from_changelog(None, changelog.Changelog()) self.assertRaises(SubstitutionUnavailable, var.get) def test_simple(self): var = DebVersionVariable.from_changelog( None, self.write_changelog("2.3-1")) self.assertEquals("2.3-1", var.get()) def test_epoch(self): var = DebVersionVariable.from_changelog( None, self.write_changelog("4:2.3-1")) self.assertEquals("4:2.3-1", var.get()) class RecipeBranchTests(TestCaseWithTransport): def test_substitute_time(self): time = datetime.datetime.utcfromtimestamp(1) base_branch = BaseRecipeBranch("base_url", "1-{time}", 0.2) substitute_time(base_branch, time) self.assertEqual("1-197001010000", base_branch.deb_version) substitute_time(base_branch, time) self.assertEqual("1-197001010000", base_branch.deb_version) def test_substitute_date(self): time = datetime.datetime.utcfromtimestamp(1) base_branch = BaseRecipeBranch("base_url", "1-{date}", 0.2) substitute_time(base_branch, time) self.assertEqual("1-19700101", base_branch.deb_version) substitute_time(base_branch, time) self.assertEqual("1-19700101", base_branch.deb_version) def test_substitute_branch_vars(self): base_branch = BaseRecipeBranch("base_url", "1", 0.2) wt = self.make_branch_and_tree("br") revid = wt.commit("acommit") substitute_branch_vars(base_branch, None, wt.branch, revid) self.assertEqual("1", base_branch.deb_version) substitute_branch_vars(base_branch, None, wt.branch, revid) self.assertEqual("1", base_branch.deb_version) base_branch = BaseRecipeBranch("base_url", "{revno}", 0.2) substitute_branch_vars(base_branch, None, wt.branch, revid) self.assertEqual("1", base_branch.deb_version) base_branch = BaseRecipeBranch("base_url", "{revno}", 0.2) substitute_branch_vars(base_branch, "foo", wt.branch, revid) self.assertEqual("{revno}", base_branch.deb_version) substitute_branch_vars(base_branch, "foo", wt.branch, revid) self.assertEqual("{revno}", base_branch.deb_version) base_branch = BaseRecipeBranch("base_url", "{revno:foo}", 0.2) substitute_branch_vars(base_branch, "foo", wt.branch, revid) self.assertEqual("1", base_branch.deb_version) def test_substitute_branch_vars_debupstream(self): wt = self.make_branch_and_tree("br") revid1 = wt.commit("acommit") cl_contents = ("package (0.1-1) unstable; urgency=low\n * foo\n" " -- maint Tue, 04 Aug 2009 " "10:03:10 +0100\n") self.build_tree_contents( [("br/debian/", ), ('br/debian/changelog', cl_contents)]) wt.add(['debian', 'debian/changelog']) revid2 = wt.commit("with changelog") base_branch = BaseRecipeBranch("base_url", "{debupstream}", 0.4) # No changelog file, so no substitution substitute_branch_vars(base_branch, None, wt.branch, revid1) self.assertEqual("{debupstream}", base_branch.deb_version) substitute_branch_vars(base_branch, None, wt.branch, revid2) self.assertEqual("0.1", base_branch.deb_version) base_branch = BaseRecipeBranch("base_url", "{debupstream:tehname}", 0.4) substitute_branch_vars(base_branch, "tehname", wt.branch, revid2) self.assertEqual("0.1", base_branch.deb_version) def test_substitute_branch_vars_debupstream_pre_0_4(self): wt = self.make_branch_and_tree("br") cl_contents = ("package (0.1-1) unstable; urgency=low\n * foo\n" " -- maint Tue, 04 Aug 2009 " "10:03:10 +0100\n") self.build_tree_contents( [("br/debian/", ), ('br/debian/changelog', cl_contents)]) wt.add(['debian', 'debian/changelog']) revid = wt.commit("with changelog") # In recipe format < 0.4 {debupstream} gets replaced from the resulting # tree, not from the branch vars. base_branch = BaseRecipeBranch("base_url", "{debupstream}", 0.2) substitute_branch_vars(base_branch, None, wt.branch, revid) self.assertEqual("{debupstream}", base_branch.deb_version) bzr-builder-0.7.3/tests/test_recipe.py0000644000000000000000000015423311740346202016121 0ustar 00000000000000# bzr-builder: a bzr plugin to constuct trees based on recipes # Copyright 2009 Canonical Ltd. # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License version 3, as published # by the Free Software Foundation. # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranties of # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR # PURPOSE. See the GNU General Public License for more details. # You should have received a copy of the GNU General Public License along # with this program. If not, see . import os from bzrlib import ( errors, transport, workingtree, ) from bzrlib.tests import ( TestCaseInTempDir, TestCaseWithTransport, ) from bzrlib.plugins.builder.recipe import ( BaseRecipeBranch, build_tree, ensure_basedir, InstructionParseError, ForbiddenInstructionError, MERGE_INSTRUCTION, NEST_INSTRUCTION, NEST_PART_INSTRUCTION, pull_or_branch, RecipeParser, RecipeBranch, RecipeParseError, RUN_INSTRUCTION, SAFE_INSTRUCTIONS, USAGE, ) class RecipeParserTests(TestCaseInTempDir): deb_version = "0.1-{revno}" basic_header = ("# bzr-builder format 0.3 deb-version " + deb_version +"\n") basic_branch = "http://foo.org/" basic_header_and_branch = basic_header + basic_branch + "\n" def get_recipe(self, recipe_text, **kwargs): return RecipeParser(recipe_text).parse(**kwargs) def assertParseError(self, line, char, problem, callable, *args, **kwargs): exc = self.assertRaises(RecipeParseError, callable, *args, **kwargs) self.assertEqual(problem, exc.problem) self.assertEqual(line, exc.line) self.assertEqual(char, exc.char) self.assertEqual("recipe", exc.filename) return exc def assertInstructionParseError(self, line, char, problem, instruction, callable, *args, **kwargs): exc = self.assertParseError(line, char, problem, callable, *args, **kwargs) self.assertIsInstance(exc, InstructionParseError), self.assertEqual(USAGE[instruction], exc.usage) def check_recipe_branch(self, branch, name, url, revspec=None, num_child_branches=0, revid=None): self.assertEqual(name, branch.name) self.assertEqual(url, branch.url) self.assertEqual(revspec, branch.revspec) self.assertEqual(revid, branch.revid) self.assertEqual(num_child_branches, len(branch.child_branches)) def check_base_recipe_branch(self, branch, url, revspec=None, num_child_branches=0, revid=None, deb_version=deb_version): self.check_recipe_branch(branch, None, url, revspec=revspec, num_child_branches=num_child_branches, revid=revid) self.assertEqual(deb_version, branch.deb_version) def test_parses_most_basic(self): self.get_recipe(self.basic_header_and_branch) def test_parses_missing_deb_version(self): header = "# bzr-builder format 0.3\n" branch = "http://foo.org/\n" self.get_recipe(header + branch) def tests_rejects_non_comment_to_start(self): self.assertParseError(1, 1, "Expecting '#', got 'b'", self.get_recipe, "bzr-builder") def tests_rejects_wrong_format_definition(self): self.assertParseError(1, 3, "Expecting 'bzr-builder', " "got 'bzr-nothing'", self.get_recipe, "# bzr-nothing") def tests_rejects_no_format_definition(self): self.assertParseError(1, 3, "End of line while looking for " "'bzr-builder'", self.get_recipe, "# \n") def tests_rejects_no_format_definition_eof(self): self.assertParseError(1, 3, "End of line while looking for " "'bzr-builder'", self.get_recipe, "# ") def tests_rejects_wrong_format_version_marker(self): self.assertParseError(1, 15, "Expecting 'format', got 'aaaa'", self.get_recipe, "# bzr-builder aaaa") def test_rejects_invalid_format_version(self): self.assertParseError(1, 22, "Expecting a float, got 'foo'", self.get_recipe, "# bzr-builder format foo") def test_rejects_invalid_format_version2(self): self.assertParseError(1, 22, "Expecting a float, got '1.'", self.get_recipe, "# bzr-builder format 1.") def test_unknown_format_version(self): self.assertParseError(1, 22, "Unknown format: '10000'", self.get_recipe, "# bzr-builder format 10000 deb-version 1\n") def test_rejects_invalid_deb_version_marker(self): self.assertParseError(1, 26, "Expecting 'deb-version', " "got 'deb'", self.get_recipe, "# bzr-builder format 0.1 deb") def tests_rejects_no_deb_version_value(self): self.assertParseError(1, 37, "End of line while looking for " "a value for 'deb-version'", self.get_recipe, "# bzr-builder format 0.1 deb-version") def tests_rejects_extra_text_after_deb_version(self): self.assertParseError(1, 40, "Expecting the end of the line, " "got 'foo'", self.get_recipe, "# bzr-builder format 0.1 deb-version 1 foo") def tests_rejects_indented_base_branch(self): self.assertParseError(2, 3, "Not allowed to indent unless after " "a 'nest' line", self.get_recipe, self.basic_header + " http://foo.org/") def tests_rejects_text_after_base_branch(self): self.assertParseError(2, 19, "Expecting the end of the line, " "got 'foo'", self.get_recipe, self.basic_header + "http://foo.org/ 2 foo") def tests_rejects_unknown_instruction(self): self.assertParseError(3, 1, "Expecting 'merge', 'nest', 'nest-part' " "or 'run', got 'cat'", self.get_recipe, self.basic_header + "http://foo.org/\n" + "cat") def test_rejects_merge_no_name(self): self.assertInstructionParseError(3, 7, "End of line while looking for " "the branch id", MERGE_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "merge ") def test_rejects_merge_no_name_no_space(self): self.assertInstructionParseError(3, 6, "End of line while looking for " "the branch id", MERGE_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "merge") def test_rejects_merge_no_url(self): self.assertInstructionParseError(3, 11, "End of line while looking for" " the branch url", MERGE_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "merge foo ") def test_rejects_text_at_end_of_merge_line(self): self.assertInstructionParseError(3, 17, "Expecting the end of the line, got 'bar'", MERGE_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "merge foo url 2 bar") def test_rejects_nest_part_no_name(self): self.assertInstructionParseError(3, 11, "End of line while looking for" " the branch id", NEST_PART_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest-part ") def test_rejects_nest_part_no_url(self): self.assertInstructionParseError(3, 15, "End of line while looking for" " the branch url", NEST_PART_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest-part foo ") def test_rejects_nest_part_no_subpath(self): self.assertInstructionParseError(3, 22, "End of line while looking for" " the subpath to merge", NEST_PART_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest-part foo url:// ") def test_rejects_nest_part_no_subpath_no_space(self): self.assertInstructionParseError(3, 21, "End of line while looking for" " the subpath to merge", NEST_PART_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest-part foo url://") def test_rejects_nest_no_name(self): self.assertInstructionParseError(3, 6, "End of line while looking for" " the branch id", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest ") def test_rejects_nest_no_url(self): self.assertInstructionParseError(3, 10, "End of line while looking for the branch url", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest foo ") def test_rejects_nest_no_location(self): self.assertInstructionParseError(3, 14, "End of line while looking for the location to nest", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest foo url ") def test_rejects_text_at_end_of_nest_line(self): self.assertInstructionParseError(3, 20, "Expecting the end of the line, got 'baz'", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest foo url bar 2 baz") def test_rejects_indent_after_first_branch(self): self.assertParseError(3, 3, "Not allowed to indent unless after " "a 'nest' line", self.get_recipe, self.basic_header_and_branch + " nest foo url bar") def test_rejects_indent_after_merge(self): self.assertParseError(4, 3, "Not allowed to indent unless after " "a 'nest' line", self.get_recipe, self.basic_header_and_branch + "merge foo url\n" + " nest baz url bar") def test_rejects_tab_indent(self): self.assertParseError(4, 3, "Indents may not be done by tabs", self.get_recipe, self.basic_header_and_branch + "nest foo url bar\n" + "\t\tmerge baz url") def test_rejects_odd_space_indent(self): self.assertParseError(4, 2, "Indent not a multiple of two spaces", self.get_recipe, self.basic_header_and_branch + "nest foo url bar\n" + " merge baz url") def test_rejects_four_space_indent(self): self.assertParseError(4, 5, "Indented by more than two spaces " "at once", self.get_recipe, self.basic_header_and_branch + "nest foo url bar\n" + " merge baz url") def test_rejects_empty_recipe(self): self.assertParseError(3, 1, "Empty recipe", self.get_recipe, self.basic_header) def test_rejects_non_unique_ids(self): self.assertParseError(4, 7, "'foo' was already used to identify " "a branch.", self.get_recipe, self.basic_header_and_branch + "merge foo url\n" + "merge foo other-url\n") def test_rejects_run_with_no_instruction(self): self.assertInstructionParseError(3, 4, "End of line while looking for the command", RUN_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "run\n") def test_builds_simplest_recipe(self): base_branch = self.get_recipe(self.basic_header_and_branch) self.check_base_recipe_branch(base_branch, "http://foo.org/") def test_skips_comments(self): base_branch = self.get_recipe(self.basic_header + "# comment\n" + "http://foo.org/\n") self.check_base_recipe_branch(base_branch, "http://foo.org/") def test_builds_recipe_with_merge(self): base_branch = self.get_recipe(self.basic_header_and_branch + "merge bar http://bar.org") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) child_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "bar", "http://bar.org") def test_builds_recipe_with_nest_part(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest-part bar http://bar.org some/path") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) instruction = base_branch.child_branches[0] self.assertEqual(None, instruction.nest_path) self.check_recipe_branch( instruction.recipe_branch, "bar", "http://bar.org") self.assertEqual("some/path", instruction.subpath) self.assertEqual(None, instruction.target_subdir) def test_builds_recipe_with_nest_part_subdir(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest-part bar http://bar.org some/path target-subdir") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) instruction = base_branch.child_branches[0] self.assertEqual(None, instruction.nest_path) self.check_recipe_branch( instruction.recipe_branch, "bar", "http://bar.org") self.assertEqual("some/path", instruction.subpath) self.assertEqual("target-subdir", instruction.target_subdir) def test_builds_recipe_with_nest_part_subdir_and_revspec(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest-part bar http://bar.org some/path target-subdir 1234") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) instruction = base_branch.child_branches[0] self.assertEqual(None, instruction.nest_path) self.check_recipe_branch( instruction.recipe_branch, "bar", "http://bar.org", "1234") self.assertEqual("some/path", instruction.subpath) self.assertEqual("target-subdir", instruction.target_subdir) def test_builds_recipe_with_nest(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest bar http://bar.org baz") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) child_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("baz", location) self.check_recipe_branch(child_branch, "bar", "http://bar.org") def test_builds_recipe_with_nest_then_merge(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest bar http://bar.org baz\nmerge zam lp:zam") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=2) child_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("baz", location) self.check_recipe_branch(child_branch, "bar", "http://bar.org") child_branch, location = base_branch.child_branches[1].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "zam", "lp:zam") def test_builds_recipe_with_merge_then_nest(self): base_branch = self.get_recipe(self.basic_header_and_branch + "merge zam lp:zam\nnest bar http://bar.org baz") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=2) child_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "zam", "lp:zam") child_branch, location = base_branch.child_branches[1].as_tuple() self.assertEqual("baz", location) self.check_recipe_branch(child_branch, "bar", "http://bar.org") def test_builds_a_merge_in_to_a_nest(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest bar http://bar.org baz\n merge zam lp:zam") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) child_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("baz", location) self.check_recipe_branch(child_branch, "bar", "http://bar.org", num_child_branches=1) child_branch, location = child_branch.child_branches[0].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "zam", "lp:zam") def tests_builds_nest_into_a_nest(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest bar http://bar.org baz\n nest zam lp:zam zoo") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) child_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("baz", location) self.check_recipe_branch(child_branch, "bar", "http://bar.org", num_child_branches=1) child_branch, location = child_branch.child_branches[0].as_tuple() self.assertEqual("zoo", location) self.check_recipe_branch(child_branch, "zam", "lp:zam") def tests_builds_recipe_with_revspecs(self): base_branch = self.get_recipe(self.basic_header + "http://foo.org/ revid:a\n" + "nest bar http://bar.org baz tag:b\n" + "merge zam lp:zam 2") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=2, revspec="revid:a") instruction = base_branch.child_branches[0] child_branch = instruction.recipe_branch location = instruction.nest_path self.assertEqual("baz", location) self.check_recipe_branch(child_branch, "bar", "http://bar.org", revspec="tag:b") child_branch, location = base_branch.child_branches[1].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "zam", "lp:zam", revspec="2") def test_builds_recipe_with_commands(self): base_branch = self.get_recipe(self.basic_header + "http://foo.org/\n" + "run touch test \n") self.check_base_recipe_branch(base_branch, "http://foo.org/", num_child_branches=1) child_branch, command = base_branch.child_branches[0].as_tuple() self.assertEqual(None, child_branch) self.assertEqual("touch test", command) def test_accepts_blank_line_during_nest(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest foo http://bar.org bar\n merge baz baz.org\n\n" " merge zap zap.org\n") self.check_base_recipe_branch(base_branch, self.basic_branch, num_child_branches=1) nested_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("bar", location) self.check_recipe_branch(nested_branch, "foo", "http://bar.org", num_child_branches=2) child_branch, location = nested_branch.child_branches[0].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "baz", "baz.org") child_branch, location = nested_branch.child_branches[1].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "zap", "zap.org") def test_accepts_blank_line_at_start_of_nest(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest foo http://bar.org bar\n\n merge baz baz.org\n") self.check_base_recipe_branch(base_branch, self.basic_branch, num_child_branches=1) nested_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("bar", location) self.check_recipe_branch(nested_branch, "foo", "http://bar.org", num_child_branches=1) child_branch, location = nested_branch.child_branches[0].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "baz", "baz.org") def test_accepts_blank_line_as_only_thing_in_nest(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest foo http://bar.org bar\n\nmerge baz baz.org\n") self.check_base_recipe_branch(base_branch, self.basic_branch, num_child_branches=2) nested_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("bar", location) self.check_recipe_branch(nested_branch, "foo", "http://bar.org") child_branch, location = base_branch.child_branches[1].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "baz", "baz.org") def test_accepts_comment_line_with_any_number_of_spaces(self): base_branch = self.get_recipe(self.basic_header_and_branch + "nest foo http://bar.org bar\n #foo\nmerge baz baz.org\n") self.check_base_recipe_branch(base_branch, self.basic_branch, num_child_branches=2) nested_branch, location = base_branch.child_branches[0].as_tuple() self.assertEqual("bar", location) self.check_recipe_branch(nested_branch, "foo", "http://bar.org") child_branch, location = base_branch.child_branches[1].as_tuple() self.assertEqual(None, location) self.check_recipe_branch(child_branch, "baz", "baz.org") def test_old_format_rejects_run(self): header = ("# bzr-builder format 0.1 deb-version " + self.deb_version +"\n") self.assertParseError(3, 1, "Expecting 'merge' or 'nest', got 'run'" , self.get_recipe, header + "http://foo.org/\n" + "run touch test \n") def test_old_format_rejects_nest_part(self): header = ("# bzr-builder format 0.2 deb-version " + self.deb_version +"\n") self.assertParseError(3, 1, "Expecting 'merge', 'nest' or 'run', " "got 'nest-part'" , self.get_recipe, header + "http://foo.org/\nnest-part packaging foo test \n") def test_error_on_forbidden_instructions(self): exc = self.assertParseError(3, 1, "The 'run' instruction is " "forbidden", self.get_recipe, self.basic_header_and_branch + "run touch test\n", permitted_instructions=[SAFE_INSTRUCTIONS]) self.assertTrue(isinstance(exc, ForbiddenInstructionError)) self.assertEqual("run", exc.instruction_name) def test_error_on_duplicate_path(self): exc = self.assertInstructionParseError(3, 15, "The path '.' is a duplicate of the one used on line 1.", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest nest url .\n") def test_error_on_duplicate_path_with_another_nest(self): exc = self.assertInstructionParseError(4, 16, "The path 'foo' is a duplicate of the one used on line 3.", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest nest url foo\n" + "nest nest2 url foo\n") def test_duplicate_path_check_uses_normpath(self): exc = self.assertInstructionParseError(3, 15, "The path 'foo/..' is a duplicate of the one used on line 1.", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest nest url foo/..\n") def test_error_absolute_path(self): exc = self.assertInstructionParseError(3, 15, "Absolute paths are not allowed: /etc/passwd", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest nest url /etc/passwd\n") def test_error_simple_parent_dir(self): exc = self.assertInstructionParseError(3, 15, "Paths outside the current directory are not allowed: ../foo", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest nest url ../foo\n") def test_error_complex_parent_dir(self): exc = self.assertInstructionParseError(3, 15, "Paths outside the current directory are not allowed:" " ./foo/../..", NEST_INSTRUCTION, self.get_recipe, self.basic_header_and_branch + "nest nest url ./foo/../..\n") class BuildTreeTests(TestCaseWithTransport): if not getattr(TestCaseWithTransport, "assertPathExists", None): # Compatibility with bzr < 2.4 def assertPathExists(self, path): self.failUnlessExists(path) def test_ensure_basedir(self): to_transport = transport.get_transport("a") ensure_basedir(to_transport) self.assertPathExists("a") ensure_basedir(to_transport) self.assertPathExists("a") e = self.assertRaises(errors.BzrCommandError, ensure_basedir, transport.get_transport("b/c")) self.assertTrue('Parent of "' in str(e)) self.assertTrue('" does not exist.' in str(e)) def test_build_tree_single_branch(self): source = self.make_branch_and_tree("source") revid = source.commit("one") base_branch = BaseRecipeBranch("source", "1", 0.2) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") self.assertEqual(revid, tree.last_revision()) self.assertEqual(revid, base_branch.revid) def test_build_tree_single_branch_dir_not_branch(self): source = self.make_branch_and_tree("source") revid = source.commit("one") # We just create the target as a directory os.mkdir("target") base_branch = BaseRecipeBranch("source", "1", 0.2) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") self.assertEqual(revid, tree.last_revision()) self.assertEqual(revid, base_branch.revid) def test_build_tree_single_branch_existing_branch(self): source = self.make_branch_and_tree("source") revid = source.commit("one") self.make_branch_and_tree("target") base_branch = BaseRecipeBranch("source", "1", 0.2) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") self.assertEqual(revid, tree.last_revision()) self.assertEqual(revid, base_branch.revid) def make_source_branch(self, relpath): """Make a branch with one file and one commit.""" source1 = self.make_branch_and_tree(relpath) self.build_tree([relpath + "/a"]) source1.add(["a"]) source1.commit("one") return source1 def test_build_tree_nested(self): source1_rev_id = self.make_source_branch("source1").last_revision() source2_rev_id = self.make_source_branch("source2").last_revision() base_branch = BaseRecipeBranch("source1", "1", 0.2) nested_branch = RecipeBranch("nested", "source2") base_branch.nest_branch("sub", nested_branch) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") self.assertEqual([source1_rev_id], tree.get_parent_ids()) tree = workingtree.WorkingTree.open("target/sub") self.assertEqual([source2_rev_id], tree.get_parent_ids()) self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, nested_branch.revid) def test_build_tree_merged(self): source1 = self.make_source_branch("source1") source1_rev_id = source1.last_revision() source2 = source1.bzrdir.sprout("source2").open_workingtree() self.build_tree_contents([("source2/a", "other change")]) source2_rev_id = source2.commit("one") base_branch = BaseRecipeBranch("source1", "1", 0.2) merged_branch = RecipeBranch("merged", "source2") base_branch.merge_branch(merged_branch) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") last_revid = tree.last_revision() last_revtree = tree.branch.repository.revision_tree(last_revid) self.assertEqual([source1_rev_id, source2_rev_id], last_revtree.get_parent_ids()) self.check_file_contents("target/a", "other change") self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, merged_branch.revid) def test_build_tree_merge_unrelated(self): """Make a branch with one file and one commit.""" source1 = self.make_branch_and_tree("source1") self.build_tree_contents([("source1/a", "file a")]) source1.add(["a"]) source1_rev_id = source1.commit("one") source2 = self.make_branch_and_tree("source2") self.build_tree_contents([("source2/b", "file b")]) source2.add(["b"]) source2_rev_id = source2.commit("one") base_branch = BaseRecipeBranch("source1", "1", 0.2) merged_branch = RecipeBranch("merged", "source2") base_branch.merge_branch(merged_branch) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") last_revid = tree.last_revision() last_revtree = tree.branch.repository.revision_tree(last_revid) self.assertEqual([source1_rev_id, source2_rev_id], last_revtree.get_parent_ids()) self.check_file_contents("target/a", "file a") self.check_file_contents("target/b", "file b") self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, merged_branch.revid) def test_build_tree_implicit_dir(self): # Branches nested into non-existant directories trigger creation of # those directories. source1 = self.make_source_branch("source1") source2 = self.make_source_branch("source2") source3 = self.make_source_branch("source3") self.build_tree_contents([ ("source2/file", "new file"), ("source3/yetanotherfile", "rugby")]) source2.add(["file"]) source2.commit("two") source3.add(["yetanotherfile"]) source3.commit("three") base_branch = BaseRecipeBranch("source1", "1", 0.4) merged_branch_2 = RecipeBranch("merged", "source2") # Merge source2 into path implicit/b base_branch.nest_part_branch(merged_branch_2, ".", target_subdir="implicit/b") # Merge source3 into path implicit/moreimplicit/c merged_branch_3 = RecipeBranch("merged", "source3") base_branch.nest_part_branch(merged_branch_3, ".", target_subdir="moreimplicit/another/c") build_tree(base_branch, "target") self.check_file_contents("target/implicit/b/file", "new file") self.check_file_contents("target/moreimplicit/another/c/yetanotherfile", "rugby") def test_build_tree_nest_part(self): """A recipe can specify a merge of just part of an unrelated tree.""" source1 = self.make_source_branch("source1") source2 = self.make_source_branch("source2") source1.lock_read() self.addCleanup(source1.unlock) source1_rev_id = source1.last_revision() # Add 'b' to source2. self.build_tree_contents([ ("source2/b", "new file"), ("source2/not-b", "other file")]) source2.add(["b", "not-b"]) source2_rev_id = source2.commit("two") base_branch = BaseRecipeBranch("source1", "1", 0.2) merged_branch = RecipeBranch("merged", "source2") # Merge just 'b' from source2; 'a' is untouched. base_branch.nest_part_branch(merged_branch, "b") build_tree(base_branch, "target") file_id = source1.path2id("a") self.check_file_contents("target/a", source1.get_file_text(file_id)) self.check_file_contents("target/b", "new file") self.assertNotInWorkingTree("not-b", "target") self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, merged_branch.revid) def test_build_tree_nest_part_explicit_target(self): """A recipe can specify a merge of just part of an unrelated tree into a specific subdirectory of the target tree. """ source1 = self.make_branch_and_tree("source1") self.build_tree(["source1/dir/"]) source1.add(["dir"]) source1.commit("one") source2 = self.make_source_branch("source2") source1.lock_read() self.addCleanup(source1.unlock) source1_rev_id = source1.last_revision() # Add 'b' to source2. self.build_tree_contents([ ("source2/b", "new file"), ("source2/not-b", "other file")]) source2.add(["b", "not-b"]) source2_rev_id = source2.commit("two") base_branch = BaseRecipeBranch("source1", "1", 0.2) merged_branch = RecipeBranch("merged", "source2") # Merge just 'b' from source2; 'a' is untouched. base_branch.nest_part_branch(merged_branch, "b", "dir/b") build_tree(base_branch, "target") self.check_file_contents("target/dir/b", "new file") self.assertNotInWorkingTree("dir/not-b", "target") self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, merged_branch.revid) def test_build_tree_merge_twice(self): source1 = self.make_source_branch("source1") source1_rev_id = source1.last_revision() source2 = source1.bzrdir.sprout("source2").open_workingtree() self.build_tree_contents([("source2/a", "other change")]) source2_rev_id = source2.commit("one") source3 = source2.bzrdir.sprout("source3").open_workingtree() self.build_tree_contents([("source3/a", "third change")]) source3_rev_id = source3.commit("one") base_branch = BaseRecipeBranch("source1", "1", 0.2) merged_branch1 = RecipeBranch("merged", "source2") base_branch.merge_branch(merged_branch1) merged_branch2 = RecipeBranch("merged2", "source3") base_branch.merge_branch(merged_branch2) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") last_revid = tree.last_revision() previous_revid = tree.branch.repository.get_revision(tree.branch.last_revision()).parent_ids[0] last_revtree = tree.branch.repository.revision_tree(last_revid) previous_revtree = tree.branch.repository.revision_tree(previous_revid) self.assertEqual([previous_revid, source3_rev_id], last_revtree.get_parent_ids()) self.assertEqual([source1_rev_id, source2_rev_id], previous_revtree.get_parent_ids()) self.check_file_contents("target/a", "third change") self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, merged_branch1.revid) self.assertEqual(source3_rev_id, merged_branch2.revid) def test_build_tree_merged_with_conflicts(self): source1 = self.make_source_branch("source1") source2 = source1.bzrdir.sprout("source2").open_workingtree() self.build_tree_contents([("source2/a", "other change\n")]) source2_rev_id = source2.commit("one") self.build_tree_contents([("source1/a", "trunk change\n")]) source1_rev_id = source1.commit("two") base_branch = BaseRecipeBranch("source1", "1", 0.2) merged_branch = RecipeBranch("merged", "source2") base_branch.merge_branch(merged_branch) self.assertRaises(errors.BzrCommandError, build_tree, base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") self.assertEqual(source1_rev_id, tree.last_revision()) self.assertEqual([source1_rev_id, source2_rev_id], tree.get_parent_ids()) self.assertEqual(1, len(tree.conflicts())) conflict = tree.conflicts()[0] self.assertEqual("text conflict", conflict.typestring) self.assertEqual("a", conflict.path) self.check_file_contents("target/a", "<<<<<<< TREE\ntrunk change\n" "=======\nother change\n>>>>>>> MERGE-SOURCE\n") self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, merged_branch.revid) def test_build_tree_with_revspecs(self): source1 = self.make_source_branch("source1") source1_rev_id = source1.last_revision() source2 = source1.bzrdir.sprout("source2").open_workingtree() self.build_tree_contents([("source2/a", "other change\n")]) source2_rev_id = source2.commit("one") self.build_tree_contents([("source2/a", "unwanted change\n")]) source2.commit("one") self.build_tree_contents([("source1/a", "unwanted trunk change\n")]) source1.commit("two") base_branch = BaseRecipeBranch("source1", "1", 0.2, revspec="1") merged_branch = RecipeBranch("merged", "source2", revspec="2") base_branch.merge_branch(merged_branch) build_tree(base_branch, "target") self.assertPathExists("target") tree = workingtree.WorkingTree.open("target") last_revid = tree.last_revision() last_revtree = tree.branch.repository.revision_tree(last_revid) self.assertEqual([source1_rev_id, source2_rev_id], last_revtree.get_parent_ids()) self.assertEqual(source1_rev_id, base_branch.revid) self.assertEqual(source2_rev_id, merged_branch.revid) def test_pull_or_branch_branch(self): source = self.make_branch_and_tree("source") source.lock_write() self.addCleanup(source.unlock) self.build_tree(["source/a"]) source.add(["a"]) rev_id = source.commit("one") source.branch.tags.set_tag("one", rev_id) to_transport = transport.get_transport("target") tree_to, br_to = pull_or_branch(None, None, source.branch, to_transport, rev_id) self.addCleanup(tree_to.unlock) self.addCleanup(br_to.unlock) self.assertEqual(rev_id, tree_to.last_revision()) self.assertEqual(rev_id, br_to.last_revision()) self.assertTrue(tree_to.is_locked()) self.assertTrue(br_to.is_locked()) self.assertEqual(rev_id, br_to.tags.lookup_tag("one")) def test_pull_or_branch_branch_in_no_trees_repo(self): """When in a no-trees repo we need to force a working tree""" repo = self.make_repository(".", shared=True) repo.set_make_working_trees(False) source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) rev_id = source.commit("one") source.branch.tags.set_tag("one", rev_id) to_transport = transport.get_transport("target") tree_to, br_to = pull_or_branch(None, None, source.branch, to_transport, rev_id) self.addCleanup(tree_to.unlock) self.addCleanup(br_to.unlock) self.assertEqual(rev_id, tree_to.last_revision()) self.assertEqual(rev_id, br_to.last_revision()) self.assertTrue(tree_to.is_locked()) self.assertTrue(br_to.is_locked()) self.assertEqual(rev_id, br_to.tags.lookup_tag("one")) def test_pull_or_branch_pull_with_tree(self): source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) first_rev_id = source.commit("one") source.branch.tags.set_tag("one", first_rev_id) to_transport = transport.get_transport("target") tree_to, br_to = pull_or_branch(None, None, source.branch, to_transport, first_rev_id) self.addCleanup(tree_to.unlock) self.addCleanup(br_to.unlock) self.build_tree(["source/b"]) source.add(["b"]) rev_id = source.commit("two") source.branch.tags.set_tag("one", rev_id) tree_to, br_to = pull_or_branch(tree_to, br_to, source.branch, to_transport, rev_id) self.assertEqual(rev_id, tree_to.last_revision()) self.assertEqual(rev_id, br_to.last_revision()) self.assertTrue(tree_to.is_locked()) self.assertTrue(br_to.is_locked()) # Changed tag isn't overwritten self.assertEqual(first_rev_id, br_to.tags.lookup_tag("one")) def test_pull_or_branch_pull_with_no_tree(self): source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) first_rev_id = source.commit("one") source.branch.tags.set_tag("one", first_rev_id) to_transport = transport.get_transport("target") tree_to, br_to = pull_or_branch(None, None, source.branch, to_transport, first_rev_id) tree_to.unlock() tree_to.bzrdir.destroy_workingtree() self.build_tree(["source/b"]) source.add(["b"]) rev_id = source.commit("two") source.branch.tags.set_tag("one", rev_id) tree_to, br_to = pull_or_branch(None, br_to, source.branch, to_transport, rev_id) self.addCleanup(tree_to.unlock) self.addCleanup(br_to.unlock) self.assertEqual(rev_id, tree_to.last_revision()) self.assertEqual(rev_id, br_to.last_revision()) self.assertTrue(tree_to.is_locked()) self.assertTrue(br_to.is_locked()) # Changed tag isn't overwritten self.assertEqual(first_rev_id, br_to.tags.lookup_tag("one")) def test_pull_or_branch_pull_with_conflicts(self): source = self.make_branch_and_tree("source") self.build_tree(["source/a"]) source.add(["a"]) first_rev_id = source.commit("one") source.branch.tags.set_tag("one", first_rev_id) to_transport = transport.get_transport("target") tree_to, br_to = pull_or_branch(None, None, source.branch, to_transport, first_rev_id) self.build_tree(["source/b"]) self.build_tree_contents([("target/b", "other contents")]) source.add(["b"]) rev_id = source.commit("two") source.branch.tags.set_tag("one", rev_id) e = self.assertRaises(errors.BzrCommandError, pull_or_branch, tree_to, br_to, source.branch, to_transport, rev_id, accelerator_tree=source) self.assertEqual("Conflicts... aborting.", str(e)) tree_to.unlock() br_to.unlock() tree_to = workingtree.WorkingTree.open("target") br_to = tree_to.branch self.assertEqual(rev_id, tree_to.last_revision()) self.assertEqual(rev_id, br_to.last_revision()) # Changed tag isn't overwritten self.assertEqual(first_rev_id, br_to.tags.lookup_tag("one")) self.assertEqual(1, len(tree_to.conflicts())) conflict = tree_to.conflicts()[0] self.assertEqual("duplicate", conflict.typestring) self.assertEqual("b.moved", conflict.path) self.assertEqual("b", conflict.conflict_path) def test_build_tree_runs_commands(self): source = self.make_branch_and_tree("source") revid = source.commit("one") base_branch = BaseRecipeBranch("source", "1", 0.2) base_branch.run_command("touch test") build_tree(base_branch, "target") self.assertPathExists("target") self.assertPathExists("target/test") tree = workingtree.WorkingTree.open("target") self.assertEqual(revid, tree.last_revision()) self.assertEqual(revid, base_branch.revid) def test_error_on_merge_revspec(self): # See bug 416950 source = self.make_branch_and_tree("source") revid = source.commit("one") base_branch = BaseRecipeBranch("source", "1", 0.2) merged_branch = RecipeBranch("merged", "source", revspec="debian") base_branch.merge_branch(merged_branch) e = self.assertRaises(errors.InvalidRevisionSpec, build_tree, base_branch, "target") self.assertTrue(str(e).startswith("Requested revision: 'debian' " "does not exist in branch: ")) self.assertTrue(str(e).endswith(". Did you not mean to specify a " "revspec at the end of the merge line?")) class StringifyTests(TestCaseInTempDir): def test_missing_debversion(self): base_branch = BaseRecipeBranch("base_url", None, 0.1) base_branch.revid = "base_revid" manifest = str(base_branch) self.assertEqual("# bzr-builder format 0.1\n" "base_url revid:base_revid\n", manifest) def test_simple_manifest(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" manifest = str(base_branch) self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url revid:base_revid\n", manifest) def test_complex_manifest(self): base_branch = BaseRecipeBranch("base_url", "2", 0.2) base_branch.revid = "base_revid" nested_branch1 = RecipeBranch("nested1", "nested1_url") nested_branch1.revid = "nested1_revid" base_branch.nest_branch("nested", nested_branch1) nested_branch2 = RecipeBranch("nested2", "nested2_url") nested_branch2.revid = "nested2_revid" nested_branch1.nest_branch("nested2", nested_branch2) merged_branch = RecipeBranch("merged", "merged_url") merged_branch.revid = "merged_revid" base_branch.merge_branch(merged_branch) manifest = str(base_branch) self.assertEqual("# bzr-builder format 0.2 deb-version 2\n" "base_url revid:base_revid\n" "nest nested1 nested1_url nested revid:nested1_revid\n" " nest nested2 nested2_url nested2 revid:nested2_revid\n" "merge merged merged_url revid:merged_revid\n", manifest) def test_manifest_with_command(self): base_branch = BaseRecipeBranch("base_url", "1", 0.2) base_branch.revid = "base_revid" base_branch.run_command("touch test") manifest = str(base_branch) self.assertEqual("# bzr-builder format 0.2 deb-version 1\n" "base_url revid:base_revid\n" "run touch test\n", manifest) def test_recipe_with_no_revspec(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) manifest = str(base_branch) self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url\n", manifest) def test_recipe_with_tag_revspec(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1, revspec="tag:foo") manifest = str(base_branch) self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url tag:foo\n", manifest) def test_recipe_with_child(self): base_branch = BaseRecipeBranch("base_url", "2", 0.2) nested_branch1 = RecipeBranch("nested1", "nested1_url", revspec="tag:foo") base_branch.nest_branch("nested", nested_branch1) nested_branch2 = RecipeBranch("nested2", "nested2_url") nested_branch1.nest_branch("nested2", nested_branch2) merged_branch = RecipeBranch("merged", "merged_url") base_branch.merge_branch(merged_branch) manifest = str(base_branch) self.assertEqual("# bzr-builder format 0.2 deb-version 2\n" "base_url\n" "nest nested1 nested1_url nested tag:foo\n" " nest nested2 nested2_url nested2\n" "merge merged merged_url\n", manifest) def test_get_recipe_text(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" manifest = base_branch.get_recipe_text() self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url revid:base_revid\n", manifest) def test_get_recipe_doesnt_raise_on_invalid_recipe(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" nested_branch1 = RecipeBranch("nested1", "nested1_url", revspec="tag:foo") base_branch.nest_branch(".", nested_branch1) manifest = base_branch.get_recipe_text() self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url revid:base_revid\n" "nest nested1 nested1_url . tag:foo\n", manifest) def test_get_recipe_text_validate_True(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" nested_branch1 = RecipeBranch("nested1", "nested1_url", revspec="tag:foo") base_branch.nest_branch(".", nested_branch1) self.assertRaises(InstructionParseError, base_branch.get_recipe_text, validate=True) def test_str_validates(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" nested_branch1 = RecipeBranch("nested1", "nested1_url", revspec="tag:foo") base_branch.nest_branch(".", nested_branch1) self.assertRaises(InstructionParseError, str, base_branch) def test_with_nest_part(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" nested_branch1 = RecipeBranch("nested1", "nested1_url", revspec="tag:foo") base_branch.nest_part_branch(nested_branch1, "foo", "bar") manifest = base_branch.get_recipe_text() self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url revid:base_revid\n" "nest-part nested1 nested1_url foo bar tag:foo\n", manifest) def test_with_nest_part_with_no_target_dir(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" nested_branch1 = RecipeBranch("nested1", "nested1_url", revspec="tag:foo") base_branch.nest_part_branch(nested_branch1, "foo", None) manifest = base_branch.get_recipe_text() self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url revid:base_revid\n" "nest-part nested1 nested1_url foo foo tag:foo\n", manifest) def test_with_nest_part_with_no_target_dir_no_revspec(self): base_branch = BaseRecipeBranch("base_url", "1", 0.1) base_branch.revid = "base_revid" nested_branch1 = RecipeBranch("nested1", "nested1_url") base_branch.nest_part_branch(nested_branch1, "foo", None) manifest = base_branch.get_recipe_text() self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" "base_url revid:base_revid\n" "nest-part nested1 nested1_url foo\n", manifest) class RecipeBranchTests(TestCaseWithTransport): def test_base_recipe_branch(self): base_branch = BaseRecipeBranch("base_url", "1", 0.2, revspec="2") self.assertEqual(None, base_branch.name) self.assertEqual("base_url", base_branch.url) self.assertEqual("1", base_branch.deb_version) self.assertEqual("2", base_branch.revspec) self.assertEqual(0, len(base_branch.child_branches)) self.assertEqual(None, base_branch.revid) def test_recipe_branch(self): branch = RecipeBranch("name", "url", revspec="2") self.assertEqual("name", branch.name) self.assertEqual("url", branch.url) self.assertEqual("2", branch.revspec) self.assertEqual(0, len(branch.child_branches)) self.assertEqual(None, branch.revid) def test_different_shape_to(self): branch1 = BaseRecipeBranch("base_url", "1", 0.2, revspec="2") branch2 = BaseRecipeBranch("base_url", "1", 0.2, revspec="3") self.assertFalse(branch1.different_shape_to(branch2)) branch2 = BaseRecipeBranch("base", "1", 0.2, revspec="2") self.assertTrue(branch1.different_shape_to(branch2)) branch2 = BaseRecipeBranch("base_url", "2", 0.2, revspec="2") self.assertFalse(branch1.different_shape_to(branch2)) rbranch1 = RecipeBranch("name", "other_url") rbranch2 = RecipeBranch("name2", "other_url") self.assertTrue(rbranch1.different_shape_to(rbranch2)) rbranch2 = RecipeBranch("name", "other_url2") self.assertTrue(rbranch1.different_shape_to(rbranch2)) def test_list_branch_names(self): base_branch = BaseRecipeBranch("base_url", "1", 0.2) base_branch.merge_branch(RecipeBranch("merged", "merged_url")) nested_branch = RecipeBranch("nested", "nested_url") nested_branch.merge_branch( RecipeBranch("merged_into_nested", "another_url")) base_branch.nest_branch("subdir", nested_branch) base_branch.merge_branch( RecipeBranch("another_nested", "yet_another_url")) base_branch.run_command("a command") self.assertEqual( ["merged", "nested", "merged_into_nested", "another_nested"], base_branch.list_branch_names()) def test_iter_all_branches(self): base_branch = BaseRecipeBranch("base_url", "1", 0.2) merged_branch = RecipeBranch("merged", "merged_url") base_branch.merge_branch(merged_branch) nested_branch = RecipeBranch("nested", "nested_url") merge_into_nested_branch = RecipeBranch("merged_into_nested", "another_url") nested_branch.merge_branch(merge_into_nested_branch) base_branch.nest_branch("subdir", nested_branch) another_nested_branch = RecipeBranch("another_nested", "yet_another_url") base_branch.merge_branch(another_nested_branch) base_branch.run_command("a command") self.assertEqual([ base_branch, merged_branch, nested_branch, merge_into_nested_branch, another_nested_branch], list(base_branch.iter_all_branches())) def test_iter_all_instructions(self): base_branch = BaseRecipeBranch("base_url", "1", 0.2) nested_branch = RecipeBranch("nested", "nested_url") merge_into_nested_branch = RecipeBranch("merged_into_nested", "another_url") nested_branch.merge_branch(merge_into_nested_branch) merge_into_nested = nested_branch.child_branches[-1] base_branch.run_command("a command") cmd = base_branch.child_branches[-1] base_branch.nest_branch("subdir", nested_branch) nest = base_branch.child_branches[-1] self.assertEqual([ cmd, nest, merge_into_nested], list(base_branch.iter_all_instructions()))