Merge lp:~spiv/bzr-builder/merge-subdirs-479705 into lp:~james-w/bzr-builder/trunk-old
- merge-subdirs-479705
- Merge into trunk-old
Status: | Work in progress | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~spiv/bzr-builder/merge-subdirs-479705 | ||||
Merge into: | lp:~james-w/bzr-builder/trunk-old | ||||
Prerequisite: | lp:~spiv/bzr-builder/refactor | ||||
Diff against target: |
2784 lines (+1496/-600) 10 files modified
TODO (+0/-3) __init__.py (+33/-377) cmds.py (+492/-0) ppa.py (+124/-0) recipe.py (+421/-150) setup.py (+13/-12) tests/__init__.py (+4/-3) tests/test_blackbox.py (+62/-16) tests/test_ppa.py (+28/-0) tests/test_recipe.py (+319/-39) |
||||
To merge this branch: | bzr merge lp:~spiv/bzr-builder/merge-subdirs-479705 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andrew Bennetts (community) | Disapprove | ||
James Westby | Needs Fixing | ||
Review via email: mp+14979@code.launchpad.net |
Commit message
Description of the change
James Westby (james-w) wrote : | # |
Hi,
Thanks for working on this.
I have some concerns about the utility of it. If the branch
you are merging doesn't make any modifications outside of
./debian/ then this makes no difference, therefore it must
be for the case where it does.
Is this then to take the packaging without any modifications
to the source, so that it is a vanilla packaging?
If so, then it is just assumed that this won't be done if
some of the modifications are needed, or if the packaging
depends on some of the modifications?
That would be acceptable, but I wonder why we want to encourage
people to do things this way, instead of say, layering another
branch on top that undoes the unwanted things? Obviously the two
are not equivalent, but the latter seems to fit better with the
model to me.
As for a code review, two things mean that this is not mergeable
as is:
* Needs a format bump.
* Needs an update to the documentation so that people know it is possible.
Other than that the changes look fine with one obvious exception:
45 + :param subpath: XXX
Thanks,
James
Robert Collins (lifeless) wrote : | # |
> Hi,
>
> Thanks for working on this.
>
> I have some concerns about the utility of it. If the branch
> you are merging doesn't make any modifications outside of
> ./debian/ then this makes no difference, therefore it must
> be for the case where it does.
This is false. If the branches are not related, merging a subdir can avoid very many unnecessary conflicts while still generating a single tree.
> Is this then to take the packaging without any modifications
> to the source, so that it is a vanilla packaging?
Or to merge unrelated branches.
> As for a code review, two things mean that this is not mergeable
> as is:
>
> * Needs a format bump.
I don't see why it needs this: old versions will error appropriately, new versions will work.
If old versions would misbehave then I'd agree on the need to bump the format.
James Westby (james-w) wrote : | # |
On Tue Nov 24 00:59:18 UTC 2009 Robert Collins wrote:
> > Hi,
> >
> > Thanks for working on this.
> >
> > I have some concerns about the utility of it. If the branch
> > you are merging doesn't make any modifications outside of
> > ./debian/ then this makes no difference, therefore it must
> > be for the case where it does.
>
> This is false. If the branches are not related, merging a subdir can avoid very
> many unnecessary conflicts while still generating a single tree.
If the branches are not related there is no way in builder to merge
them at this point anyway (no way to specify a merge base). I haven't
tested to make sure that the API works the same as the UI here, so
apologies if I mispoke.
I appreciate trying to improve this situation, but I'm not sure that
this is the correct fix, at least at this time. I would like to know
more about the design though, as I feel that there is something I am
missing.
Thanks,
James
Andrew Bennetts (spiv) wrote : | # |
> > This is false. If the branches are not related, merging a subdir can avoid
> very
> > many unnecessary conflicts while still generating a single tree.
>
> If the branches are not related there is no way in builder to merge
> them at this point anyway (no way to specify a merge base). I haven't
> tested to make sure that the API works the same as the UI here, so
> apologies if I mispoke.
You're right about that. It would be easy to catch UnrelatedBranches and if a subpath is specified try again with a base of NULL_REVISION. Or we could add a 'mergeunrelated' directive. Or further extend the syntax to allow specifying bases.
Do you have a preference for one of those options, or do you feel this is the wrong track entirely? It seems to me that however it is specified a partial merge is a fairly neat way to provide "please give me this branch, plus the debian/ dir from another branch."
I'm sure there are many cases where blindly combining a debian/ dir from an existing package with an arbitrary version of upstream won't work. I think it's likely it will
work ok for many nightly packages though: have a recipe that specifies "current trunk + packaging from $recent-packaging". I'm not certain which use-case Robert had in mind, but that's what I'm thinking of.
James Westby (james-w) wrote : | # |
Hi,
I'm sorry the discussion on this has stalled, but I'm not sure what to do
about it.
I'd rather know the concrete use cases for the change and how this would fix it
before merging so that I can better support them in future. Help understanding that
would be greatly appreciated.
Thanks,
James
Robert Collins (lifeless) wrote : | # |
So, in two parts:
- Andrew is looking at making sure it works with unrelated branches.
Use cases: as we discussed, for launchpads 'build from branch' to be successful for upstreams, they need easy access to debian/ in package branches that are likely to have no history relationship: This syntax allows them to specify that that is what they want without us resorting to guessing.
James Westby (james-w) wrote : | # |
Hi,
I think I would like to see catching UnrelatedBranches, and then if a subdir
is specified, try again with NULL_REVISION as the base. If there are then
any conflicts it should bail out as normal.
This should have it work for the simple case, which is a start.
Why specify the subdirectory separate from the URL? Could we not do an
open_containing and use the subpath returned from that?
Thanks,
James
- 64. By Andrew Bennetts
-
Make merge_unrelated
_branch work using my merge-into-merger branch of bzrlib. - 65. By Andrew Bennetts
-
Define a new 'merge-into' instruction.
- 66. By Andrew Bennetts
-
Expand docstrings, and allow specifying a target subdir.
- 67. By Andrew Bennetts
-
Bump format, add tests for parsing of merge-into, revert unused change to allow 'merge' instructions to take subpaths.
- 68. By Andrew Bennetts
-
Reduce cruft slightly.
- 69. By Andrew Bennetts
-
Rename 'merge-into' instruction to 'merge-part'.
James Westby (james-w) wrote : | # |
Hi Andrew,
This is great now, thanks.
merge-part NAME BRANCH REVISION SUBDIR [TARGET-DIR]
I think I might prefer
merge-part NAME BRANCH SUBDIR REVISION [TARGET-DIR]
(Maybe even:
merge-part NAME BRANCH SUBDIR [REVISION [TARGET-DIR]]
or
merge-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]])
We can't do
from bzrlib.cleanup import OperationWithCl
unfortunately, as it is too new for the bzr that we need to work with.
We could backport newer bzr's for building older distroseries, but that's
quite a bit of work.
MergeIntoMerger only seems to be in newer bzr's too? Is there any way that
we can avoid depending on that?
108 + if target_subdir is None:
109 + target_subdir = os.path.
I wouldn't expect that, I would think that it would just be "subpath".
I'm happy to do the work to merge this in, the only blocker right now
is MergeIntoMerger, so if you could comment on that then we can move
forward.
Thanks,
James
James Westby (james-w) wrote : | # |
Right, sorry, I now see that MergeIntoMerger is proposed
for adding to bzr.
I think that's the right way to go as it should be in bzr,
but it does make this hard for deploying on older distroseries
in Launchpad.
Thanks,
James
Andrew Bennetts (spiv) wrote : | # |
James Westby wrote:
> Right, sorry, I now see that MergeIntoMerger is proposed
> for adding to bzr.
>
> I think that's the right way to go as it should be in bzr,
> but it does make this hard for deploying on older distroseries
> in Launchpad.
Ah, I didn't realise that was a requirement. I can extract that out in a way
that will work with older bzrs without much trouble. Would you like that in
bzr-builder or a separate plugin?
Also, what's the oldest bzr version that bzr-builder wants to work with?
James Westby (james-w) wrote : | # |
On Fri, 09 Jul 2010 00:49:38 -0000, Andrew Bennetts <email address hidden> wrote:
> James Westby wrote:
> > Right, sorry, I now see that MergeIntoMerger is proposed
> > for adding to bzr.
> >
> > I think that's the right way to go as it should be in bzr,
> > but it does make this hard for deploying on older distroseries
> > in Launchpad.
>
> Ah, I didn't realise that was a requirement. I can extract that out in a way
> that will work with older bzrs without much trouble. Would you like that in
> bzr-builder or a separate plugin?
Great. bzr-builder is easier.
Do you think it should still go in bzr with a "backport" inside
bzr-builder?
> Also, what's the oldest bzr version that bzr-builder wants to work with?
Well, dapper is still "supported" so 0.8.2.
Really though we're probably only going to have hardy and later,
meaning 1.3.1.
Previously bzr-builder used very simple APIs, so this wasn't really an
issue. If we keep running in to this we can review our deployment and
perhaps use a newer bzr as well as a newer bzr-builder (given that the
plugin isn't even in hardy).
Thanks,
James
Andrew Bennetts (spiv) wrote : | # |
James Westby wrote:
> On Fri, 09 Jul 2010 00:49:38 -0000, Andrew Bennetts <email address hidden> wrote:
[...]
> Great. bzr-builder is easier.
>
> Do you think it should still go in bzr with a "backport" inside
> bzr-builder?
Yes, exactly.
> > Also, what's the oldest bzr version that bzr-builder wants to work with?
>
> Well, dapper is still "supported" so 0.8.2.
Ouch!
> Really though we're probably only going to have hardy and later,
> meaning 1.3.1.
>
> Previously bzr-builder used very simple APIs, so this wasn't really an
> issue. If we keep running in to this we can review our deployment and
> perhaps use a newer bzr as well as a newer bzr-builder (given that the
> plugin isn't even in hardy).
Considering that so many branches on Launchpad are 2a format now it
seems to me that bzr >= 2.0 is almost a requirement anyway. Please tell
me I'm right ;)
A compromise be to say "merge-part" is not available unless you
are using a new-enough bzr. (Again, probably >= 2.0 with the help of a
backport in bzr-builder.)
James Westby (james-w) wrote : | # |
On Fri, 09 Jul 2010 05:25:53 -0000, Andrew Bennetts <email address hidden> wrote:
> Considering that so many branches on Launchpad are 2a format now it
> seems to me that bzr >= 2.0 is almost a requirement anyway. Please tell
> me I'm right ;)
That's a good point. It may be that the archive we are using to backport
bzr-builder already has a bzr in for other reasons, and so that is
getting picked up.
Checking...
It does not, so indeed the newer branch formats can't be built on older
releases.
Filed
https:/
about this.
Depending on how we fix this we could then not put the new classes in to
bzr-builder.
> A compromise be to say "merge-part" is not available unless you
> are using a new-enough bzr. (Again, probably >= 2.0 with the help of a
> backport in bzr-builder.)
I don't fancy that, but it would be an option, yes.
Thanks,
James
Daniel Hahler (blueyed) wrote : | # |
I am looking forward to using this, but wonder why it used with branching ("merge") and not nesting ("nest")?
Andrew wrote:
> have a recipe that specifies "current trunk + packaging from $recent-packaging".
That's the most common use case and looks like "nesting", not merging.
As far as I understand the following will take of this / make it behave like "nest":
> I think I would like to see catching UnrelatedBranches, and then if
> a subdir is specified, try again with NULL_REVISION as the base.
Andrew Bennetts (spiv) wrote : | # |
Daniel: I agree, nest-part is probably a better description for users. It happens that the implementation in bzr reuses a bunch of merge infrastructure, but I think it's fair to say that's an implementation detail that is irrelevant to users. So I've updated the patch accordingly, thank you!
James: So what is the plan for dealing or not with older bzr versions for now? The relevant bzr change is with PQM for landing in 2.2 right now. I'll going resubmit this patch for merging without any backwards compat for bzr < 2.2 included, but if you do want it I can make an additional proposal to copy the MergeIntoMerge bits into bzr-builder. At this point I'm basically just asking for a clear "Yes, do that" or "No, don't bother" instruction and then I'll go and do as you say, the current plan is a bit unclear with bug 603615 not going anywhere atm.
(Because the trunk branch has changed since I first created this proposal I'm going to mark this one as rejected and create a new one. But I really just resubmitting this patch.)
Andrew Bennetts (spiv) wrote : | # |
The new merge proposal is at <https:/
- 70. By Andrew Bennetts
-
Rename merge-part instruction to nest-part.
- 71. By Andrew Bennetts
-
Merge trunk.
- 72. By Andrew Bennetts
-
Change the syntax of nest-part to have the revspec at the end, and make it optional.
- 73. By Andrew Bennetts
-
Tweak plugin help text.
Unmerged revisions
- 73. By Andrew Bennetts
-
Tweak plugin help text.
- 72. By Andrew Bennetts
-
Change the syntax of nest-part to have the revspec at the end, and make it optional.
- 71. By Andrew Bennetts
-
Merge trunk.
- 70. By Andrew Bennetts
-
Rename merge-part instruction to nest-part.
- 69. By Andrew Bennetts
-
Rename 'merge-into' instruction to 'merge-part'.
- 68. By Andrew Bennetts
-
Reduce cruft slightly.
- 67. By Andrew Bennetts
-
Bump format, add tests for parsing of merge-into, revert unused change to allow 'merge' instructions to take subpaths.
- 66. By Andrew Bennetts
-
Expand docstrings, and allow specifying a target subdir.
- 65. By Andrew Bennetts
-
Define a new 'merge-into' instruction.
- 64. By Andrew Bennetts
-
Make merge_unrelated
_branch work using my merge-into-merger branch of bzrlib.
Preview Diff
1 | === modified file 'TODO' |
2 | --- TODO 2009-04-15 14:10:19 +0000 |
3 | +++ TODO 2010-08-13 01:14:42 +0000 |
4 | @@ -1,6 +1,3 @@ |
5 | -- Implement building the source package from the tree, including adding |
6 | - the changelog entry with the desired version. |
7 | -- Implement uploading to a PPA. |
8 | - Documentation. |
9 | - Work out how to get tarballs for non-native packages if we want that. |
10 | - Decide on error on existing target directory vs. re-use and pull changes. |
11 | |
12 | === modified file '__init__.py' |
13 | --- __init__.py 2010-08-13 01:14:42 +0000 |
14 | +++ __init__.py 2010-08-13 01:14:42 +0000 |
15 | @@ -21,7 +21,7 @@ |
16 | |
17 | A recipe is just a text file that starts with a line such as:: |
18 | |
19 | - # bzr-builder format 0.2 deb-version 1.0+{revno}-{revno:packaging} |
20 | + # bzr-builder format 0.3 deb-version 1.0+{revno}-{revno:packaging} |
21 | |
22 | The format specifier is there to allow the syntax to be changed in later |
23 | versions, and the meaning of "deb-version" will be explained later. |
24 | @@ -70,6 +70,20 @@ |
25 | for the revisionspec is identical to that taken by the "--revision" argument |
26 | to many bzr commands. See "bzr help revisionspec" for details. |
27 | |
28 | +You can also merge specific subdirectories from a branch with a "nest-part" |
29 | +line like |
30 | + |
31 | +nest-part packaging lp:~foo-dev/foo/packaging debian |
32 | + |
33 | +which specifies that the only the debian/ subdirectory should be merged. This |
34 | +works even if the branches share no revision history. You can optionally |
35 | +specify the subdirectory and revision in the target with a line like |
36 | + |
37 | +nest-part libfoo lp:libfoo src lib/foo tag:release-1.2 |
38 | + |
39 | +which will put the "src" directory of libfoo in "lib/foo", using the revision |
40 | +of libfoo tagged "release-1.2". |
41 | + |
42 | It is also possible to run an arbitrary command at a particular point in the |
43 | construction process. For example:: |
44 | |
45 | @@ -104,14 +118,28 @@ |
46 | |
47 | * {time} will be substituted with the current date and time, such as |
48 | 200908191512. |
49 | + * {date} will be substituted with just the current date, such as |
50 | + 20090819. |
51 | * {revno} will be the revno of the base branch (the first specified). |
52 | * {revno:<branch name>} will be substituted with the revno for the |
53 | branch named <branch name> in the recipe. |
54 | + * {debupstream} will be replaced by the upstream portion of the version |
55 | + number taken from debian/changelog in the final tree. If when the |
56 | + tree is built the top of debian/changelog has a version number of |
57 | + "1.0-1" then this would evaluate to "1.0". |
58 | + |
59 | +Instruction syntax summary: |
60 | + |
61 | + * nest NAME BRANCH TARGET-DIR [REVISION] |
62 | + * merge NAME BRANCH [REVISION] |
63 | + * nest-part NAME BRANCH SUBDIR [TARGET-DIR [REVISION]] |
64 | + * run COMMAND |
65 | |
66 | Format versions: |
67 | |
68 | 0.1 - original format. |
69 | 0.2 - added "run" instruction. |
70 | + 0.3 - added "nest-part" instruction. |
71 | """ |
72 | |
73 | if __name__ == '__main__': |
74 | @@ -123,382 +151,10 @@ |
75 | shell=True, env={"BZR_PLUGIN_PATH": dir}) |
76 | sys.exit(retcode) |
77 | |
78 | -import datetime |
79 | -from email import utils |
80 | -import os |
81 | -import pwd |
82 | -import re |
83 | -import socket |
84 | -import shutil |
85 | -import subprocess |
86 | -import tempfile |
87 | - |
88 | -from debian_bundle import changelog |
89 | - |
90 | -from bzrlib import ( |
91 | - errors, |
92 | - trace, |
93 | - transport, |
94 | - ) |
95 | -from bzrlib.commands import Command, register_command |
96 | -from bzrlib.option import Option |
97 | - |
98 | -from bzrlib.plugins.builder.recipe import ( |
99 | - build_manifest, |
100 | - build_tree, |
101 | - RecipeParser, |
102 | - resolve_revisions, |
103 | - ) |
104 | - |
105 | - |
106 | -def write_manifest_to_path(path, base_branch): |
107 | - parent_dir = os.path.dirname(path) |
108 | - if parent_dir != '' and not os.path.exists(parent_dir): |
109 | - os.makedirs(parent_dir) |
110 | - manifest_f = open(path, 'wb') |
111 | - try: |
112 | - manifest_f.write(build_manifest(base_branch)) |
113 | - finally: |
114 | - manifest_f.close() |
115 | - |
116 | - |
117 | -def get_branch_from_recipe_file(recipe_file): |
118 | - recipe_transport = transport.get_transport(os.path.dirname(recipe_file)) |
119 | - try: |
120 | - recipe_contents = recipe_transport.get_bytes( |
121 | - os.path.basename(recipe_file)) |
122 | - except errors.NoSuchFile: |
123 | - raise errors.BzrCommandError("Specified recipe does not exist: " |
124 | - "%s" % recipe_file) |
125 | - parser = RecipeParser(recipe_contents, filename=recipe_file) |
126 | - return parser.parse() |
127 | - |
128 | - |
129 | -def get_old_recipe(if_changed_from): |
130 | - old_manifest_transport = transport.get_transport(os.path.dirname( |
131 | - if_changed_from)) |
132 | - try: |
133 | - old_manifest_contents = old_manifest_transport.get_bytes( |
134 | - os.path.basename(if_changed_from)) |
135 | - except errors.NoSuchFile: |
136 | - return None |
137 | - old_recipe = RecipeParser(old_manifest_contents, |
138 | - filename=if_changed_from).parse() |
139 | - return old_recipe |
140 | - |
141 | - |
142 | -def get_maintainer(): |
143 | - """ |
144 | - Create maintainer string using the same algorithm as in dch |
145 | - """ |
146 | - env = os.environ |
147 | - regex = re.compile(r"^(.*)\s+<(.*)>$") |
148 | - |
149 | - # Split email and name |
150 | - if 'DEBEMAIL' in env: |
151 | - match_obj = regex.match(env['DEBEMAIL']) |
152 | - if match_obj: |
153 | - if not 'DEBFULLNAME' in env: |
154 | - env['DEBFULLNAME'] = match_obj.group(1) |
155 | - env['DEBEMAIL'] = match_obj.group(2) |
156 | - if 'DEBEMAIL' not in env or 'DEBFULLNAME' not in env: |
157 | - if 'EMAIL' in env: |
158 | - match_obj = regex.match(env['EMAIL']) |
159 | - if match_obj: |
160 | - if not 'DEBFULLNAME' in env: |
161 | - env['DEBFULLNAME'] = match_obj.group(1) |
162 | - env['EMAIL'] = match_obj.group(2) |
163 | - |
164 | - # Get maintainer's name |
165 | - if 'DEBFULLNAME' in env: |
166 | - maintainer = env['DEBFULLNAME'] |
167 | - elif 'NAME' in env: |
168 | - maintainer = env['NAME'] |
169 | - else: |
170 | - # Use password database if no data in environment variables |
171 | - try: |
172 | - maintainer = re.sub(r',.*', '', pwd.getpwuid(os.getuid()).pw_gecos) |
173 | - except KeyError, AttributeError: |
174 | - # TBD: Use last changelog entry value |
175 | - maintainer = "bzr-builder" |
176 | - |
177 | - # Get maintainer's mail address |
178 | - if 'DEBEMAIL' in env: |
179 | - email = env['DEBEMAIL'] |
180 | - elif 'EMAIL' in env: |
181 | - email = env['EMAIL'] |
182 | - else: |
183 | - addr = None |
184 | - if os.path.exists('/etc/mailname'): |
185 | - f = open('/etc/mailname') |
186 | - try: |
187 | - addr = f.readline().strip() |
188 | - finally: |
189 | - f.close() |
190 | - if not addr: |
191 | - addr = socket.getfqdn() |
192 | - if addr: |
193 | - user = pwd.getpwuid(os.getuid()).pw_name |
194 | - if not user: |
195 | - addr = None |
196 | - else: |
197 | - addr = "%s@%s" % (user, addr) |
198 | - |
199 | - if addr: |
200 | - email = addr |
201 | - else: |
202 | - # TBD: Use last changelog entry value |
203 | - email = "none@example.org" |
204 | - |
205 | - return (maintainer, email) |
206 | - |
207 | - |
208 | -def add_changelog_entry(base_branch, basedir, distribution=None, |
209 | - package=None): |
210 | - debian_dir = os.path.join(basedir, "debian") |
211 | - if not os.path.exists(debian_dir): |
212 | - os.makedirs(debian_dir) |
213 | - cl_path = os.path.join(debian_dir, "changelog") |
214 | - if os.path.exists(cl_path): |
215 | - cl_f = open(cl_path) |
216 | - try: |
217 | - cl = changelog.Changelog(file=cl_f) |
218 | - finally: |
219 | - cl_f.close() |
220 | - else: |
221 | - cl = changelog.Changelog() |
222 | - if len(cl._blocks) > 0: |
223 | - if distribution is None: |
224 | - distribution = cl._blocks[0].distributions.split()[0] |
225 | - if package is None: |
226 | - package = cl._blocks[0].package |
227 | - if "{debupstream}" in base_branch.deb_version: |
228 | - cl_version = cl._blocks[0].version |
229 | - base_branch.deb_version = base_branch.deb_version.replace( |
230 | - "{debupstream}", cl_version.upstream_version) |
231 | - else: |
232 | - if package is None: |
233 | - raise errors.BzrCommandError("No previous changelog to " |
234 | - "take the package name from, and --package not " |
235 | - "specified.") |
236 | - if "{debupstream}" in base_branch.deb_version: |
237 | - raise errors.BzrCommandError("No previous changelog to " |
238 | - "take the upstream version from - {debupstream} was " |
239 | - "used.") |
240 | - if distribution is None: |
241 | - distribution = "jaunty" |
242 | - # Use debian packaging environment variables |
243 | - # or default values if they don't exist |
244 | - author = "%s <%s>" % get_maintainer() |
245 | - |
246 | - date = utils.formatdate(localtime=True) |
247 | - cl.new_block(package=package, version=base_branch.deb_version, |
248 | - distributions=distribution, urgency="low", |
249 | - changes=['', ' * Auto build.', ''], |
250 | - author=author, date=date) |
251 | - cl_f = open(cl_path, 'wb') |
252 | - try: |
253 | - cl.write_to_open_file(cl_f) |
254 | - finally: |
255 | - cl_f.close() |
256 | - |
257 | - |
258 | -def build_source_package(basedir): |
259 | - trace.note("Building the source package") |
260 | - command = ["/usr/bin/debuild", "--no-tgz-check", "-i", "-I", "-S", |
261 | - "-uc", "-us"] |
262 | - proc = subprocess.Popen(command, cwd=basedir, |
263 | - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, |
264 | - stdin=subprocess.PIPE) |
265 | - proc.stdin.close() |
266 | - retcode = proc.wait() |
267 | - if retcode != 0: |
268 | - output = proc.stdout.read() |
269 | - raise errors.BzrCommandError("Failed to build the source package: " |
270 | - "%s" % output) |
271 | - |
272 | - |
273 | -def sign_source_package(basedir, key_id): |
274 | - trace.note("Signing the source package") |
275 | - command = ["/usr/bin/debsign", "-S", "-k%s" % key_id] |
276 | - proc = subprocess.Popen(command, cwd=basedir, |
277 | - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, |
278 | - stdin=subprocess.PIPE) |
279 | - proc.stdin.close() |
280 | - retcode = proc.wait() |
281 | - if retcode != 0: |
282 | - output = proc.stdout.read() |
283 | - raise errors.BzrCommandError("Signing the package failed: " |
284 | - "%s" % output) |
285 | - |
286 | - |
287 | -def dput_source_package(basedir, target): |
288 | - trace.note("Uploading the source package") |
289 | - command = ["/usr/bin/debrelease", "-S", "--dput", target] |
290 | - proc = subprocess.Popen(command, cwd=basedir, |
291 | - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, |
292 | - stdin=subprocess.PIPE) |
293 | - proc.stdin.close() |
294 | - retcode = proc.wait() |
295 | - if retcode != 0: |
296 | - output = proc.stdout.read() |
297 | - raise errors.BzrCommandError("Uploading the package failed: " |
298 | - "%s" % output) |
299 | - |
300 | - |
301 | -class cmd_build(Command): |
302 | - """Build a tree based on a 'recipe'. |
303 | - |
304 | - Pass the name of the recipe file and the directory to work in. |
305 | - |
306 | - See "bzr help builder" for more information on what a recipe is. |
307 | - """ |
308 | - takes_args = ["recipe_file", "working_directory"] |
309 | - takes_options = [ |
310 | - Option('manifest', type=str, argname="path", |
311 | - help="Path to write the manifest to."), |
312 | - Option('if-changed-from', type=str, argname="path", |
313 | - help="Only build if the outcome would be different " |
314 | - "to that specified in the specified manifest."), |
315 | - ] |
316 | - |
317 | - def _substitute_stuff(self, recipe_file, if_changed_from): |
318 | - """Common code to substitute stuff |
319 | - |
320 | - :return: A tuple with (retcode, base_branch). If retcode is None |
321 | - then the command execution should continue. |
322 | - """ |
323 | - base_branch = get_branch_from_recipe_file(recipe_file) |
324 | - time = datetime.datetime.utcnow() |
325 | - base_branch.substitute_time(time) |
326 | - old_recipe = None |
327 | - if if_changed_from is not None: |
328 | - old_recipe = get_old_recipe(if_changed_from) |
329 | - # Save the unsubstituted version for dailydeb. |
330 | - self._template_version = base_branch.deb_version |
331 | - changed = resolve_revisions(base_branch, if_changed_from=old_recipe) |
332 | - if not changed: |
333 | - trace.note("Unchanged") |
334 | - return 0, base_branch |
335 | - return None, base_branch |
336 | - |
337 | - def run(self, recipe_file, working_directory, manifest=None, |
338 | - if_changed_from=None): |
339 | - result, base_branch = self._substitute_stuff(recipe_file, |
340 | - if_changed_from) |
341 | - if result is not None: |
342 | - return result |
343 | - manifest_path = manifest or os.path.join(working_directory, |
344 | - "bzr-builder.manifest") |
345 | - build_tree(base_branch, working_directory) |
346 | - write_manifest_to_path(manifest_path, base_branch) |
347 | - |
348 | - |
349 | -register_command(cmd_build) |
350 | - |
351 | - |
352 | -class cmd_dailydeb(cmd_build): |
353 | - """Build a deb based on a 'recipe'. |
354 | - |
355 | - See "bzr help builder" for more information on what a recipe is. |
356 | - |
357 | - If you do not specify a working directory then a temporary |
358 | - directory will be used and it will be removed when the command |
359 | - finishes. |
360 | - """ |
361 | - |
362 | - takes_options = cmd_build.takes_options + [ |
363 | - Option("package", type=str, |
364 | - help="The package name to use in the changelog entry. " |
365 | - "If not specified then the package from the " |
366 | - "previous changelog entry will be used, so it " |
367 | - "must be specified if there is no changelog."), |
368 | - Option("distribution", type=str, |
369 | - help="The distribution to target. If not specified " |
370 | - "then the same distribution as the last entry " |
371 | - "in debian/changelog will be used."), |
372 | - Option("dput", type=str, argname="target", |
373 | - help="dput the built package to the specified " |
374 | - "dput target."), |
375 | - Option("key-id", type=str, short_name="k", |
376 | - help="Sign the packages with the specified GnuPG key, " |
377 | - "must be specified if you use --dput."), |
378 | - ] |
379 | - |
380 | - takes_args = ["recipe_file", "working_basedir?"] |
381 | - |
382 | - def run(self, recipe_file, working_basedir=None, manifest=None, |
383 | - if_changed_from=None, package=None, distribution=None, |
384 | - dput=None, key_id=None): |
385 | - |
386 | - if dput is not None and key_id is None: |
387 | - raise errors.BzrCommandError("You must specify --key-id if you " |
388 | - "specify --dput.") |
389 | - result, base_branch = self._substitute_stuff(recipe_file, |
390 | - if_changed_from) |
391 | - if result is not None: |
392 | - return result |
393 | - if working_basedir is None: |
394 | - temp_dir = tempfile.mkdtemp(prefix="bzr-builder-") |
395 | - working_basedir = temp_dir |
396 | - else: |
397 | - temp_dir = None |
398 | - if not os.path.exists(working_basedir): |
399 | - os.makedirs(working_basedir) |
400 | - self._calculate_package_name(recipe_file, package) |
401 | - working_directory = os.path.join(working_basedir, |
402 | - "%s-%s" % (self._package_name, self._template_version)) |
403 | - try: |
404 | - # we want to use a consistent package_dir always to support |
405 | - # updates in place, but debuild etc want PACKAGE-UPSTREAMVERSION |
406 | - # on disk, so we build_tree with the unsubstituted version number |
407 | - # and do a final rename-to step before calling into debian build |
408 | - # tools. We then rename the working dir back. |
409 | - manifest_path = os.path.join(working_directory, "debian", |
410 | - "bzr-builder.manifest") |
411 | - build_tree(base_branch, working_directory) |
412 | - write_manifest_to_path(manifest_path, base_branch) |
413 | - # Add changelog also substitutes {debupstream}. |
414 | - add_changelog_entry(base_branch, working_directory, |
415 | - distribution=distribution, package=package) |
416 | - package_dir = self._calculate_package_dir(base_branch, |
417 | - working_basedir) |
418 | - # working_directory -> package_dir: after this debian stuff works. |
419 | - os.rename(working_directory, package_dir) |
420 | - try: |
421 | - build_source_package(package_dir) |
422 | - if key_id is not None: |
423 | - sign_source_package(package_dir, key_id) |
424 | - if dput is not None: |
425 | - dput_source_package(package_dir, dput) |
426 | - finally: |
427 | - # package_dir -> working_directory |
428 | - os.rename(package_dir, working_directory) |
429 | - # Note that this may write a second manifest. |
430 | - if manifest is not None: |
431 | - write_manifest_to_path(manifest, base_branch) |
432 | - finally: |
433 | - if temp_dir is not None: |
434 | - shutil.rmtree(temp_dir) |
435 | - |
436 | - def _calculate_package_dir(self, base_branch, working_basedir): |
437 | - """Calculate the directory name that should be used while debuilding.""" |
438 | - version = base_branch.deb_version |
439 | - if "-" in version: |
440 | - version = version[:version.rindex("-")] |
441 | - package_basedir = "%s-%s" % (self._package_name, version) |
442 | - package_dir = os.path.join(working_basedir, package_basedir) |
443 | - return package_dir |
444 | - |
445 | - def _calculate_package_name(self, recipe_file, package): |
446 | - """Calculate the directory name that should be used while debuilding.""" |
447 | - recipe_name = os.path.basename(recipe_file) |
448 | - if recipe_name.endswith(".recipe"): |
449 | - recipe_name = recipe_name[:-len(".recipe")] |
450 | - self._package_name = package or recipe_name |
451 | - |
452 | - |
453 | -register_command(cmd_dailydeb) |
454 | + |
455 | +from bzrlib.commands import plugin_cmds |
456 | +plugin_cmds.register_lazy("cmd_build", [], "bzrlib.plugins.builder.cmds") |
457 | +plugin_cmds.register_lazy("cmd_dailydeb", [], "bzrlib.plugins.builder.cmds") |
458 | |
459 | |
460 | def test_suite(): |
461 | |
462 | === added file 'cmds.py' |
463 | --- cmds.py 1970-01-01 00:00:00 +0000 |
464 | +++ cmds.py 2010-08-13 01:14:42 +0000 |
465 | @@ -0,0 +1,492 @@ |
466 | +# bzr-builder: a bzr plugin to constuct trees based on recipes |
467 | +# Copyright 2009 Canonical Ltd. |
468 | + |
469 | +# This program is free software: you can redistribute it and/or modify it |
470 | +# under the terms of the GNU General Public License version 3, as published |
471 | +# by the Free Software Foundation. |
472 | + |
473 | +# This program is distributed in the hope that it will be useful, but |
474 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
475 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
476 | +# PURPOSE. See the GNU General Public License for more details. |
477 | + |
478 | +# You should have received a copy of the GNU General Public License along |
479 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
480 | + |
481 | +"""Subcommands provided by bzr-builder.""" |
482 | + |
483 | +import datetime |
484 | +from email import utils |
485 | +import errno |
486 | +import os |
487 | +import pwd |
488 | +import re |
489 | +import socket |
490 | +import shutil |
491 | +import subprocess |
492 | +import tempfile |
493 | + |
494 | +try: |
495 | + from debian import changelog |
496 | +except ImportError: |
497 | + # In older versions of python-debian the main package was named |
498 | + # debian_bundle |
499 | + from debian_bundle import changelog |
500 | + |
501 | +from bzrlib import ( |
502 | + errors, |
503 | + trace, |
504 | + transport, |
505 | + ) |
506 | +from bzrlib.commands import Command |
507 | +from bzrlib.option import Option |
508 | + |
509 | +from bzrlib.plugins.builder.recipe import ( |
510 | + build_tree, |
511 | + DEBUPSTREAM_VAR, |
512 | + RecipeParser, |
513 | + resolve_revisions, |
514 | + ) |
515 | + |
516 | + |
517 | +# The default distribution used by add_changelog_entry() |
518 | +DEFAULT_UBUNTU_DISTRIBUTION = "lucid" |
519 | + |
520 | + |
521 | +class MissingDependency(errors.BzrError): |
522 | + pass |
523 | + |
524 | + |
525 | +def write_manifest_to_path(path, base_branch): |
526 | + parent_dir = os.path.dirname(path) |
527 | + if parent_dir != '' and not os.path.exists(parent_dir): |
528 | + os.makedirs(parent_dir) |
529 | + manifest_f = open(path, 'wb') |
530 | + try: |
531 | + manifest_f.write(str(base_branch)) |
532 | + finally: |
533 | + manifest_f.close() |
534 | + |
535 | + |
536 | +def get_branch_from_recipe_file(recipe_file): |
537 | + recipe_transport = transport.get_transport(os.path.dirname(recipe_file)) |
538 | + try: |
539 | + recipe_contents = recipe_transport.get_bytes( |
540 | + os.path.basename(recipe_file)) |
541 | + except errors.NoSuchFile: |
542 | + raise errors.BzrCommandError("Specified recipe does not exist: " |
543 | + "%s" % recipe_file) |
544 | + parser = RecipeParser(recipe_contents, filename=recipe_file) |
545 | + return parser.parse() |
546 | + |
547 | + |
548 | +def get_old_recipe(if_changed_from): |
549 | + old_manifest_transport = transport.get_transport(os.path.dirname( |
550 | + if_changed_from)) |
551 | + try: |
552 | + old_manifest_contents = old_manifest_transport.get_bytes( |
553 | + os.path.basename(if_changed_from)) |
554 | + except errors.NoSuchFile: |
555 | + return None |
556 | + old_recipe = RecipeParser(old_manifest_contents, |
557 | + filename=if_changed_from).parse() |
558 | + return old_recipe |
559 | + |
560 | + |
561 | +def get_maintainer(): |
562 | + """Create maintainer string using the same algorithm as in dch. |
563 | + """ |
564 | + env = os.environ |
565 | + regex = re.compile(r"^(.*)\s+<(.*)>$") |
566 | + |
567 | + # Split email and name |
568 | + if 'DEBEMAIL' in env: |
569 | + match_obj = regex.match(env['DEBEMAIL']) |
570 | + if match_obj: |
571 | + if not 'DEBFULLNAME' in env: |
572 | + env['DEBFULLNAME'] = match_obj.group(1) |
573 | + env['DEBEMAIL'] = match_obj.group(2) |
574 | + if 'DEBEMAIL' not in env or 'DEBFULLNAME' not in env: |
575 | + if 'EMAIL' in env: |
576 | + match_obj = regex.match(env['EMAIL']) |
577 | + if match_obj: |
578 | + if not 'DEBFULLNAME' in env: |
579 | + env['DEBFULLNAME'] = match_obj.group(1) |
580 | + env['EMAIL'] = match_obj.group(2) |
581 | + |
582 | + # Get maintainer's name |
583 | + if 'DEBFULLNAME' in env: |
584 | + maintainer = env['DEBFULLNAME'] |
585 | + elif 'NAME' in env: |
586 | + maintainer = env['NAME'] |
587 | + else: |
588 | + # Use password database if no data in environment variables |
589 | + try: |
590 | + maintainer = re.sub(r',.*', '', pwd.getpwuid(os.getuid()).pw_gecos) |
591 | + except (KeyError, AttributeError): |
592 | + # TBD: Use last changelog entry value |
593 | + maintainer = "bzr-builder" |
594 | + |
595 | + # Get maintainer's mail address |
596 | + if 'DEBEMAIL' in env: |
597 | + email = env['DEBEMAIL'] |
598 | + elif 'EMAIL' in env: |
599 | + email = env['EMAIL'] |
600 | + else: |
601 | + addr = None |
602 | + if os.path.exists('/etc/mailname'): |
603 | + f = open('/etc/mailname') |
604 | + try: |
605 | + addr = f.readline().strip() |
606 | + finally: |
607 | + f.close() |
608 | + if not addr: |
609 | + addr = socket.getfqdn() |
610 | + if addr: |
611 | + user = pwd.getpwuid(os.getuid()).pw_name |
612 | + if not user: |
613 | + addr = None |
614 | + else: |
615 | + addr = "%s@%s" % (user, addr) |
616 | + |
617 | + if addr: |
618 | + email = addr |
619 | + else: |
620 | + # TBD: Use last changelog entry value |
621 | + email = "none@example.org" |
622 | + |
623 | + return (maintainer, email) |
624 | + |
625 | + |
626 | +def add_changelog_entry(base_branch, basedir, distribution=None, |
627 | + package=None, author_name=None, author_email=None, |
628 | + append_version=None): |
629 | + debian_dir = os.path.join(basedir, "debian") |
630 | + if not os.path.exists(debian_dir): |
631 | + os.makedirs(debian_dir) |
632 | + cl_path = os.path.join(debian_dir, "changelog") |
633 | + file_found = False |
634 | + if os.path.exists(cl_path): |
635 | + file_found = True |
636 | + cl_f = open(cl_path) |
637 | + try: |
638 | + contents = cl_f.read() |
639 | + finally: |
640 | + cl_f.close() |
641 | + cl = changelog.Changelog(file=contents) |
642 | + else: |
643 | + cl = changelog.Changelog() |
644 | + if len(cl._blocks) > 0: |
645 | + if distribution is None: |
646 | + distribution = cl._blocks[0].distributions.split()[0] |
647 | + if package is None: |
648 | + package = cl._blocks[0].package |
649 | + if DEBUPSTREAM_VAR in base_branch.deb_version: |
650 | + cl_version = cl._blocks[0].version |
651 | + base_branch.substitute_debupstream(cl_version) |
652 | + else: |
653 | + if file_found: |
654 | + if len(contents.strip()) > 0: |
655 | + reason = ("debian/changelog didn't contain any " |
656 | + "parseable stanzas") |
657 | + else: |
658 | + reason = "debian/changelog was empty" |
659 | + else: |
660 | + reason = "debian/changelog was not present" |
661 | + if package is None: |
662 | + raise errors.BzrCommandError("No previous changelog to " |
663 | + "take the package name from, and --package not " |
664 | + "specified: %s." % reason) |
665 | + if DEBUPSTREAM_VAR in base_branch.deb_version: |
666 | + raise errors.BzrCommandError("No previous changelog to " |
667 | + "take the upstream version from as %s was " |
668 | + "used: %s." % (DEBUPSTREAM_VAR, reason)) |
669 | + if distribution is None: |
670 | + distribution = DEFAULT_UBUNTU_DISTRIBUTION |
671 | + # Use debian packaging environment variables |
672 | + # or default values if they don't exist |
673 | + if author_name is None or author_email is None: |
674 | + author_name, author_email = get_maintainer() |
675 | + author = "%s <%s>" % (author_name, author_email) |
676 | + |
677 | + date = utils.formatdate(localtime=True) |
678 | + version = base_branch.deb_version |
679 | + if append_version is not None: |
680 | + version += append_version |
681 | + try: |
682 | + changelog.Version(version) |
683 | + except (changelog.VersionError, ValueError), e: |
684 | + raise errors.BzrCommandError("Invalid deb-version: %s: %s" |
685 | + % (version, e)) |
686 | + cl.new_block(package=package, version=version, |
687 | + distributions=distribution, urgency="low", |
688 | + changes=['', ' * Auto build.', ''], |
689 | + author=author, date=date) |
690 | + cl_f = open(cl_path, 'wb') |
691 | + try: |
692 | + cl.write_to_open_file(cl_f) |
693 | + finally: |
694 | + cl_f.close() |
695 | + |
696 | + |
697 | +def calculate_package_dir(base_branch, package_name, working_basedir): |
698 | + """Calculate the directory name that should be used while debuilding.""" |
699 | + version = base_branch.deb_version |
700 | + if "-" in version: |
701 | + version = version[:version.rindex("-")] |
702 | + package_basedir = "%s-%s" % (package_name, version) |
703 | + package_dir = os.path.join(working_basedir, package_basedir) |
704 | + return package_dir |
705 | + |
706 | + |
707 | +def _run_command(command, basedir, msg, error_msg, |
708 | + not_installed_msg=None): |
709 | + """ Run a command in a subprocess. |
710 | + |
711 | + :param command: list with command and parameters |
712 | + :param msg: message to display to the user |
713 | + :param error_msg: message to display if something fails. |
714 | + :param not_installed_msg: the message to display if the command |
715 | + isn't available. |
716 | + """ |
717 | + trace.note(msg) |
718 | + # Hide output if -q is in use. |
719 | + quiet = trace.is_quiet() |
720 | + if quiet: |
721 | + kwargs = {"stderr": subprocess.STDOUT, "stdout": subprocess.PIPE} |
722 | + else: |
723 | + kwargs = {} |
724 | + try: |
725 | + proc = subprocess.Popen(command, cwd=basedir, |
726 | + stdin=subprocess.PIPE, **kwargs) |
727 | + except OSError, e: |
728 | + if e.errno != errno.ENOENT: |
729 | + raise |
730 | + if not_installed_msg is None: |
731 | + raise |
732 | + raise MissingDependency(msg=not_installed_msg) |
733 | + output = proc.communicate() |
734 | + if proc.returncode != 0: |
735 | + if quiet: |
736 | + raise errors.BzrCommandError("%s: %s" % (error_msg, output)) |
737 | + else: |
738 | + raise errors.BzrCommandError(error_msg) |
739 | + |
740 | + |
741 | +def build_source_package(basedir): |
742 | + command = ["/usr/bin/debuild", "--no-tgz-check", "-i", "-I", "-S", |
743 | + "-uc", "-us"] |
744 | + _run_command(command, basedir, |
745 | + "Building the source package", |
746 | + "Failed to build the source package", |
747 | + not_installed_msg="debuild is not installed, please install " |
748 | + "the devscripts package.") |
749 | + |
750 | + |
751 | +def sign_source_package(basedir, key_id): |
752 | + command = ["/usr/bin/debsign", "-S", "-k%s" % key_id] |
753 | + _run_command(command, basedir, |
754 | + "Signing the source package", |
755 | + "Signing the package failed", |
756 | + not_installed_msg="debsign is not installed, please install " |
757 | + "the devscripts package.") |
758 | + |
759 | + |
760 | +def dput_source_package(basedir, target): |
761 | + command = ["/usr/bin/debrelease", "-S", "--dput", target] |
762 | + _run_command(command, basedir, |
763 | + "Uploading the source package", |
764 | + "Uploading the package failed", |
765 | + not_installed_msg="debrelease is not installed, please " |
766 | + "install the devscripts package.") |
767 | + |
768 | + |
769 | +class cmd_build(Command): |
770 | + """Build a tree based on a 'recipe'. |
771 | + |
772 | + Pass the name of the recipe file and the directory to work in. |
773 | + |
774 | + See "bzr help builder" for more information on what a recipe is. |
775 | + """ |
776 | + takes_args = ["recipe_file", "working_directory"] |
777 | + takes_options = [ |
778 | + Option('manifest', type=str, argname="path", |
779 | + help="Path to write the manifest to."), |
780 | + Option('if-changed-from', type=str, argname="path", |
781 | + help="Only build if the outcome would be different " |
782 | + "to that specified in the specified manifest."), |
783 | + ] |
784 | + |
785 | + def _get_prepared_branch_from_recipe(self, recipe_file, |
786 | + if_changed_from=None): |
787 | + """Common code to prepare a branch and do substitutions. |
788 | + |
789 | + :param recipe_file: a path to a recipe file to work from. |
790 | + :param if_changed_from: an optional path to a manifest to |
791 | + compare the recipe against. |
792 | + :return: A tuple with (retcode, base_branch). If retcode is None |
793 | + then the command execution should continue. |
794 | + """ |
795 | + base_branch = get_branch_from_recipe_file(recipe_file) |
796 | + time = datetime.datetime.utcnow() |
797 | + base_branch.substitute_time(time) |
798 | + old_recipe = None |
799 | + if if_changed_from is not None: |
800 | + old_recipe = get_old_recipe(if_changed_from) |
801 | + # Save the unsubstituted version for dailydeb. |
802 | + self._template_version = base_branch.deb_version |
803 | + changed = resolve_revisions(base_branch, if_changed_from=old_recipe) |
804 | + if not changed: |
805 | + trace.note("Unchanged") |
806 | + return 0, base_branch |
807 | + return None, base_branch |
808 | + |
809 | + def run(self, recipe_file, working_directory, manifest=None, |
810 | + if_changed_from=None): |
811 | + result, base_branch = self._get_prepared_branch_from_recipe(recipe_file, |
812 | + if_changed_from=if_changed_from) |
813 | + if result is not None: |
814 | + return result |
815 | + manifest_path = manifest or os.path.join(working_directory, |
816 | + "bzr-builder.manifest") |
817 | + build_tree(base_branch, working_directory) |
818 | + write_manifest_to_path(manifest_path, base_branch) |
819 | + |
820 | + |
821 | +class cmd_dailydeb(cmd_build): |
822 | + """Build a deb based on a 'recipe'. |
823 | + |
824 | + See "bzr help builder" for more information on what a recipe is. |
825 | + |
826 | + If you do not specify a working directory then a temporary |
827 | + directory will be used and it will be removed when the command |
828 | + finishes. |
829 | + """ |
830 | + |
831 | + takes_options = cmd_build.takes_options + [ |
832 | + Option("package", type=str, |
833 | + help="The package name to use in the changelog entry. " |
834 | + "If not specified then the package from the " |
835 | + "previous changelog entry will be used, so it " |
836 | + "must be specified if there is no changelog."), |
837 | + Option("distribution", type=str, |
838 | + help="The distribution to target. If not specified " |
839 | + "then the same distribution as the last entry " |
840 | + "in debian/changelog will be used."), |
841 | + Option("dput", type=str, argname="target", |
842 | + help="dput the built package to the specified " |
843 | + "dput target."), |
844 | + Option("key-id", type=str, short_name="k", |
845 | + help="Sign the packages with the specified GnuPG key. " |
846 | + "Must be specified if you use --dput."), |
847 | + Option("no-build", |
848 | + help="Just ready the source package and don't " |
849 | + "actually build it."), |
850 | + Option("watch-ppa", help="Watch the PPA the package was " |
851 | + "dput to and exit with 0 only if it builds and " |
852 | + "publishes successfully."), |
853 | + Option("append-version", type=str, help="Append the " |
854 | + "specified string to the end of the version used " |
855 | + "in debian/changelog."), |
856 | + ] |
857 | + |
858 | + takes_args = ["recipe_file", "working_basedir?"] |
859 | + |
860 | + def run(self, recipe_file, working_basedir=None, manifest=None, |
861 | + if_changed_from=None, package=None, distribution=None, |
862 | + dput=None, key_id=None, no_build=None, watch_ppa=False, |
863 | + append_version=None): |
864 | + |
865 | + if dput is not None and key_id is None: |
866 | + raise errors.BzrCommandError("You must specify --key-id if you " |
867 | + "specify --dput.") |
868 | + if watch_ppa: |
869 | + if not dput: |
870 | + raise errors.BzrCommandError( |
871 | + "cannot watch a ppa without doing dput.") |
872 | + else: |
873 | + # Check we can calculate a PPA url. |
874 | + target_from_dput(dput) |
875 | + |
876 | + result, base_branch = self._get_prepared_branch_from_recipe(recipe_file, |
877 | + if_changed_from=if_changed_from) |
878 | + if result is not None: |
879 | + return result |
880 | + if working_basedir is None: |
881 | + temp_dir = tempfile.mkdtemp(prefix="bzr-builder-") |
882 | + working_basedir = temp_dir |
883 | + else: |
884 | + temp_dir = None |
885 | + if not os.path.exists(working_basedir): |
886 | + os.makedirs(working_basedir) |
887 | + package_name = self._calculate_package_name(recipe_file, package) |
888 | + working_directory = os.path.join(working_basedir, |
889 | + "%s-%s" % (package_name, self._template_version)) |
890 | + try: |
891 | + # we want to use a consistent package_dir always to support |
892 | + # updates in place, but debuild etc want PACKAGE-UPSTREAMVERSION |
893 | + # on disk, so we build_tree with the unsubstituted version number |
894 | + # and do a final rename-to step before calling into debian build |
895 | + # tools. We then rename the working dir back. |
896 | + manifest_path = os.path.join(working_directory, "debian", |
897 | + "bzr-builder.manifest") |
898 | + build_tree(base_branch, working_directory) |
899 | + write_manifest_to_path(manifest_path, base_branch) |
900 | + # Add changelog also substitutes {debupstream}. |
901 | + add_changelog_entry(base_branch, working_directory, |
902 | + distribution=distribution, package=package, |
903 | + append_version=append_version) |
904 | + package_dir = calculate_package_dir(base_branch, |
905 | + package_name, working_basedir) |
906 | + # working_directory -> package_dir: after this debian stuff works. |
907 | + os.rename(working_directory, package_dir) |
908 | + if no_build: |
909 | + if manifest is not None: |
910 | + write_manifest_to_path(manifest, base_branch) |
911 | + return 0 |
912 | + try: |
913 | + build_source_package(package_dir) |
914 | + if key_id is not None: |
915 | + sign_source_package(package_dir, key_id) |
916 | + if dput is not None: |
917 | + dput_source_package(package_dir, dput) |
918 | + finally: |
919 | + # package_dir -> working_directory |
920 | + # FIXME: may fail in error unwind, masking the original exception. |
921 | + os.rename(package_dir, working_directory) |
922 | + # Note that this may write a second manifest. |
923 | + if manifest is not None: |
924 | + write_manifest_to_path(manifest, base_branch) |
925 | + finally: |
926 | + if temp_dir is not None: |
927 | + shutil.rmtree(temp_dir) |
928 | + if watch_ppa: |
929 | + from bzrlib.plugins.builder.ppa import watch |
930 | + target = target_from_dput(dput) |
931 | + if not watch(target, self.package, base_branch.deb_version): |
932 | + return 2 |
933 | + |
934 | + def _calculate_package_name(self, recipe_file, package): |
935 | + """Calculate the directory name that should be used while debuilding.""" |
936 | + recipe_name = os.path.basename(recipe_file) |
937 | + if recipe_name.endswith(".recipe"): |
938 | + recipe_name = recipe_name[:-len(".recipe")] |
939 | + return package or recipe_name |
940 | + |
941 | + |
942 | +def target_from_dput(dput): |
943 | + """Convert a dput specification to a LP API specification. |
944 | + |
945 | + :param dput: A dput command spec like ppa:team-name. |
946 | + :return: A LP API target like team-name/ppa. |
947 | + """ |
948 | + ppa_prefix = 'ppa:' |
949 | + if not dput.startswith(ppa_prefix): |
950 | + raise errors.BzrCommandError('%r does not appear to be a PPA. ' |
951 | + 'A dput target like \'%suser[/name]\' must be used.' |
952 | + % (dput, ppa_prefix)) |
953 | + base, _, suffix = dput[len(ppa_prefix):].partition('/') |
954 | + if not suffix: |
955 | + suffix = 'ppa' |
956 | + return base + '/' + suffix |
957 | + |
958 | |
959 | === added file 'ppa.py' |
960 | --- ppa.py 1970-01-01 00:00:00 +0000 |
961 | +++ ppa.py 2010-08-13 01:14:42 +0000 |
962 | @@ -0,0 +1,124 @@ |
963 | +# ppa support for bzr builder. |
964 | +# |
965 | +# Copyright: Canonical Ltd. (C) 2009 |
966 | +# |
967 | +# This program is free software: you can redistribute it and/or modify it |
968 | +# under the terms of the GNU General Public License version 3, as published |
969 | +# by the Free Software Foundation. |
970 | + |
971 | +# This program is distributed in the hope that it will be useful, but |
972 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
973 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
974 | +# PURPOSE. See the GNU General Public License for more details. |
975 | + |
976 | +# You should have received a copy of the GNU General Public License along |
977 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
978 | + |
979 | +import os |
980 | +import time |
981 | + |
982 | + |
983 | +from launchpadlib.launchpad import ( |
984 | + Launchpad, |
985 | + EDGE_SERVICE_ROOT, |
986 | + ) |
987 | +from launchpadlib.credentials import Credentials |
988 | + |
989 | +from bzrlib import ( |
990 | + errors, |
991 | + trace, |
992 | + ) |
993 | + |
994 | + |
995 | +def get_lp(): |
996 | + credentials = Credentials() |
997 | + oauth_file = os.path.expanduser('~/.cache/launchpadlib/bzr-builder') |
998 | + if os.path.exists(oauth_file): |
999 | + f = open(oauth_file) |
1000 | + try: |
1001 | + credentials.load(f) |
1002 | + finally: |
1003 | + f.close() |
1004 | + launchpad = Launchpad(credentials, EDGE_SERVICE_ROOT) |
1005 | + else: |
1006 | + launchpad = Launchpad.get_token_and_login('bzr-builder', |
1007 | + EDGE_SERVICE_ROOT) |
1008 | + f = open(oauth_file, 'wb') |
1009 | + try: |
1010 | + launchpad.credentials.save(f) |
1011 | + finally: |
1012 | + f.close() |
1013 | + return launchpad |
1014 | + |
1015 | + |
1016 | +def watch(owner_name, archive_name, package_name, version): |
1017 | + """Watch a package build. |
1018 | + |
1019 | + :return: True once the package built and published, or False if it fails |
1020 | + or there is a timeout waiting. |
1021 | + """ |
1022 | + version = str(version) |
1023 | + trace.note("Logging into Launchpad") |
1024 | + |
1025 | + launchpad = get_lp() |
1026 | + owner = launchpad.people[owner_name] |
1027 | + archive = owner.getPPAByName(name=archive_name) |
1028 | + end_states = ['FAILEDTOBUILD', 'FULLYBUILT'] |
1029 | + important_arches = ['amd64', 'i386', 'armel'] |
1030 | + trace.note("Waiting for version %s of %s to build." % (version, package_name)) |
1031 | + start = time.time() |
1032 | + while True: |
1033 | + sourceRecords = list(archive.getPublishedSources( |
1034 | + source_name=package_name, version=version)) |
1035 | + if not sourceRecords: |
1036 | + if time.time() - 900 > start: |
1037 | + # Over 15 minutes and no source yet, upload FAIL. |
1038 | + raise errors.BzrCommandError("No source record in %s/%s for " |
1039 | + "package %s=%s after 15 minutes." % (owner_name, |
1040 | + archive_name, package_name, version)) |
1041 | + return False |
1042 | + trace.note("Source not available yet - waiting.") |
1043 | + time.sleep(60) |
1044 | + continue |
1045 | + pkg = sourceRecords[0] |
1046 | + if pkg.status.lower() not in ('published', 'pending'): |
1047 | + trace.note("Package status: %s" % (pkg.status,)) |
1048 | + time.sleep(60) |
1049 | + continue |
1050 | + # FIXME: LP should export this as an attribute. |
1051 | + source_id = pkg.self_link.rsplit('/', 1)[1] |
1052 | + buildSummaries = archive.getBuildSummariesForSourceIds( |
1053 | + source_ids=[source_id])[source_id] |
1054 | + if buildSummaries['status'] in end_states: |
1055 | + break |
1056 | + if buildSummaries['status'] == 'NEEDSBUILD': |
1057 | + # We ignore non-virtual PPA architectures that are sparsely |
1058 | + # supplied with buildds. |
1059 | + missing = [] |
1060 | + for build in buildSummaries['builds']: |
1061 | + arch = build['arch_tag'] |
1062 | + if arch in important_arches: |
1063 | + missing.append(arch) |
1064 | + if not missing: |
1065 | + break |
1066 | + extra = ' on ' + ', '.join(missing) |
1067 | + else: |
1068 | + extra = '' |
1069 | + trace.note("%s is still in %s%s" % (pkg.display_name, |
1070 | + buildSummaries['status'], extra)) |
1071 | + time.sleep(60) |
1072 | + trace.note("%s is now %s" % (pkg.display_name, buildSummaries['status'])) |
1073 | + result = True |
1074 | + if pkg.status.lower() != 'published': |
1075 | + result = False # should this perhaps keep waiting? |
1076 | + if buildSummaries['status'] != 'FULLYBUILT': |
1077 | + if buildSummaries['status'] == 'NEEDSBUILD': |
1078 | + # We're stopping early cause the important_arches are built. |
1079 | + builds = pkg.getBuilds() |
1080 | + for build in builds: |
1081 | + if build.arch_tag in important_arches: |
1082 | + if build.buildstate != 'Successfully built': |
1083 | + result = False |
1084 | + else: |
1085 | + result = False |
1086 | + return result |
1087 | |
1088 | === modified file 'recipe.py' |
1089 | --- recipe.py 2010-08-13 01:14:42 +0000 |
1090 | +++ recipe.py 2010-08-13 01:14:42 +0000 |
1091 | @@ -22,22 +22,53 @@ |
1092 | bzrdir, |
1093 | errors, |
1094 | merge, |
1095 | + revision, |
1096 | revisionspec, |
1097 | tag, |
1098 | + trace, |
1099 | transport, |
1100 | - ui, |
1101 | urlutils, |
1102 | ) |
1103 | |
1104 | |
1105 | +try: |
1106 | + from debian import changelog |
1107 | +except ImportError: |
1108 | + from debian_bundle import changelog |
1109 | + |
1110 | + |
1111 | def subprocess_setup(): |
1112 | signal.signal(signal.SIGPIPE, signal.SIG_DFL) |
1113 | |
1114 | |
1115 | MERGE_INSTRUCTION = "merge" |
1116 | +NEST_PART_INSTRUCTION = "nest-part" |
1117 | NEST_INSTRUCTION = "nest" |
1118 | RUN_INSTRUCTION = "run" |
1119 | |
1120 | +TIME_VAR = "{time}" |
1121 | +DATE_VAR = "{date}" |
1122 | +REVNO_VAR = "{revno}" |
1123 | +REVNO_PARAM_VAR = "{revno:%s}" |
1124 | +DEBUPSTREAM_VAR = "{debupstream}" |
1125 | + |
1126 | +ok_to_preserve = [DEBUPSTREAM_VAR] |
1127 | +# The variables that don't require substitution in their name |
1128 | +simple_vars = [TIME_VAR, DATE_VAR, REVNO_VAR, DEBUPSTREAM_VAR] |
1129 | + |
1130 | + |
1131 | +def check_expanded_deb_version(base_branch): |
1132 | + checked_version = base_branch.deb_version |
1133 | + for token in ok_to_preserve: |
1134 | + checked_version = checked_version.replace(token, "") |
1135 | + if "{" in checked_version: |
1136 | + available_tokens = simple_vars |
1137 | + for name in base_branch.list_branch_names(): |
1138 | + available_tokens.append(REVNO_PARAM_VAR % name) |
1139 | + raise errors.BzrCommandError("deb-version not fully " |
1140 | + "expanded: %s. Valid substitutions are: %s" |
1141 | + % (base_branch.deb_version, available_tokens)) |
1142 | + |
1143 | |
1144 | class CommandFailedError(errors.BzrError): |
1145 | |
1146 | @@ -113,10 +144,10 @@ |
1147 | # We do a "pull" |
1148 | if tree_to is not None: |
1149 | # FIXME: should these pulls overwrite? |
1150 | - result = tree_to.pull(br_from, stop_revision=revision_id, |
1151 | + tree_to.pull(br_from, stop_revision=revision_id, |
1152 | possible_transports=possible_transports) |
1153 | else: |
1154 | - result = br_to.pull(br_from, stop_revision=revision_id, |
1155 | + br_to.pull(br_from, stop_revision=revision_id, |
1156 | possible_transports=possible_transports) |
1157 | tree_to = br_to.bzrdir.create_workingtree() |
1158 | # Ugh, we have to assume that the caller replaces their reference |
1159 | @@ -146,7 +177,7 @@ |
1160 | return tree_to, br_to |
1161 | |
1162 | |
1163 | -def merge_branch(child_branch, tree_to, br_to): |
1164 | +def merge_branch(child_branch, tree_to, br_to, possible_transports=None): |
1165 | """Merge the branch specified by child_branch. |
1166 | |
1167 | :param child_branch: the RecipeBranch to retrieve the branch and revision to |
1168 | @@ -154,82 +185,129 @@ |
1169 | :param tree_to: the WorkingTree to merge in to. |
1170 | :param br_to: the Branch to merge in to. |
1171 | """ |
1172 | + if child_branch.branch is None: |
1173 | + child_branch.branch = branch.Branch.open(child_branch.url, |
1174 | + possible_transports=possible_transports) |
1175 | + child_branch.branch.lock_read() |
1176 | + try: |
1177 | + tag._merge_tags_if_possible(child_branch.branch, br_to) |
1178 | + if child_branch.revspec is not None: |
1179 | + merge_revspec = revisionspec.RevisionSpec.from_string( |
1180 | + child_branch.revspec) |
1181 | + try: |
1182 | + merge_revid = merge_revspec.as_revision_id( |
1183 | + child_branch.branch) |
1184 | + except errors.InvalidRevisionSpec, e: |
1185 | + # Give the user a hint if they didn't mean to speciy |
1186 | + # a revspec. |
1187 | + e.extra = (". Did you not mean to specify a revspec " |
1188 | + "at the end of the merge line?") |
1189 | + raise e |
1190 | + else: |
1191 | + merge_revid = child_branch.branch.last_revision() |
1192 | + child_branch.revid = merge_revid |
1193 | + merger = merge.Merger.from_revision_ids(None, tree_to, merge_revid, |
1194 | + other_branch=child_branch.branch, tree_branch=br_to) |
1195 | + merger.merge_type = merge.Merge3Merger |
1196 | + if (merger.base_rev_id == merger.other_rev_id and |
1197 | + merger.other_rev_id is not None): |
1198 | + # Nothing to do. |
1199 | + return |
1200 | + conflict_count = merger.do_merge() |
1201 | + merger.set_pending() |
1202 | + if conflict_count: |
1203 | + # FIXME: better reporting |
1204 | + raise errors.BzrCommandError("Conflicts from merge") |
1205 | + tree_to.commit("Merge %s" % |
1206 | + urlutils.unescape_for_display( |
1207 | + child_branch.url, 'utf-8')) |
1208 | + finally: |
1209 | + child_branch.branch.unlock() |
1210 | + |
1211 | + |
1212 | +def nest_part_branch(op, child_branch, tree_to, br_to, subpath, |
1213 | + target_subdir=None): |
1214 | + """Merge the branch subdirectory specified by child_branch. |
1215 | + |
1216 | + :param child_branch: the RecipeBranch to retrieve the branch and revision to |
1217 | + merge from. |
1218 | + :param tree_to: the WorkingTree to merge in to. |
1219 | + :param br_to: the Branch to merge in to. |
1220 | + :param subpath: only merge files from branch that are from this path. |
1221 | + e.g. subpath='/debian' will only merge changes from that directory. |
1222 | + :param target_subdir: (optional) directory in target to merge that |
1223 | + subpath into. Defaults to basename of subpath. |
1224 | + """ |
1225 | merge_from = branch.Branch.open(child_branch.url) |
1226 | merge_from.lock_read() |
1227 | - try: |
1228 | - pb = ui.ui_factory.nested_progress_bar() |
1229 | - try: |
1230 | - tag._merge_tags_if_possible(merge_from, br_to) |
1231 | - if child_branch.revspec is not None: |
1232 | - merge_revspec = revisionspec.RevisionSpec.from_string( |
1233 | - child_branch.revspec) |
1234 | - merge_revid = merge_revspec.as_revision_id(merge_from) |
1235 | - else: |
1236 | - merge_revid = merge_from.last_revision() |
1237 | - child_branch.revid = merge_revid |
1238 | - merger = merge.Merger.from_revision_ids(pb, tree_to, merge_revid, |
1239 | - other_branch=merge_from, tree_branch=br_to) |
1240 | - merger.merge_type = merge.Merge3Merger |
1241 | - if (merger.base_rev_id == merger.other_rev_id and |
1242 | - merger.other_rev_id is not None): |
1243 | - # Nothing to do. |
1244 | - return |
1245 | - conflict_count = merger.do_merge() |
1246 | - merger.set_pending() |
1247 | - if conflict_count: |
1248 | - # FIXME: better reporting |
1249 | - raise errors.BzrCommandError("Conflicts from merge") |
1250 | - tree_to.commit("Merge %s" % |
1251 | - urlutils.unescape_for_display( |
1252 | - child_branch.url, 'utf-8')) |
1253 | - finally: |
1254 | - pb.finished() |
1255 | - finally: |
1256 | - merge_from.unlock() |
1257 | - |
1258 | - |
1259 | -def update_branch(base_branch, tree_to, br_to, to_transport): |
1260 | - from_location = base_branch.url |
1261 | - accelerator_tree, br_from = bzrdir.BzrDir.open_tree_or_branch( |
1262 | - from_location) |
1263 | - br_from.lock_read() |
1264 | + op.add_cleanup(merge_from.unlock) |
1265 | + if child_branch.revspec is not None: |
1266 | + merge_revspec = revisionspec.RevisionSpec.from_string( |
1267 | + child_branch.revspec) |
1268 | + merge_revid = merge_revspec.as_revision_id(merge_from) |
1269 | + else: |
1270 | + merge_revid = merge_from.last_revision() |
1271 | + child_branch.revid = merge_revid |
1272 | + other_tree = merge_from.basis_tree() |
1273 | + other_tree.lock_read() |
1274 | + op.add_cleanup(other_tree.unlock) |
1275 | + if target_subdir is None: |
1276 | + target_subdir = os.path.basename(subpath) |
1277 | + merger = merge.MergeIntoMerger(this_tree=tree_to, other_tree=other_tree, |
1278 | + other_branch=merge_from, target_subdir=target_subdir, |
1279 | + source_subpath=subpath, other_rev_id=merge_revid) |
1280 | + merger.set_base_revision(revision.NULL_REVISION, merge_from) |
1281 | + conflict_count = merger.do_merge() |
1282 | + merger.set_pending() |
1283 | + if conflict_count: |
1284 | + # FIXME: better reporting |
1285 | + raise errors.BzrCommandError("Conflicts from merge") |
1286 | + tree_to.commit("Merge %s of %s" % |
1287 | + (subpath, urlutils.unescape_for_display(child_branch.url, 'utf-8'))) |
1288 | + |
1289 | + |
1290 | +def update_branch(base_branch, tree_to, br_to, to_transport, |
1291 | + possible_transports=None): |
1292 | + if base_branch.branch is None: |
1293 | + base_branch.branch = branch.Branch.open(base_branch.url, |
1294 | + possible_transports=possible_transports) |
1295 | + base_branch.branch.lock_read() |
1296 | try: |
1297 | if base_branch.revspec is not None: |
1298 | revspec = revisionspec.RevisionSpec.from_string( |
1299 | base_branch.revspec) |
1300 | - revision_id = revspec.as_revision_id(br_from) |
1301 | + revision_id = revspec.as_revision_id(base_branch.branch) |
1302 | else: |
1303 | - revision_id = br_from.last_revision() |
1304 | + revision_id = base_branch.branch.last_revision() |
1305 | base_branch.revid = revision_id |
1306 | - tree_to, br_to = pull_or_branch(tree_to, br_to, br_from, |
1307 | + tree_to, br_to = pull_or_branch(tree_to, br_to, base_branch.branch, |
1308 | to_transport, revision_id, |
1309 | - accelerator_tree=accelerator_tree, |
1310 | - possible_transports=[to_transport]) |
1311 | + possible_transports=possible_transports) |
1312 | finally: |
1313 | - br_from.unlock() |
1314 | + base_branch.branch.unlock() |
1315 | return tree_to, br_to |
1316 | |
1317 | |
1318 | def _resolve_revisions_recurse(new_branch, substitute_revno, |
1319 | if_changed_from=None): |
1320 | changed = False |
1321 | - br_from = branch.Branch.open(new_branch.url) |
1322 | - br_from.lock_read() |
1323 | + new_branch.branch = branch.Branch.open(new_branch.url) |
1324 | + new_branch.branch.lock_read() |
1325 | try: |
1326 | if new_branch.revspec is not None: |
1327 | revspec = revisionspec.RevisionSpec.from_string( |
1328 | new_branch.revspec) |
1329 | - revision_id = revspec.as_revision_id(br_from) |
1330 | + revision_id = revspec.as_revision_id(new_branch.branch) |
1331 | else: |
1332 | - revision_id = br_from.last_revision() |
1333 | + revision_id = new_branch.branch.last_revision() |
1334 | new_branch.revid = revision_id |
1335 | def get_revno(): |
1336 | try: |
1337 | - revno = br_from.revision_id_to_revno(revision_id) |
1338 | + revno = new_branch.branch.revision_id_to_revno(revision_id) |
1339 | return str(revno) |
1340 | except errors.NoSuchRevision: |
1341 | # We need to load and use the full revno map after all |
1342 | - result = br_from.get_revision_id_to_revno_map().get( |
1343 | + result = new_branch.branch.get_revision_id_to_revno_map().get( |
1344 | revision_id) |
1345 | if result is None: |
1346 | return result |
1347 | @@ -242,14 +320,13 @@ |
1348 | changed_revspec = revisionspec.RevisionSpec.from_string( |
1349 | if_changed_from.revspec) |
1350 | changed_revision_id = changed_revspec.as_revision_id( |
1351 | - br_from) |
1352 | + new_branch.branch) |
1353 | else: |
1354 | - changed_revision_id = br_from.last_revision() |
1355 | + changed_revision_id = new_branch.branch.last_revision() |
1356 | if revision_id != changed_revision_id: |
1357 | changed = True |
1358 | for index, instruction in enumerate(new_branch.child_branches): |
1359 | child_branch = instruction.recipe_branch |
1360 | - nest_location = instruction.nest_path |
1361 | if_changed_child = None |
1362 | if if_changed_from is not None: |
1363 | if_changed_child = if_changed_from.child_branches[index].recipe_branch |
1364 | @@ -261,7 +338,7 @@ |
1365 | changed = child_changed |
1366 | return changed |
1367 | finally: |
1368 | - br_from.unlock() |
1369 | + new_branch.branch.unlock() |
1370 | |
1371 | |
1372 | def resolve_revisions(base_branch, if_changed_from=None): |
1373 | @@ -289,30 +366,20 @@ |
1374 | if_changed_from=if_changed_from_revisions) |
1375 | if not changed: |
1376 | changed = changed_revisions |
1377 | - ok_to_preserve = ["{debupstream}"] |
1378 | - checked_version = base_branch.deb_version |
1379 | - for token in ok_to_preserve: |
1380 | - checked_version = checked_version.replace(token, "") |
1381 | - if "{" in checked_version: |
1382 | - raise errors.BzrCommandError("deb-version not fully " |
1383 | - "expanded: %s" % base_branch.deb_version) |
1384 | + check_expanded_deb_version(base_branch) |
1385 | if if_changed_from is not None and not changed: |
1386 | return False |
1387 | return True |
1388 | |
1389 | |
1390 | -def build_tree(base_branch, target_path): |
1391 | - """Build the RecipeBranch at a path. |
1392 | - |
1393 | - Follow the instructions embodied in RecipeBranch and build a tree |
1394 | - based on them rooted at target_path. If target_path exists and |
1395 | - is the root of the branch then the branch will be updated based on |
1396 | - what the RecipeBranch requires. |
1397 | - |
1398 | - :param base_branch: a RecipeBranch to build. |
1399 | - :param target_path: the path to the base of the desired output. |
1400 | - """ |
1401 | - to_transport = transport.get_transport(target_path) |
1402 | +def _build_inner_tree(base_branch, target_path, possible_transports=None): |
1403 | + revision_of = "" |
1404 | + if base_branch.revspec is not None: |
1405 | + revision_of = "revision '%s' of " % base_branch.revspec |
1406 | + trace.note("Retrieving %s'%s' to put at '%s'." |
1407 | + % (revision_of, base_branch.url, target_path)) |
1408 | + to_transport = transport.get_transport(target_path, |
1409 | + possible_transports=possible_transports) |
1410 | try: |
1411 | tree_to, br_to = bzrdir.BzrDir.open_tree_or_branch(target_path) |
1412 | # Should we commit any changes in the tree here? If we don't |
1413 | @@ -327,7 +394,7 @@ |
1414 | br_to.lock_write() |
1415 | try: |
1416 | tree_to, br_to = update_branch(base_branch, tree_to, br_to, |
1417 | - to_transport) |
1418 | + to_transport, possible_transports=possible_transports) |
1419 | for instruction in base_branch.child_branches: |
1420 | instruction.apply(target_path, tree_to, br_to) |
1421 | finally: |
1422 | @@ -339,49 +406,25 @@ |
1423 | tree_to.unlock() |
1424 | |
1425 | |
1426 | -def _add_child_branches_to_manifest(child_branches, indent_level): |
1427 | - manifest = "" |
1428 | - for instruction in child_branches: |
1429 | - child_branch = instruction.recipe_branch |
1430 | - nest_location = instruction.nest_path |
1431 | - if child_branch is None: |
1432 | - manifest += "%s%s %s\n" % (" " * indent_level, RUN_INSTRUCTION, |
1433 | - nest_location) |
1434 | - else: |
1435 | - assert child_branch.revid is not None, "Branch hasn't been built" |
1436 | - if nest_location is not None: |
1437 | - manifest += "%s%s %s %s %s revid:%s\n" % \ |
1438 | - (" " * indent_level, NEST_INSTRUCTION, |
1439 | - child_branch.name, |
1440 | - child_branch.url, nest_location, |
1441 | - child_branch.revid) |
1442 | - manifest += _add_child_branches_to_manifest( |
1443 | - child_branch.child_branches, indent_level+1) |
1444 | - else: |
1445 | - manifest += "%s%s %s %s revid:%s\n" % \ |
1446 | - (" " * indent_level, MERGE_INSTRUCTION, |
1447 | - child_branch.name, |
1448 | - child_branch.url, child_branch.revid) |
1449 | - return manifest |
1450 | - |
1451 | - |
1452 | -def build_manifest(base_branch): |
1453 | - manifest = "# bzr-builder format %s deb-version " % str(base_branch.format) |
1454 | - # TODO: should we store the expanded version that was used? |
1455 | - manifest += "%s\n" % (base_branch.deb_version,) |
1456 | - assert base_branch.revid is not None, "Branch hasn't been built" |
1457 | - manifest += "%s revid:%s\n" % (base_branch.url, base_branch.revid) |
1458 | - manifest += _add_child_branches_to_manifest(base_branch.child_branches, 0) |
1459 | - # Sanity check. |
1460 | - # TODO: write a function that compares the result of this parse with |
1461 | - # the branch that we built it from. |
1462 | - RecipeParser(manifest).parse() |
1463 | - return manifest |
1464 | +def build_tree(base_branch, target_path, possible_transports=None): |
1465 | + """Build the RecipeBranch at a path. |
1466 | + |
1467 | + Follow the instructions embodied in RecipeBranch and build a tree |
1468 | + based on them rooted at target_path. If target_path exists and |
1469 | + is the root of the branch then the branch will be updated based on |
1470 | + what the RecipeBranch requires. |
1471 | + |
1472 | + :param base_branch: a RecipeBranch to build. |
1473 | + :param target_path: the path to the base of the desired output. |
1474 | + """ |
1475 | + trace.note("Building tree.") |
1476 | + _build_inner_tree(base_branch, target_path, |
1477 | + possible_transports=possible_transports) |
1478 | |
1479 | |
1480 | class ChildBranch(object): |
1481 | """A child branch in a recipe. |
1482 | - |
1483 | + |
1484 | If the nest path is not None it is the path relative to the recipe branch |
1485 | where the child branch should be placed. If it is None then the child |
1486 | branch should be merged instead of nested. |
1487 | @@ -390,8 +433,8 @@ |
1488 | def __init__(self, recipe_branch, nest_path=None): |
1489 | self.recipe_branch = recipe_branch |
1490 | self.nest_path = nest_path |
1491 | - |
1492 | - def apply(self, target_path, tree_to, br_to): |
1493 | + |
1494 | + def apply(self, target_path, tree_to, br_to, possible_transports=None): |
1495 | raise NotImplementedError(self.apply) |
1496 | |
1497 | def as_tuple(self): |
1498 | @@ -400,8 +443,9 @@ |
1499 | |
1500 | class CommandInstruction(ChildBranch): |
1501 | |
1502 | - def apply(self, target_path, tree_to, br_to): |
1503 | + def apply(self, target_path, tree_to, br_to, possible_transports=None): |
1504 | # it's a command |
1505 | + trace.note("Running '%s' in '%s'." % (self.nest_path, target_path)) |
1506 | proc = subprocess.Popen(self.nest_path, cwd=target_path, |
1507 | preexec_fn=subprocess_setup, shell=True, stdin=subprocess.PIPE) |
1508 | proc.communicate() |
1509 | @@ -411,16 +455,36 @@ |
1510 | |
1511 | class MergeInstruction(ChildBranch): |
1512 | |
1513 | + def apply(self, target_path, tree_to, br_to, possible_transports=None): |
1514 | + revision_of = "" |
1515 | + if self.recipe_branch.revspec is not None: |
1516 | + revision_of = "revision '%s' of " % self.recipe_branch.revspec |
1517 | + trace.note("Merging %s'%s' in to '%s'." |
1518 | + % (revision_of, self.recipe_branch.url, target_path)) |
1519 | + merge_branch(self.recipe_branch, tree_to, br_to, |
1520 | + possible_transports=possible_transports) |
1521 | + |
1522 | + |
1523 | +class NestPartInstruction(ChildBranch): |
1524 | + |
1525 | + def __init__(self, recipe_branch, subpath, target_subdir): |
1526 | + ChildBranch.__init__(self, recipe_branch) |
1527 | + self.subpath = subpath |
1528 | + self.target_subdir = target_subdir |
1529 | + |
1530 | def apply(self, target_path, tree_to, br_to): |
1531 | - merge_branch(self.recipe_branch, tree_to, br_to) |
1532 | + from bzrlib.cleanup import OperationWithCleanups |
1533 | + op = OperationWithCleanups(nest_part_branch) |
1534 | + op.run(self.recipe_branch, tree_to, br_to, self.subpath, |
1535 | + self.target_subdir) |
1536 | |
1537 | |
1538 | class NestInstruction(ChildBranch): |
1539 | |
1540 | - def apply(self, target_path, tree_to, br_to): |
1541 | - # FIXME: pass possible_transports around |
1542 | - build_tree(self.recipe_branch, |
1543 | - target_path=os.path.join(target_path, self.nest_path)) |
1544 | + def apply(self, target_path, tree_to, br_to, possible_transports=None): |
1545 | + _build_inner_tree(self.recipe_branch, |
1546 | + target_path=os.path.join(target_path, self.nest_path), |
1547 | + possible_transports=possible_transports) |
1548 | |
1549 | |
1550 | class RecipeBranch(object): |
1551 | @@ -430,9 +494,12 @@ |
1552 | root branch), and optionally child branches that are either merged |
1553 | or nested. |
1554 | |
1555 | - The child_branches attribute is a list of tuples of ChildBranch objects. |
1556 | + The child_branches attribute is a list of tuples of ChildBranch objects. |
1557 | The revid attribute records the revid that the url and revspec resolved |
1558 | to when the RecipeBranch was built, or None if it has not been built. |
1559 | + |
1560 | + :ivar revid: after this recipe branch has been built this is set to the |
1561 | + revision ID that was merged/nested from the branch at self.url. |
1562 | """ |
1563 | |
1564 | def __init__(self, name, url, revspec=None): |
1565 | @@ -448,6 +515,7 @@ |
1566 | self.revspec = revspec |
1567 | self.child_branches = [] |
1568 | self.revid = None |
1569 | + self.branch = None |
1570 | |
1571 | def merge_branch(self, branch): |
1572 | """Merge a child branch in to this one. |
1573 | @@ -456,6 +524,18 @@ |
1574 | """ |
1575 | self.child_branches.append(MergeInstruction(branch)) |
1576 | |
1577 | + def nest_part_branch(self, branch, subpath=None, target_subdir=None): |
1578 | + """Merge subdir of a child branch into this one. |
1579 | + |
1580 | + :param branch: the RecipeBranch to merge. |
1581 | + :param subpath: only merge files from branch that are from this path. |
1582 | + e.g. subpath='/debian' will only merge changes from that directory. |
1583 | + :param target_subdir: (optional) directory in target to merge that |
1584 | + subpath into. Defaults to basename of subpath. |
1585 | + """ |
1586 | + self.child_branches.append( |
1587 | + NestPartInstruction(branch, subpath, target_subdir)) |
1588 | + |
1589 | def nest_branch(self, location, branch): |
1590 | """Nest a child branch in to this one. |
1591 | |
1592 | @@ -492,10 +572,32 @@ |
1593 | if ((child_branch is None and other_child_branch is not None) |
1594 | or (child_branch is not None and other_child_branch is None)): |
1595 | return True |
1596 | - if child_branch.different_shape_to(other_child_branch): |
1597 | + # if child_branch is None then other_child_branch must be |
1598 | + # None too, meaning that they are both run instructions, |
1599 | + # we would compare their nest locations (commands), but |
1600 | + # that has already been done, so just guard |
1601 | + if (child_branch is not None |
1602 | + and child_branch.different_shape_to(other_child_branch)): |
1603 | return True |
1604 | return False |
1605 | |
1606 | + def _list_child_names(self): |
1607 | + child_names = [] |
1608 | + for instruction in self.child_branches: |
1609 | + child_branch = instruction.recipe_branch |
1610 | + if child_branch is None: |
1611 | + continue |
1612 | + child_names += child_branch.list_branch_names() |
1613 | + return child_names |
1614 | + |
1615 | + def list_branch_names(self): |
1616 | + """List all of the branch names under this one. |
1617 | + |
1618 | + :return: a list of the branch names. |
1619 | + :rtype: list(str) |
1620 | + """ |
1621 | + return [self.name] + self._list_child_names() |
1622 | + |
1623 | |
1624 | class BaseRecipeBranch(RecipeBranch): |
1625 | """The RecipeBranch that is at the root of a recipe.""" |
1626 | @@ -521,9 +623,9 @@ |
1627 | needed. |
1628 | """ |
1629 | if branch_name is None: |
1630 | - subst_string = "{revno}" |
1631 | + subst_string = REVNO_VAR |
1632 | else: |
1633 | - subst_string = "{revno:%s}" % branch_name |
1634 | + subst_string = REVNO_PARAM_VAR % branch_name |
1635 | if subst_string in self.deb_version: |
1636 | revno = get_revno_cb() |
1637 | if revno is None: |
1638 | @@ -537,9 +639,75 @@ |
1639 | |
1640 | :param time: a datetime.datetime with the desired time. |
1641 | """ |
1642 | - if "{time}" in self.deb_version: |
1643 | - self.deb_version = self.deb_version.replace("{time}", |
1644 | + if TIME_VAR in self.deb_version: |
1645 | + self.deb_version = self.deb_version.replace(TIME_VAR, |
1646 | time.strftime("%Y%m%d%H%M")) |
1647 | + if DATE_VAR in self.deb_version: |
1648 | + self.deb_version = self.deb_version.replace(DATE_VAR, |
1649 | + time.strftime("%Y%m%d")) |
1650 | + |
1651 | + def substitute_debupstream(self, version): |
1652 | + """Substitute {debupstream} in to deb_version if needed. |
1653 | + |
1654 | + :param version: the Version object to take the upstream version |
1655 | + from. |
1656 | + """ |
1657 | + if DEBUPSTREAM_VAR in self.deb_version: |
1658 | + # Should we include the epoch? |
1659 | + self.deb_version = self.deb_version.replace(DEBUPSTREAM_VAR, |
1660 | + version.upstream_version) |
1661 | + |
1662 | + def _add_child_branches_to_manifest(self, child_branches, indent_level): |
1663 | + manifest = "" |
1664 | + for instruction in child_branches: |
1665 | + child_branch = instruction.recipe_branch |
1666 | + nest_location = instruction.nest_path |
1667 | + if child_branch is None: |
1668 | + manifest += "%s%s %s\n" % (" " * indent_level, RUN_INSTRUCTION, |
1669 | + nest_location) |
1670 | + else: |
1671 | + if child_branch.revid is not None: |
1672 | + revid_part = " revid:%s" % child_branch.revid |
1673 | + elif child_branch.revspec is not None: |
1674 | + revid_part = " %s" % child_branch.revspec |
1675 | + else: |
1676 | + revid_part = "" |
1677 | + if nest_location is not None: |
1678 | + manifest += "%s%s %s %s %s%s\n" % \ |
1679 | + (" " * indent_level, NEST_INSTRUCTION, |
1680 | + child_branch.name, |
1681 | + child_branch.url, nest_location, |
1682 | + revid_part) |
1683 | + manifest += self._add_child_branches_to_manifest( |
1684 | + child_branch.child_branches, indent_level+1) |
1685 | + else: |
1686 | + manifest += "%s%s %s %s%s\n" % \ |
1687 | + (" " * indent_level, MERGE_INSTRUCTION, |
1688 | + child_branch.name, |
1689 | + child_branch.url, revid_part) |
1690 | + return manifest |
1691 | + |
1692 | + |
1693 | + def __str__(self): |
1694 | + manifest = "# bzr-builder format %s deb-version " % str(self.format) |
1695 | + # TODO: should we store the expanded version that was used? |
1696 | + manifest += "%s\n" % (self.deb_version,) |
1697 | + if self.revid is not None: |
1698 | + manifest += "%s revid:%s\n" % (self.url, self.revid) |
1699 | + elif self.revspec is not None: |
1700 | + manifest += "%s %s\n" % (self.url, self.revspec) |
1701 | + else: |
1702 | + manifest += "%s\n" % (self.url,) |
1703 | + manifest += self._add_child_branches_to_manifest(self.child_branches, |
1704 | + 0) |
1705 | + # Sanity check. |
1706 | + # TODO: write a function that compares the result of this parse with |
1707 | + # the branch that we built it from. |
1708 | + RecipeParser(manifest).parse() |
1709 | + return manifest |
1710 | + |
1711 | + def list_branch_names(self): |
1712 | + return self._list_child_names() |
1713 | |
1714 | |
1715 | class RecipeParseError(errors.BzrError): |
1716 | @@ -550,6 +718,13 @@ |
1717 | problem=problem) |
1718 | |
1719 | |
1720 | +class ForbiddenInstructionError(RecipeParseError): |
1721 | + |
1722 | + def __init__(self, filename, line, char, problem, instruction_name=None): |
1723 | + RecipeParseError.__init__(self, filename, line, char, problem) |
1724 | + self.instruction_name = instruction_name |
1725 | + |
1726 | + |
1727 | class RecipeParser(object): |
1728 | """Parse a recipe. |
1729 | |
1730 | @@ -560,7 +735,7 @@ |
1731 | eol_char = "\n" |
1732 | digit_chars = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") |
1733 | |
1734 | - NEWEST_VERSION = 0.2 |
1735 | + NEWEST_VERSION = 0.3 |
1736 | |
1737 | def __init__(self, f, filename=None): |
1738 | """Create a RecipeParser. |
1739 | @@ -578,9 +753,12 @@ |
1740 | if filename is None: |
1741 | self.filename = "recipe" |
1742 | |
1743 | - def parse(self): |
1744 | + def parse(self, forbidden_instructions=None): |
1745 | """Parse the recipe. |
1746 | |
1747 | + :param forbidden_instructions: a list of instructions that you |
1748 | + don't want to allow. Defaults to None allowing them all. |
1749 | + :type forbidden_instructions: list(str) or None |
1750 | :return: a RecipeBranch representing the recipe. |
1751 | """ |
1752 | self.lines = self.text.split("\n") |
1753 | @@ -588,12 +766,21 @@ |
1754 | self.line_index = 0 |
1755 | self.current_line = self.lines[self.line_index] |
1756 | self.current_indent_level = 0 |
1757 | + self.seen_nicks = set() |
1758 | + self.seen_paths = {".": 1} |
1759 | (version, deb_version) = self.parse_header() |
1760 | self.version = version |
1761 | last_instruction = None |
1762 | active_branches = [] |
1763 | last_branch = None |
1764 | while self.line_index < len(self.lines): |
1765 | + if self.is_blankline(): |
1766 | + self.new_line() |
1767 | + continue |
1768 | + comment = self.parse_comment_line() |
1769 | + if comment is not None: |
1770 | + self.new_line() |
1771 | + continue |
1772 | old_indent_level = self.parse_indent() |
1773 | if old_indent_level is not None: |
1774 | if (old_indent_level < self.current_indent_level |
1775 | @@ -605,10 +792,6 @@ |
1776 | else: |
1777 | unindent = self.current_indent_level - old_indent_level |
1778 | active_branches = active_branches[:unindent] |
1779 | - comment = self.parse_comment_line() |
1780 | - if comment is not None: |
1781 | - self.new_line() |
1782 | - continue |
1783 | if last_instruction is None: |
1784 | url = self.take_to_whitespace("branch to start from") |
1785 | revspec = self.parse_optional_revspec() |
1786 | @@ -618,7 +801,8 @@ |
1787 | active_branches = [last_branch] |
1788 | last_instruction = "" |
1789 | else: |
1790 | - instruction = self.parse_instruction() |
1791 | + instruction = self.parse_instruction( |
1792 | + forbidden_instructions=forbidden_instructions) |
1793 | if instruction == RUN_INSTRUCTION: |
1794 | self.parse_whitespace("the command") |
1795 | command = self.take_to_newline().strip() |
1796 | @@ -629,13 +813,25 @@ |
1797 | url = self.parse_branch_url() |
1798 | if instruction == NEST_INSTRUCTION: |
1799 | location = self.parse_branch_location() |
1800 | - revspec = self.parse_optional_revspec() |
1801 | + if instruction == NEST_PART_INSTRUCTION: |
1802 | + path = self.parse_subpath() |
1803 | + target_subdir = self.parse_optional_path() |
1804 | + if target_subdir == '': |
1805 | + target_subdir = None |
1806 | + revspec = None |
1807 | + else: |
1808 | + revspec = self.parse_optional_revspec() |
1809 | + else: |
1810 | + revspec = self.parse_optional_revspec() |
1811 | self.new_line() |
1812 | last_branch = RecipeBranch(branch_id, url, revspec=revspec) |
1813 | if instruction == NEST_INSTRUCTION: |
1814 | active_branches[-1].nest_branch(location, last_branch) |
1815 | - else: |
1816 | + elif instruction == MERGE_INSTRUCTION: |
1817 | active_branches[-1].merge_branch(last_branch) |
1818 | + elif instruction == NEST_PART_INSTRUCTION: |
1819 | + active_branches[-1].nest_part_branch( |
1820 | + last_branch, path, target_subdir) |
1821 | last_instruction = instruction |
1822 | if len(active_branches) == 0: |
1823 | self.throw_parse_error("Empty recipe") |
1824 | @@ -655,18 +851,28 @@ |
1825 | self.new_line() |
1826 | return version, deb_version |
1827 | |
1828 | - def parse_instruction(self): |
1829 | + def parse_instruction(self, forbidden_instructions=None): |
1830 | if self.version < 0.2: |
1831 | options = (MERGE_INSTRUCTION, NEST_INSTRUCTION) |
1832 | options_str = "'%s' or '%s'" % options |
1833 | - else: |
1834 | + elif self.version < 0.3: |
1835 | options = (MERGE_INSTRUCTION, NEST_INSTRUCTION, RUN_INSTRUCTION) |
1836 | options_str = "'%s', '%s' or '%s'" % options |
1837 | + else: |
1838 | + options = (MERGE_INSTRUCTION, NEST_INSTRUCTION, |
1839 | + NEST_PART_INSTRUCTION, RUN_INSTRUCTION) |
1840 | + options_str = "'%s', '%s', '%s' or '%s'" % options |
1841 | instruction = self.peek_to_whitespace() |
1842 | if instruction is None: |
1843 | self.throw_parse_error("End of line while looking for %s" |
1844 | % options_str) |
1845 | if instruction in options: |
1846 | + if forbidden_instructions is not None: |
1847 | + if instruction in forbidden_instructions: |
1848 | + self.throw_parse_error("The '%s' instruction is " |
1849 | + "forbidden." % instruction, |
1850 | + cls=ForbiddenInstructionError, |
1851 | + instruction_name=instruction) |
1852 | self.take_chars(len(instruction)) |
1853 | return instruction |
1854 | self.throw_parse_error("Expecting %s, got '%s'" |
1855 | @@ -674,7 +880,15 @@ |
1856 | |
1857 | def parse_branch_id(self): |
1858 | self.parse_whitespace("the branch id") |
1859 | - branch_id = self.take_to_whitespace("the branch id") |
1860 | + branch_id = self.peek_to_whitespace() |
1861 | + if branch_id is None: |
1862 | + self.throw_parse_error("End of line while looking for the " |
1863 | + "branch id") |
1864 | + if branch_id in self.seen_nicks: |
1865 | + self.throw_parse_error("'%s' was already used to identify " |
1866 | + "a branch." % branch_id) |
1867 | + self.take_chars(len(branch_id)) |
1868 | + self.seen_nicks.add(branch_id) |
1869 | return branch_id |
1870 | |
1871 | def parse_branch_url(self): |
1872 | @@ -685,8 +899,34 @@ |
1873 | def parse_branch_location(self): |
1874 | # FIXME: Needs a better term |
1875 | self.parse_whitespace("the location to nest") |
1876 | - location = self.take_to_whitespace("the location to nest") |
1877 | - return location |
1878 | + location = self.peek_to_whitespace() |
1879 | + if location is None: |
1880 | + self.throw_parse_error("End of line while looking for the " |
1881 | + "location to nest") |
1882 | + norm_location = os.path.normpath(location) |
1883 | + if norm_location in self.seen_paths: |
1884 | + self.throw_parse_error("The path '%s' is a duplicate of " |
1885 | + "the one used on line %d." % (location, |
1886 | + self.seen_paths[norm_location])) |
1887 | + if os.path.isabs(norm_location): |
1888 | + self.throw_parse_error("Absolute paths are not allowed: %s" |
1889 | + % location) |
1890 | + if norm_location.startswith(".."): |
1891 | + self.throw_parse_error("Paths outside the current directory " |
1892 | + "are not allowed: %s" % location) |
1893 | + self.take_chars(len(location)) |
1894 | + self.seen_paths[norm_location] = self.line_index + 1 |
1895 | + return location |
1896 | + |
1897 | + def parse_subpath(self): |
1898 | + self.parse_whitespace("the subpath to merge") |
1899 | + location = self.take_to_whitespace("the subpath to merge") |
1900 | + return location |
1901 | + |
1902 | + def parse_revspec(self): |
1903 | + self.parse_whitespace("the revspec") |
1904 | + revspec = self.take_to_whitespace("the revspec") |
1905 | + return revspec |
1906 | |
1907 | def parse_optional_revspec(self): |
1908 | self.parse_whitespace(None, require=False) |
1909 | @@ -695,9 +935,18 @@ |
1910 | self.take_chars(len(revspec)) |
1911 | return revspec |
1912 | |
1913 | - def throw_parse_error(self, problem): |
1914 | - raise RecipeParseError(self.filename, self.line_index + 1, |
1915 | - self.index + 1, problem) |
1916 | + def parse_optional_path(self): |
1917 | + self.parse_whitespace(None, require=False) |
1918 | + path = self.peek_to_whitespace() |
1919 | + if path is not None: |
1920 | + self.take_chars(len(path)) |
1921 | + return path |
1922 | + |
1923 | + def throw_parse_error(self, problem, cls=None, **kwargs): |
1924 | + if cls is None: |
1925 | + cls = RecipeParseError |
1926 | + raise cls(self.filename, self.line_index + 1, |
1927 | + self.index + 1, problem, **kwargs) |
1928 | |
1929 | def throw_expecting_error(self, expected, actual): |
1930 | self.throw_parse_error("Expecting '%s', got '%s'" |
1931 | @@ -720,6 +969,12 @@ |
1932 | else: |
1933 | self.current_line = self.lines[self.line_index] |
1934 | |
1935 | + def is_blankline(self): |
1936 | + whitespace = self.peek_whitespace() |
1937 | + if whitespace is None: |
1938 | + return True |
1939 | + return self.peek_char(skip=len(whitespace)) is None |
1940 | + |
1941 | def take_char(self): |
1942 | if self.index >= len(self.current_line): |
1943 | return None |
1944 | @@ -789,6 +1044,18 @@ |
1945 | actual = self.peek_char() |
1946 | return ret |
1947 | |
1948 | + def peek_whitespace(self): |
1949 | + ret = "" |
1950 | + char = self.peek_char() |
1951 | + if char is None: |
1952 | + return char |
1953 | + count = 0 |
1954 | + while char is not None and char in self.whitespace_chars: |
1955 | + ret += char |
1956 | + count += 1 |
1957 | + char = self.peek_char(skip=count) |
1958 | + return ret |
1959 | + |
1960 | def parse_word(self, expected, require_whitespace=True): |
1961 | self.parse_whitespace("'%s'" % expected, require=require_whitespace) |
1962 | length = len(expected) |
1963 | @@ -865,10 +1132,14 @@ |
1964 | return text |
1965 | |
1966 | def parse_comment_line(self): |
1967 | - if self.peek_char() is None: |
1968 | - return "" |
1969 | - if self.peek_char() != "#": |
1970 | + whitespace = self.peek_whitespace() |
1971 | + if whitespace is None: |
1972 | + return "" |
1973 | + if self.peek_char(skip=len(whitespace)) is None: |
1974 | + return "" |
1975 | + if self.peek_char(skip=len(whitespace)) != "#": |
1976 | return None |
1977 | + self.parse_whitespace(None, require=False) |
1978 | comment = self.current_line[self.index:] |
1979 | self.index += len(comment) |
1980 | return comment |
1981 | |
1982 | === modified file 'setup.py' |
1983 | --- setup.py 2009-08-25 11:43:42 +0000 |
1984 | +++ setup.py 2010-08-13 01:14:42 +0000 |
1985 | @@ -2,15 +2,16 @@ |
1986 | |
1987 | from distutils.core import setup |
1988 | |
1989 | -setup(name="bzr-builder", |
1990 | - version="0.1", |
1991 | - description="Turn a recipe in to a bzr branch", |
1992 | - author="James Westby", |
1993 | - author_email="james.westby@canonical.com", |
1994 | - license="GNU GPL v3", |
1995 | - url="http://launchpad.net/bzr-builder", |
1996 | - packages=['bzrlib.plugins.builder', |
1997 | - 'bzrlib.plugins.builder.tests', |
1998 | - ], |
1999 | - package_dir={'bzrlib.plugins.builder': '.'}, |
2000 | - ) |
2001 | +if __name__ == '__main__': |
2002 | + setup(name="bzr-builder", |
2003 | + version="0.2", |
2004 | + description="Turn a recipe in to a bzr branch", |
2005 | + author="James Westby", |
2006 | + author_email="james.westby@canonical.com", |
2007 | + license="GNU GPL v3", |
2008 | + url="http://launchpad.net/bzr-builder", |
2009 | + packages=['bzrlib.plugins.builder', |
2010 | + 'bzrlib.plugins.builder.tests', |
2011 | + ], |
2012 | + package_dir={'bzrlib.plugins.builder': '.'}, |
2013 | + ) |
2014 | |
2015 | === modified file 'tests/__init__.py' |
2016 | --- tests/__init__.py 2009-04-16 20:23:39 +0000 |
2017 | +++ tests/__init__.py 2010-08-13 01:14:42 +0000 |
2018 | @@ -20,9 +20,10 @@ |
2019 | loader = TestUtil.TestLoader() |
2020 | suite = TestSuite() |
2021 | testmod_names = [ |
2022 | - 'test_blackbox', |
2023 | - 'test_recipe', |
2024 | + 'blackbox', |
2025 | + 'ppa', |
2026 | + 'recipe', |
2027 | ] |
2028 | - suite.addTest(loader.loadTestsFromModuleNames(["%s.%s" % (__name__, i) |
2029 | + suite.addTest(loader.loadTestsFromModuleNames(["%s.test_%s" % (__name__, i) |
2030 | for i in testmod_names])) |
2031 | return suite |
2032 | |
2033 | === modified file 'tests/test_blackbox.py' |
2034 | --- tests/test_blackbox.py 2010-08-13 01:14:42 +0000 |
2035 | +++ tests/test_blackbox.py 2010-08-13 01:14:42 +0000 |
2036 | @@ -55,7 +55,7 @@ |
2037 | self.build_tree(["source/a"]) |
2038 | source.add(["a"]) |
2039 | revid = source.commit("one") |
2040 | - self.run_bzr("build recipe working") |
2041 | + self.run_bzr("build -q recipe working") |
2042 | self.failUnlessExists("working/a") |
2043 | tree = workingtree.WorkingTree.open("working") |
2044 | self.assertEqual(revid, tree.last_revision()) |
2045 | @@ -71,7 +71,7 @@ |
2046 | self.build_tree(["source/a"]) |
2047 | source.add(["a"]) |
2048 | revid = source.commit("one") |
2049 | - self.run_bzr("build recipe working --manifest manifest") |
2050 | + self.run_bzr("build -q recipe working --manifest manifest") |
2051 | self.failUnlessExists("working/a") |
2052 | self.failUnlessExists("manifest") |
2053 | self.check_file_contents("manifest", "# bzr-builder format 0.1 " |
2054 | @@ -83,7 +83,7 @@ |
2055 | source = self.make_branch_and_tree("source") |
2056 | self.build_tree(["source/a"]) |
2057 | source.add(["a"]) |
2058 | - revid = source.commit("one") |
2059 | + source.commit("one") |
2060 | out, err = self.run_bzr("build recipe working " |
2061 | "--if-changed-from manifest") |
2062 | |
2063 | @@ -111,7 +111,7 @@ |
2064 | "deb-version 1\nsource 1\n")]) |
2065 | self.build_tree_contents([("old-manifest", "# bzr-builder format 0.1 " |
2066 | "deb-version 1\nsource revid:foo\n")]) |
2067 | - out, err = self.run_bzr("build recipe working --manifest manifest " |
2068 | + out, err = self.run_bzr("build -q recipe working --manifest manifest " |
2069 | "--if-changed-from old-manifest") |
2070 | self.failUnlessExists("working/a") |
2071 | self.failUnlessExists("manifest") |
2072 | @@ -130,7 +130,7 @@ |
2073 | revid = source.commit("one") |
2074 | self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2075 | "deb-version 1\nsource 1\n")]) |
2076 | - out, err = self.run_bzr("dailydeb test.recipe working " |
2077 | + out, err = self.run_bzr("dailydeb -q test.recipe working " |
2078 | "--manifest manifest --package foo") |
2079 | self.failIfExists("working/a") |
2080 | package_root = "working/foo-1/" |
2081 | @@ -149,13 +149,14 @@ |
2082 | cl_f = open(cl_path) |
2083 | try: |
2084 | line = cl_f.readline() |
2085 | - self.assertEqual("foo (1) jaunty; urgency=low\n", line) |
2086 | + self.assertEqual("foo (1) lucid; urgency=low\n", line) |
2087 | finally: |
2088 | cl_f.close() |
2089 | |
2090 | def test_cmd_dailydeb_no_work_dir(self): |
2091 | #TODO: define a test feature for debuild and require it here. |
2092 | - self.permit_dir('/') # Allow the made working dir to be accessed. |
2093 | + if getattr(self, "permit_dir", None) is not None: |
2094 | + self.permit_dir('/') # Allow the made working dir to be accessed. |
2095 | source = self.make_branch_and_tree("source") |
2096 | self.build_tree(["source/a", "source/debian/"]) |
2097 | self.build_tree_contents([("source/debian/rules", |
2098 | @@ -163,15 +164,16 @@ |
2099 | ("source/debian/control", |
2100 | "Source: foo\nMaintainer: maint maint@maint.org\n")]) |
2101 | source.add(["a", "debian/", "debian/rules", "debian/control"]) |
2102 | - revid = source.commit("one") |
2103 | + source.commit("one") |
2104 | self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2105 | "deb-version 1\nsource 1\n")]) |
2106 | - out, err = self.run_bzr("dailydeb test.recipe " |
2107 | + out, err = self.run_bzr("dailydeb -q test.recipe " |
2108 | "--manifest manifest --package foo") |
2109 | |
2110 | def test_cmd_dailydeb_if_changed_from_non_existant(self): |
2111 | #TODO: define a test feature for debuild and require it here. |
2112 | - self.permit_dir('/') # Allow the made working dir to be accessed. |
2113 | + if getattr(self, "permit_dir", None) is not None: |
2114 | + self.permit_dir('/') # Allow the made working dir to be accessed. |
2115 | source = self.make_branch_and_tree("source") |
2116 | self.build_tree(["source/a", "source/debian/"]) |
2117 | self.build_tree_contents([("source/debian/rules", |
2118 | @@ -179,10 +181,10 @@ |
2119 | ("source/debian/control", |
2120 | "Source: foo\nMaintainer: maint maint@maint.org\n")]) |
2121 | source.add(["a", "debian/", "debian/rules", "debian/control"]) |
2122 | - revid = source.commit("one") |
2123 | + source.commit("one") |
2124 | self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2125 | "deb-version 1\nsource 1\n")]) |
2126 | - out, err = self.run_bzr("dailydeb test.recipe " |
2127 | + out, err = self.run_bzr("dailydeb -q test.recipe " |
2128 | "--manifest manifest --package foo --if-changed-from bar") |
2129 | |
2130 | def make_simple_package(self): |
2131 | @@ -201,12 +203,29 @@ |
2132 | source.commit("one") |
2133 | return source |
2134 | |
2135 | + def test_cmd_dailydeb_no_build(self): |
2136 | + self.make_simple_package() |
2137 | + self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2138 | + "deb-version 1\nsource 1\n")]) |
2139 | + out, err = self.run_bzr("dailydeb -q test.recipe " |
2140 | + "--manifest manifest --no-build working") |
2141 | + new_cl_contents = ("package (1) unstable; urgency=low\n\n" |
2142 | + " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ") |
2143 | + f = open("working/test-1/debian/changelog") |
2144 | + try: |
2145 | + actual_cl_contents = f.read() |
2146 | + finally: |
2147 | + f.close() |
2148 | + self.assertStartsWith(actual_cl_contents, new_cl_contents) |
2149 | + for fn in os.listdir("working"): |
2150 | + self.assertFalse(fn.endswith(".changes")) |
2151 | + |
2152 | def test_cmd_dailydeb_with_package_from_changelog(self): |
2153 | #TODO: define a test feature for debuild and require it here. |
2154 | - source = self.make_simple_package() |
2155 | + self.make_simple_package() |
2156 | self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2157 | "deb-version 1\nsource 1\n")]) |
2158 | - out, err = self.run_bzr("dailydeb test.recipe " |
2159 | + out, err = self.run_bzr("dailydeb -q test.recipe " |
2160 | "--manifest manifest --if-changed-from bar working") |
2161 | new_cl_contents = ("package (1) unstable; urgency=low\n\n" |
2162 | " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ") |
2163 | @@ -218,10 +237,10 @@ |
2164 | self.assertStartsWith(actual_cl_contents, new_cl_contents) |
2165 | |
2166 | def test_cmd_dailydeb_with_upstream_version_from_changelog(self): |
2167 | - source = self.make_simple_package() |
2168 | + self.make_simple_package() |
2169 | self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2170 | "deb-version {debupstream}-2\nsource 1\n")]) |
2171 | - out, err = self.run_bzr("dailydeb test.recipe working") |
2172 | + out, err = self.run_bzr("dailydeb -q test.recipe working") |
2173 | new_cl_contents = ("package (0.1-2) unstable; urgency=low\n\n" |
2174 | " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ") |
2175 | f = open("working/test-{debupstream}-2/debian/changelog") |
2176 | @@ -230,3 +249,30 @@ |
2177 | finally: |
2178 | f.close() |
2179 | self.assertStartsWith(actual_cl_contents, new_cl_contents) |
2180 | + |
2181 | + def test_cmd_dailydeb_with_append_version(self): |
2182 | + self.make_simple_package() |
2183 | + self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2184 | + "deb-version 1\nsource 1\n")]) |
2185 | + out, err = self.run_bzr("dailydeb -q test.recipe working " |
2186 | + "--append-version ~ppa1") |
2187 | + new_cl_contents = ("package (1~ppa1) unstable; urgency=low\n\n" |
2188 | + " * Auto build.\n\n -- M. Maintainer <maint@maint.org> ") |
2189 | + f = open("working/test-1/debian/changelog") |
2190 | + try: |
2191 | + actual_cl_contents = f.read() |
2192 | + finally: |
2193 | + f.close() |
2194 | + self.assertStartsWith(actual_cl_contents, new_cl_contents) |
2195 | + |
2196 | + def test_cmd_dailydeb_with_invalid_version(self): |
2197 | + source = self.make_branch_and_tree("source") |
2198 | + self.build_tree(["source/a"]) |
2199 | + source.add(["a"]) |
2200 | + revid = source.commit("one") |
2201 | + self.build_tree_contents([("test.recipe", "# bzr-builder format 0.1 " |
2202 | + "deb-version $\nsource 1\n")]) |
2203 | + err = self.run_bzr("dailydeb -q test.recipe working --package foo", |
2204 | + retcode=3)[1] |
2205 | + self.assertEqual("bzr: ERROR: Invalid deb-version: $: " |
2206 | + "Could not parse version: $\n", err) |
2207 | |
2208 | === added file 'tests/test_ppa.py' |
2209 | --- tests/test_ppa.py 1970-01-01 00:00:00 +0000 |
2210 | +++ tests/test_ppa.py 2010-08-13 01:14:42 +0000 |
2211 | @@ -0,0 +1,28 @@ |
2212 | +# bzr-builder: a bzr plugin to construct trees based on recipes |
2213 | +# Copyright 2009 Canonical Ltd. |
2214 | + |
2215 | +# This program is free software: you can redistribute it and/or modify it |
2216 | +# under the terms of the GNU General Public License version 3, as published |
2217 | +# by the Free Software Foundation. |
2218 | + |
2219 | +# This program is distributed in the hope that it will be useful, but |
2220 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
2221 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
2222 | +# PURPOSE. See the GNU General Public License for more details. |
2223 | + |
2224 | +# You should have received a copy of the GNU General Public License along |
2225 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
2226 | + |
2227 | +from bzrlib.plugins.builder.cmds import target_from_dput |
2228 | +from bzrlib.tests import ( |
2229 | + TestCase, |
2230 | + ) |
2231 | + |
2232 | + |
2233 | +class TestTargetFromDPut(TestCase): |
2234 | + |
2235 | + def test_default_ppa(self): |
2236 | + self.assertEqual('team-name/ppa', target_from_dput('ppa:team-name')) |
2237 | + |
2238 | + def test_named_ppa(self): |
2239 | + self.assertEqual('team/ppa2', target_from_dput('ppa:team/ppa2')) |
2240 | |
2241 | === modified file 'tests/test_recipe.py' |
2242 | --- tests/test_recipe.py 2010-08-13 01:14:42 +0000 |
2243 | +++ tests/test_recipe.py 2010-08-13 01:14:42 +0000 |
2244 | @@ -27,26 +27,28 @@ |
2245 | ) |
2246 | from bzrlib.plugins.builder.recipe import ( |
2247 | BaseRecipeBranch, |
2248 | - build_manifest, |
2249 | build_tree, |
2250 | ensure_basedir, |
2251 | + ForbiddenInstructionError, |
2252 | pull_or_branch, |
2253 | RecipeParser, |
2254 | RecipeBranch, |
2255 | RecipeParseError, |
2256 | resolve_revisions, |
2257 | + RUN_INSTRUCTION, |
2258 | ) |
2259 | |
2260 | |
2261 | class RecipeParserTests(TestCaseInTempDir): |
2262 | |
2263 | deb_version = "0.1-{revno}" |
2264 | - basic_header = ("# bzr-builder format 0.2 deb-version " |
2265 | + basic_header = ("# bzr-builder format 0.3 deb-version " |
2266 | + deb_version +"\n") |
2267 | - basic_header_and_branch = basic_header + "http://foo.org/\n" |
2268 | + basic_branch = "http://foo.org/" |
2269 | + basic_header_and_branch = basic_header + basic_branch + "\n" |
2270 | |
2271 | - def get_recipe(self, recipe_text): |
2272 | - return RecipeParser(recipe_text).parse() |
2273 | + def get_recipe(self, recipe_text, **kwargs): |
2274 | + return RecipeParser(recipe_text).parse(**kwargs) |
2275 | |
2276 | def assertParseError(self, line, char, problem, callable, *args, |
2277 | **kwargs): |
2278 | @@ -55,6 +57,7 @@ |
2279 | self.assertEqual(line, exc.line) |
2280 | self.assertEqual(char, exc.char) |
2281 | self.assertEqual("recipe", exc.filename) |
2282 | + return exc |
2283 | |
2284 | def check_recipe_branch(self, branch, name, url, revspec=None, |
2285 | num_child_branches=0, revid=None): |
2286 | @@ -130,8 +133,8 @@ |
2287 | self.basic_header + "http://foo.org/ 2 foo") |
2288 | |
2289 | def tests_rejects_unknown_instruction(self): |
2290 | - self.assertParseError(3, 1, "Expecting 'merge', 'nest' or 'run', " |
2291 | - "got 'cat'", self.get_recipe, |
2292 | + self.assertParseError(3, 1, "Expecting 'merge', 'nest', 'nest-part' " |
2293 | + "or 'run', got 'cat'", self.get_recipe, |
2294 | self.basic_header + "http://foo.org/\n" + "cat") |
2295 | |
2296 | def test_rejects_merge_no_name(self): |
2297 | @@ -149,6 +152,21 @@ |
2298 | "got 'bar'", self.get_recipe, |
2299 | self.basic_header_and_branch + "merge foo url 2 bar") |
2300 | |
2301 | + def test_rejects_nest_part_no_name(self): |
2302 | + self.assertParseError(3, 11, "End of line while looking for " |
2303 | + "the branch id", self.get_recipe, |
2304 | + self.basic_header_and_branch + "nest-part ") |
2305 | + |
2306 | + def test_rejects_nest_part_no_url(self): |
2307 | + self.assertParseError(3, 15, "End of line while looking for " |
2308 | + "the branch url", self.get_recipe, |
2309 | + self.basic_header_and_branch + "nest-part foo ") |
2310 | + |
2311 | + def test_rejects_nest_part_no_subpath(self): |
2312 | + self.assertParseError(3, 22, "End of line while looking for " |
2313 | + "the subpath to merge", self.get_recipe, |
2314 | + self.basic_header_and_branch + "nest-part foo url:// ") |
2315 | + |
2316 | def test_rejects_nest_no_name(self): |
2317 | self.assertParseError(3, 6, "End of line while looking for " |
2318 | "the branch id", self.get_recipe, |
2319 | @@ -202,6 +220,12 @@ |
2320 | self.assertParseError(3, 1, "Empty recipe", self.get_recipe, |
2321 | self.basic_header) |
2322 | |
2323 | + def test_rejects_non_unique_ids(self): |
2324 | + self.assertParseError(4, 7, "'foo' was already used to identify " |
2325 | + "a branch.", self.get_recipe, |
2326 | + self.basic_header_and_branch + "merge foo url\n" |
2327 | + + "merge foo other-url\n") |
2328 | + |
2329 | def test_builds_simplest_recipe(self): |
2330 | base_branch = self.get_recipe(self.basic_header_and_branch) |
2331 | self.check_base_recipe_branch(base_branch, "http://foo.org/") |
2332 | @@ -220,6 +244,42 @@ |
2333 | self.assertEqual(None, location) |
2334 | self.check_recipe_branch(child_branch, "bar", "http://bar.org") |
2335 | |
2336 | + def test_builds_recipe_with_nest_part(self): |
2337 | + base_branch = self.get_recipe(self.basic_header_and_branch |
2338 | + + "nest-part bar http://bar.org some/path") |
2339 | + self.check_base_recipe_branch(base_branch, "http://foo.org/", |
2340 | + num_child_branches=1) |
2341 | + instruction = base_branch.child_branches[0] |
2342 | + self.assertEqual(None, instruction.nest_path) |
2343 | + self.check_recipe_branch( |
2344 | + instruction.recipe_branch, "bar", "http://bar.org") |
2345 | + self.assertEqual("some/path", instruction.subpath) |
2346 | + self.assertEqual(None, instruction.target_subdir) |
2347 | + |
2348 | + def test_builds_recipe_with_nest_part_subdir(self): |
2349 | + base_branch = self.get_recipe(self.basic_header_and_branch |
2350 | + + "nest-part bar http://bar.org some/path target-subdir") |
2351 | + self.check_base_recipe_branch(base_branch, "http://foo.org/", |
2352 | + num_child_branches=1) |
2353 | + instruction = base_branch.child_branches[0] |
2354 | + self.assertEqual(None, instruction.nest_path) |
2355 | + self.check_recipe_branch( |
2356 | + instruction.recipe_branch, "bar", "http://bar.org") |
2357 | + self.assertEqual("some/path", instruction.subpath) |
2358 | + self.assertEqual("target-subdir", instruction.target_subdir) |
2359 | + |
2360 | + def test_builds_recipe_with_nest_part_subdir_and_revspec(self): |
2361 | + base_branch = self.get_recipe(self.basic_header_and_branch |
2362 | + + "nest-part bar http://bar.org some/path target-subdir 1234") |
2363 | + self.check_base_recipe_branch(base_branch, "http://foo.org/", |
2364 | + num_child_branches=1) |
2365 | + instruction = base_branch.child_branches[0] |
2366 | + self.assertEqual(None, instruction.nest_path) |
2367 | + self.check_recipe_branch( |
2368 | + instruction.recipe_branch, "bar", "http://bar.org", "1234") |
2369 | + self.assertEqual("some/path", instruction.subpath) |
2370 | + self.assertEqual("target-subdir", instruction.target_subdir) |
2371 | + |
2372 | def test_builds_recipe_with_nest(self): |
2373 | base_branch = self.get_recipe(self.basic_header_and_branch |
2374 | + "nest bar http://bar.org baz") |
2375 | @@ -306,6 +366,60 @@ |
2376 | self.assertEqual(None, child_branch) |
2377 | self.assertEqual("touch test", command) |
2378 | |
2379 | + def test_accepts_blank_line_during_nest(self): |
2380 | + base_branch = self.get_recipe(self.basic_header_and_branch |
2381 | + + "nest foo http://bar.org bar\n merge baz baz.org\n\n" |
2382 | + " merge zap zap.org\n") |
2383 | + self.check_base_recipe_branch(base_branch, self.basic_branch, |
2384 | + num_child_branches=1) |
2385 | + nested_branch, location = base_branch.child_branches[0].as_tuple() |
2386 | + self.assertEqual("bar", location) |
2387 | + self.check_recipe_branch(nested_branch, "foo", "http://bar.org", |
2388 | + num_child_branches=2) |
2389 | + child_branch, location = nested_branch.child_branches[0].as_tuple() |
2390 | + self.assertEqual(None, location) |
2391 | + self.check_recipe_branch(child_branch, "baz", "baz.org") |
2392 | + child_branch, location = nested_branch.child_branches[1].as_tuple() |
2393 | + self.assertEqual(None, location) |
2394 | + self.check_recipe_branch(child_branch, "zap", "zap.org") |
2395 | + |
2396 | + def test_accepts_blank_line_at_start_of_nest(self): |
2397 | + base_branch = self.get_recipe(self.basic_header_and_branch |
2398 | + + "nest foo http://bar.org bar\n\n merge baz baz.org\n") |
2399 | + self.check_base_recipe_branch(base_branch, self.basic_branch, |
2400 | + num_child_branches=1) |
2401 | + nested_branch, location = base_branch.child_branches[0].as_tuple() |
2402 | + self.assertEqual("bar", location) |
2403 | + self.check_recipe_branch(nested_branch, "foo", "http://bar.org", |
2404 | + num_child_branches=1) |
2405 | + child_branch, location = nested_branch.child_branches[0].as_tuple() |
2406 | + self.assertEqual(None, location) |
2407 | + self.check_recipe_branch(child_branch, "baz", "baz.org") |
2408 | + |
2409 | + def test_accepts_blank_line_as_only_thing_in_nest(self): |
2410 | + base_branch = self.get_recipe(self.basic_header_and_branch |
2411 | + + "nest foo http://bar.org bar\n\nmerge baz baz.org\n") |
2412 | + self.check_base_recipe_branch(base_branch, self.basic_branch, |
2413 | + num_child_branches=2) |
2414 | + nested_branch, location = base_branch.child_branches[0].as_tuple() |
2415 | + self.assertEqual("bar", location) |
2416 | + self.check_recipe_branch(nested_branch, "foo", "http://bar.org") |
2417 | + child_branch, location = base_branch.child_branches[1].as_tuple() |
2418 | + self.assertEqual(None, location) |
2419 | + self.check_recipe_branch(child_branch, "baz", "baz.org") |
2420 | + |
2421 | + def test_accepts_comment_line_with_any_number_of_spaces(self): |
2422 | + base_branch = self.get_recipe(self.basic_header_and_branch |
2423 | + + "nest foo http://bar.org bar\n #foo\nmerge baz baz.org\n") |
2424 | + self.check_base_recipe_branch(base_branch, self.basic_branch, |
2425 | + num_child_branches=2) |
2426 | + nested_branch, location = base_branch.child_branches[0].as_tuple() |
2427 | + self.assertEqual("bar", location) |
2428 | + self.check_recipe_branch(nested_branch, "foo", "http://bar.org") |
2429 | + child_branch, location = base_branch.child_branches[1].as_tuple() |
2430 | + self.assertEqual(None, location) |
2431 | + self.check_recipe_branch(child_branch, "baz", "baz.org") |
2432 | + |
2433 | def test_old_format_rejects_run(self): |
2434 | header = ("# bzr-builder format 0.1 deb-version " |
2435 | + self.deb_version +"\n") |
2436 | @@ -313,6 +427,45 @@ |
2437 | , self.get_recipe, header + "http://foo.org/\n" |
2438 | + "run touch test \n") |
2439 | |
2440 | + def test_error_on_forbidden_instructions(self): |
2441 | + exc = self.assertParseError(3, 1, "The 'run' instruction is " |
2442 | + "forbidden.", self.get_recipe, self.basic_header_and_branch |
2443 | + + "run touch test\n", |
2444 | + forbidden_instructions=[RUN_INSTRUCTION]) |
2445 | + self.assertTrue(isinstance(exc, ForbiddenInstructionError)) |
2446 | + self.assertEqual("run", exc.instruction_name) |
2447 | + |
2448 | + def test_error_on_duplicate_path(self): |
2449 | + exc = self.assertParseError(3, 15, "The path '.' is a duplicate " |
2450 | + "of the one used on line 1.", self.get_recipe, |
2451 | + self.basic_header_and_branch + "nest nest url .\n") |
2452 | + |
2453 | + def test_error_on_duplicate_path_with_another_nest(self): |
2454 | + exc = self.assertParseError(4, 16, "The path 'foo' is a duplicate " |
2455 | + "of the one used on line 3.", self.get_recipe, |
2456 | + self.basic_header_and_branch + "nest nest url foo\n" |
2457 | + + "nest nest2 url foo\n") |
2458 | + |
2459 | + def test_duplicate_path_check_uses_normpath(self): |
2460 | + exc = self.assertParseError(3, 15, "The path 'foo/..' is a duplicate " |
2461 | + "of the one used on line 1.", self.get_recipe, |
2462 | + self.basic_header_and_branch + "nest nest url foo/..\n") |
2463 | + |
2464 | + def test_error_absolute_path(self): |
2465 | + exc = self.assertParseError(3, 15, "Absolute paths are not allowed: " |
2466 | + "/etc/passwd", self.get_recipe, self.basic_header_and_branch |
2467 | + + "nest nest url /etc/passwd\n") |
2468 | + |
2469 | + def test_error_simple_parent_dir(self): |
2470 | + exc = self.assertParseError(3, 15, "Paths outside the current " |
2471 | + "directory are not allowed: ../foo", self.get_recipe, |
2472 | + self.basic_header_and_branch + "nest nest url ../foo\n") |
2473 | + |
2474 | + def test_error_complex_parent_dir(self): |
2475 | + exc = self.assertParseError(3, 15, "Paths outside the current " |
2476 | + "directory are not allowed: ./foo/../..", self.get_recipe, |
2477 | + self.basic_header_and_branch + "nest nest url ./foo/../..\n") |
2478 | + |
2479 | |
2480 | class BuildTreeTests(TestCaseWithTransport): |
2481 | |
2482 | @@ -352,7 +505,7 @@ |
2483 | def test_build_tree_single_branch_existing_branch(self): |
2484 | source = self.make_branch_and_tree("source") |
2485 | revid = source.commit("one") |
2486 | - target = self.make_branch_and_tree("target") |
2487 | + self.make_branch_and_tree("target") |
2488 | base_branch = BaseRecipeBranch("source", "1", 0.2) |
2489 | build_tree(base_branch, "target") |
2490 | self.failUnlessExists("target") |
2491 | @@ -360,15 +513,17 @@ |
2492 | self.assertEqual(revid, tree.last_revision()) |
2493 | self.assertEqual(revid, base_branch.revid) |
2494 | |
2495 | + def make_source_branch(self, relpath): |
2496 | + """Make a branch with one file and one commit.""" |
2497 | + source1 = self.make_branch_and_tree(relpath) |
2498 | + self.build_tree([relpath + "/a"]) |
2499 | + source1.add(["a"]) |
2500 | + source1.commit("one") |
2501 | + return source1 |
2502 | + |
2503 | def test_build_tree_nested(self): |
2504 | - source1 = self.make_branch_and_tree("source1") |
2505 | - self.build_tree(["source1/a"]) |
2506 | - source1.add(["a"]) |
2507 | - source1_rev_id = source1.commit("one") |
2508 | - source2 = self.make_branch_and_tree("source2") |
2509 | - self.build_tree(["source2/a"]) |
2510 | - source2.add(["a"]) |
2511 | - source2_rev_id = source2.commit("one") |
2512 | + source1_rev_id = self.make_source_branch("source1").last_revision() |
2513 | + source2_rev_id = self.make_source_branch("source2").last_revision() |
2514 | base_branch = BaseRecipeBranch("source1", "1", 0.2) |
2515 | nested_branch = RecipeBranch("nested", "source2") |
2516 | base_branch.nest_branch("sub", nested_branch) |
2517 | @@ -382,10 +537,8 @@ |
2518 | self.assertEqual(source2_rev_id, nested_branch.revid) |
2519 | |
2520 | def test_build_tree_merged(self): |
2521 | - source1 = self.make_branch_and_tree("source1") |
2522 | - self.build_tree(["source1/a"]) |
2523 | - source1.add(["a"]) |
2524 | - source1_rev_id = source1.commit("one") |
2525 | + source1 = self.make_source_branch("source1") |
2526 | + source1_rev_id = source1.last_revision() |
2527 | source2 = source1.bzrdir.sprout("source2").open_workingtree() |
2528 | self.build_tree_contents([("source2/a", "other change")]) |
2529 | source2_rev_id = source2.commit("one") |
2530 | @@ -403,11 +556,60 @@ |
2531 | self.assertEqual(source1_rev_id, base_branch.revid) |
2532 | self.assertEqual(source2_rev_id, merged_branch.revid) |
2533 | |
2534 | + def test_build_tree_nest_part(self): |
2535 | + """A recipe can specify a merge of just part of an unrelated tree.""" |
2536 | + source1 = self.make_source_branch("source1") |
2537 | + source2 = self.make_source_branch("source2") |
2538 | + source1.lock_read() |
2539 | + self.addCleanup(source1.unlock) |
2540 | + source1_rev_id = source1.last_revision() |
2541 | + # Add 'b' to source2. |
2542 | + self.build_tree_contents([ |
2543 | + ("source2/b", "new file"), ("source2/not-b", "other file")]) |
2544 | + source2.add(["b", "not-b"]) |
2545 | + source2_rev_id = source2.commit("two") |
2546 | + base_branch = BaseRecipeBranch("source1", "1", 0.2) |
2547 | + merged_branch = RecipeBranch("merged", "source2") |
2548 | + # Merge just 'b' from source2; 'a' is untouched. |
2549 | + base_branch.nest_part_branch(merged_branch, "b") |
2550 | + build_tree(base_branch, "target") |
2551 | + file_id = source1.path2id("a") |
2552 | + self.check_file_contents("target/a", source1.get_file_text(file_id)) |
2553 | + self.check_file_contents("target/b", "new file") |
2554 | + self.assertNotInWorkingTree("not-b", "target") |
2555 | + self.assertEqual(source1_rev_id, base_branch.revid) |
2556 | + self.assertEqual(source2_rev_id, merged_branch.revid) |
2557 | + |
2558 | + def test_build_tree_nest_part_explicit_target(self): |
2559 | + """A recipe can specify a merge of just part of an unrelated tree into |
2560 | + a specific subdirectory of the target tree. |
2561 | + """ |
2562 | + source1 = self.make_branch_and_tree("source1") |
2563 | + self.build_tree(["source1/dir/"]) |
2564 | + source1.add(["dir"]) |
2565 | + source1.commit("one") |
2566 | + source2 = self.make_source_branch("source2") |
2567 | + source1.lock_read() |
2568 | + self.addCleanup(source1.unlock) |
2569 | + source1_rev_id = source1.last_revision() |
2570 | + # Add 'b' to source2. |
2571 | + self.build_tree_contents([ |
2572 | + ("source2/b", "new file"), ("source2/not-b", "other file")]) |
2573 | + source2.add(["b", "not-b"]) |
2574 | + source2_rev_id = source2.commit("two") |
2575 | + base_branch = BaseRecipeBranch("source1", "1", 0.2) |
2576 | + merged_branch = RecipeBranch("merged", "source2") |
2577 | + # Merge just 'b' from source2; 'a' is untouched. |
2578 | + base_branch.nest_part_branch(merged_branch, "b", "dir/b") |
2579 | + build_tree(base_branch, "target") |
2580 | + self.check_file_contents("target/dir/b", "new file") |
2581 | + self.assertNotInWorkingTree("dir/not-b", "target") |
2582 | + self.assertEqual(source1_rev_id, base_branch.revid) |
2583 | + self.assertEqual(source2_rev_id, merged_branch.revid) |
2584 | + |
2585 | def test_build_tree_merge_twice(self): |
2586 | - source1 = self.make_branch_and_tree("source1") |
2587 | - self.build_tree(["source1/a"]) |
2588 | - source1.add(["a"]) |
2589 | - source1_rev_id = source1.commit("one") |
2590 | + source1 = self.make_source_branch("source1") |
2591 | + source1_rev_id = source1.last_revision() |
2592 | source2 = source1.bzrdir.sprout("source2").open_workingtree() |
2593 | self.build_tree_contents([("source2/a", "other change")]) |
2594 | source2_rev_id = source2.commit("one") |
2595 | @@ -436,10 +638,7 @@ |
2596 | self.assertEqual(source3_rev_id, merged_branch2.revid) |
2597 | |
2598 | def test_build_tree_merged_with_conflicts(self): |
2599 | - source1 = self.make_branch_and_tree("source1") |
2600 | - self.build_tree(["source1/a"]) |
2601 | - source1.add(["a"]) |
2602 | - source1_rev_id = source1.commit("one") |
2603 | + source1 = self.make_source_branch("source1") |
2604 | source2 = source1.bzrdir.sprout("source2").open_workingtree() |
2605 | self.build_tree_contents([("source2/a", "other change\n")]) |
2606 | source2_rev_id = source2.commit("one") |
2607 | @@ -448,7 +647,7 @@ |
2608 | base_branch = BaseRecipeBranch("source1", "1", 0.2) |
2609 | merged_branch = RecipeBranch("merged", "source2") |
2610 | base_branch.merge_branch(merged_branch) |
2611 | - e = self.assertRaises(errors.BzrCommandError, build_tree, |
2612 | + self.assertRaises(errors.BzrCommandError, build_tree, |
2613 | base_branch, "target") |
2614 | self.failUnlessExists("target") |
2615 | tree = workingtree.WorkingTree.open("target") |
2616 | @@ -465,10 +664,8 @@ |
2617 | self.assertEqual(source2_rev_id, merged_branch.revid) |
2618 | |
2619 | def test_build_tree_with_revspecs(self): |
2620 | - source1 = self.make_branch_and_tree("source1") |
2621 | - self.build_tree(["source1/a"]) |
2622 | - source1.add(["a"]) |
2623 | - source1_rev_id = source1.commit("one") |
2624 | + source1 = self.make_source_branch("source1") |
2625 | + source1_rev_id = source1.last_revision() |
2626 | source2 = source1.bzrdir.sprout("source2").open_workingtree() |
2627 | self.build_tree_contents([("source2/a", "other change\n")]) |
2628 | source2_rev_id = source2.commit("one") |
2629 | @@ -622,6 +819,20 @@ |
2630 | self.assertEqual(revid, tree.last_revision()) |
2631 | self.assertEqual(revid, base_branch.revid) |
2632 | |
2633 | + def test_error_on_merge_revspec(self): |
2634 | + # See bug 416950 |
2635 | + source = self.make_branch_and_tree("source") |
2636 | + revid = source.commit("one") |
2637 | + base_branch = BaseRecipeBranch("source", "1", 0.2) |
2638 | + merged_branch = RecipeBranch("merged", "source", revspec="debian") |
2639 | + base_branch.merge_branch(merged_branch) |
2640 | + e = self.assertRaises(errors.InvalidRevisionSpec, |
2641 | + build_tree, base_branch, "target") |
2642 | + self.assertTrue(str(e).startswith("Requested revision: 'debian' " |
2643 | + "does not exist in branch: ")) |
2644 | + self.assertTrue(str(e).endswith(". Did you not mean to specify a " |
2645 | + "revspec at the end of the merge line?")) |
2646 | + |
2647 | |
2648 | class ResolveRevisionsTests(TestCaseWithTransport): |
2649 | |
2650 | @@ -704,7 +915,7 @@ |
2651 | |
2652 | def test_changed_command(self): |
2653 | source =self.make_branch_and_tree("source") |
2654 | - revid = source.commit("one") |
2655 | + source.commit("one") |
2656 | branch1 = BaseRecipeBranch("source", "{revno}", 0.2) |
2657 | branch2 = BaseRecipeBranch("source", "{revno}", 0.2) |
2658 | branch1.run_command("touch test1") |
2659 | @@ -713,6 +924,17 @@ |
2660 | if_changed_from=branch2)) |
2661 | self.assertEqual("source", branch1.url) |
2662 | |
2663 | + def test_unchanged_command(self): |
2664 | + source =self.make_branch_and_tree("source") |
2665 | + source.commit("one") |
2666 | + branch1 = BaseRecipeBranch("source", "{revno}", 0.2) |
2667 | + branch2 = BaseRecipeBranch("source", "{revno}", 0.2) |
2668 | + branch1.run_command("touch test1") |
2669 | + branch2.run_command("touch test1") |
2670 | + self.assertEqual(False, resolve_revisions(branch1, |
2671 | + if_changed_from=branch2)) |
2672 | + self.assertEqual("source", branch1.url) |
2673 | + |
2674 | def test_substitute(self): |
2675 | source =self.make_branch_and_tree("source") |
2676 | revid1 = source.commit("one") |
2677 | @@ -737,13 +959,20 @@ |
2678 | resolve_revisions(branch1) |
2679 | self.assertEqual("{debupstream}-2", branch1.deb_version) |
2680 | |
2681 | - |
2682 | -class BuildManifestTests(TestCaseInTempDir): |
2683 | + def test_subsitute_not_fully_expanded(self): |
2684 | + source =self.make_branch_and_tree("source") |
2685 | + source.commit("one") |
2686 | + source.commit("two") |
2687 | + branch1 = BaseRecipeBranch("source", "{revno:packaging}", 0.2) |
2688 | + self.assertRaises(errors.BzrCommandError, resolve_revisions, branch1) |
2689 | + |
2690 | + |
2691 | +class StringifyTests(TestCaseInTempDir): |
2692 | |
2693 | def test_simple_manifest(self): |
2694 | base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
2695 | base_branch.revid = "base_revid" |
2696 | - manifest = build_manifest(base_branch) |
2697 | + manifest = str(base_branch) |
2698 | self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" |
2699 | "base_url revid:base_revid\n", manifest) |
2700 | |
2701 | @@ -759,7 +988,7 @@ |
2702 | merged_branch = RecipeBranch("merged", "merged_url") |
2703 | merged_branch.revid = "merged_revid" |
2704 | base_branch.merge_branch(merged_branch) |
2705 | - manifest = build_manifest(base_branch) |
2706 | + manifest = str(base_branch) |
2707 | self.assertEqual("# bzr-builder format 0.2 deb-version 2\n" |
2708 | "base_url revid:base_revid\n" |
2709 | "nest nested1 nested1_url nested revid:nested1_revid\n" |
2710 | @@ -770,11 +999,40 @@ |
2711 | base_branch = BaseRecipeBranch("base_url", "1", 0.2) |
2712 | base_branch.revid = "base_revid" |
2713 | base_branch.run_command("touch test") |
2714 | - manifest = build_manifest(base_branch) |
2715 | + manifest = str(base_branch) |
2716 | self.assertEqual("# bzr-builder format 0.2 deb-version 1\n" |
2717 | "base_url revid:base_revid\n" |
2718 | "run touch test\n", manifest) |
2719 | |
2720 | + def test_recipe_with_no_revspec(self): |
2721 | + base_branch = BaseRecipeBranch("base_url", "1", 0.1) |
2722 | + manifest = str(base_branch) |
2723 | + self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" |
2724 | + "base_url\n", manifest) |
2725 | + |
2726 | + def test_recipe_with_tag_revspec(self): |
2727 | + base_branch = BaseRecipeBranch("base_url", "1", 0.1, |
2728 | + revspec="tag:foo") |
2729 | + manifest = str(base_branch) |
2730 | + self.assertEqual("# bzr-builder format 0.1 deb-version 1\n" |
2731 | + "base_url tag:foo\n", manifest) |
2732 | + |
2733 | + def test_recipe_with_child(self): |
2734 | + base_branch = BaseRecipeBranch("base_url", "2", 0.2) |
2735 | + nested_branch1 = RecipeBranch("nested1", "nested1_url", |
2736 | + revspec="tag:foo") |
2737 | + base_branch.nest_branch("nested", nested_branch1) |
2738 | + nested_branch2 = RecipeBranch("nested2", "nested2_url") |
2739 | + nested_branch1.nest_branch("nested2", nested_branch2) |
2740 | + merged_branch = RecipeBranch("merged", "merged_url") |
2741 | + base_branch.merge_branch(merged_branch) |
2742 | + manifest = str(base_branch) |
2743 | + self.assertEqual("# bzr-builder format 0.2 deb-version 2\n" |
2744 | + "base_url\n" |
2745 | + "nest nested1 nested1_url nested tag:foo\n" |
2746 | + " nest nested2 nested2_url nested2\n" |
2747 | + "merge merged merged_url\n", manifest) |
2748 | + |
2749 | |
2750 | class RecipeBranchTests(TestCaseInTempDir): |
2751 | |
2752 | @@ -817,6 +1075,14 @@ |
2753 | base_branch.substitute_time(time) |
2754 | self.assertEqual("1-197001010000", base_branch.deb_version) |
2755 | |
2756 | + def test_substitute_date(self): |
2757 | + time = datetime.datetime.utcfromtimestamp(1) |
2758 | + base_branch = BaseRecipeBranch("base_url", "1-{date}", 0.2) |
2759 | + base_branch.substitute_time(time) |
2760 | + self.assertEqual("1-19700101", base_branch.deb_version) |
2761 | + base_branch.substitute_time(time) |
2762 | + self.assertEqual("1-19700101", base_branch.deb_version) |
2763 | + |
2764 | def test_substitute_revno(self): |
2765 | base_branch = BaseRecipeBranch("base_url", "1", 0.2) |
2766 | base_branch.substitute_revno(None, None) |
2767 | @@ -838,3 +1104,17 @@ |
2768 | self.assertEqual("3", base_branch.deb_version) |
2769 | base_branch.substitute_revno("foo", lambda: "3") |
2770 | self.assertEqual("3", base_branch.deb_version) |
2771 | + |
2772 | + def test_list_branch_names(self): |
2773 | + base_branch = BaseRecipeBranch("base_url", "1", 0.2) |
2774 | + base_branch.merge_branch(RecipeBranch("merged", "merged_url")) |
2775 | + nested_branch = RecipeBranch("nested", "nested_url") |
2776 | + nested_branch.merge_branch( |
2777 | + RecipeBranch("merged_into_nested", "another_url")) |
2778 | + base_branch.nest_branch("subdir", nested_branch) |
2779 | + base_branch.merge_branch( |
2780 | + RecipeBranch("another_nested", "yet_another_url")) |
2781 | + base_branch.run_command("a command") |
2782 | + self.assertEqual( |
2783 | + ["merged", "nested", "merged_into_nested", "another_nested"], |
2784 | + base_branch.list_branch_names()) |
This fixes bug 479705. It allows a merge line of a builder recipe to specify a subdirectory to merge, rather than the whole named branch. The obvious application of this is to allow merging just the debian/ directory from a branch that contains more than just a debian/ directory.