File: | Dpkg/Source/Quilt.pm |
Coverage: | 29.4% |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | # Copyright © 2008-2012 Raphaël Hertzog <hertzog@debian.org> | ||||||
2 | # | ||||||
3 | # This program is free software; you can redistribute it and/or modify | ||||||
4 | # it under the terms of the GNU General Public License as published by | ||||||
5 | # the Free Software Foundation; either version 2 of the License, or | ||||||
6 | # (at your option) any later version. | ||||||
7 | # | ||||||
8 | # This program is distributed in the hope that it will be useful, | ||||||
9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||||
11 | # GNU General Public License for more details. | ||||||
12 | # | ||||||
13 | # You should have received a copy of the GNU General Public License | ||||||
14 | # along with this program. If not, see <https://www.gnu.org/licenses/>. | ||||||
15 | |||||||
16 | package Dpkg::Source::Quilt; | ||||||
17 | |||||||
18 | 1 1 1 | 2 1 11 | use strict; | ||||
19 | 1 1 1 | 1 1 28 | use warnings; | ||||
20 | |||||||
21 | our $VERSION = '0.02'; | ||||||
22 | |||||||
23 | 1 1 1 | 2 0 28 | use List::Util qw(any none); | ||||
24 | 1 1 1 | 1 1 7 | use File::Spec; | ||||
25 | 1 1 1 | 146 1901 22 | use File::Copy; | ||||
26 | 1 1 1 | 3 0 20 | use File::Find; | ||||
27 | 1 1 1 | 1 1 17 | use File::Path qw(make_path); | ||||
28 | 1 1 1 | 1 1 18 | use File::Basename; | ||||
29 | |||||||
30 | 1 1 1 | 118 0 25 | use Dpkg::Gettext; | ||||
31 | 1 1 1 | 121 1 30 | use Dpkg::ErrorHandling; | ||||
32 | 1 1 1 | 110 1 19 | use Dpkg::File; | ||||
33 | 1 1 1 | 156 1 43 | use Dpkg::Source::Patch; | ||||
34 | 1 1 1 | 4 1 25 | use Dpkg::Source::Functions qw(erasedir chmod_if_needed fs_time); | ||||
35 | 1 1 1 | 173 0 985 | use Dpkg::Vendor qw(get_current_vendor); | ||||
36 | |||||||
37 | sub new { | ||||||
38 | 1 | 0 | 2 | my ($this, $dir, %opts) = @_; | |||
39 | 1 | 4 | my $class = ref($this) || $this; | ||||
40 | |||||||
41 | 1 | 2 | my $self = { | ||||
42 | dir => $dir, | ||||||
43 | }; | ||||||
44 | 1 | 1 | bless $self, $class; | ||||
45 | |||||||
46 | 1 | 1 | $self->load_series(); | ||||
47 | 1 | 2 | $self->load_db(); | ||||
48 | |||||||
49 | 1 | 2 | return $self; | ||||
50 | } | ||||||
51 | |||||||
52 | sub setup_db { | ||||||
53 | 0 | 0 | 0 | my $self = shift; | |||
54 | 0 | 0 | my $db_dir = $self->get_db_file(); | ||||
55 | 0 | 0 | if (not -d $db_dir) { | ||||
56 | 0 | 0 | mkdir $db_dir or syserr(g_('cannot mkdir %s'), $db_dir); | ||||
57 | } | ||||||
58 | 0 | 0 | my $file = $self->get_db_file('.version'); | ||||
59 | 0 | 0 | if (not -e $file) { | ||||
60 | 0 | 0 | file_dump($file, "2\n"); | ||||
61 | } | ||||||
62 | # The files below are used by quilt to know where patches are stored | ||||||
63 | # and what file contains the patch list (supported by quilt >= 0.48-5 | ||||||
64 | # in Debian). | ||||||
65 | 0 | 0 | $file = $self->get_db_file('.quilt_patches'); | ||||
66 | 0 | 0 | if (not -e $file) { | ||||
67 | 0 | 0 | file_dump($file, "debian/patches\n"); | ||||
68 | } | ||||||
69 | 0 | 0 | $file = $self->get_db_file('.quilt_series'); | ||||
70 | 0 | 0 | if (not -e $file) { | ||||
71 | 0 | 0 | my $series = $self->get_series_file(); | ||||
72 | 0 | 0 | $series = (File::Spec->splitpath($series))[2]; | ||||
73 | 0 | 0 | file_dump($file, "$series\n"); | ||||
74 | } | ||||||
75 | } | ||||||
76 | |||||||
77 | sub load_db { | ||||||
78 | 1 | 0 | 52 | my $self = shift; | |||
79 | |||||||
80 | 1 | 1 | my $pc_applied = $self->get_db_file('applied-patches'); | ||||
81 | 1 | 1 | $self->{applied_patches} = [ $self->read_patch_list($pc_applied) ]; | ||||
82 | } | ||||||
83 | |||||||
84 | sub save_db { | ||||||
85 | 0 | 0 | 0 | my $self = shift; | |||
86 | |||||||
87 | 0 | 0 | $self->setup_db(); | ||||
88 | 0 | 0 | my $pc_applied = $self->get_db_file('applied-patches'); | ||||
89 | 0 | 0 | $self->write_patch_list($pc_applied, $self->{applied_patches}); | ||||
90 | } | ||||||
91 | |||||||
92 | sub load_series { | ||||||
93 | 1 | 0 | 2 | my ($self, %opts) = @_; | |||
94 | |||||||
95 | 1 | 1 | my $series = $self->get_series_file(); | ||||
96 | 1 | 2 | $self->{series} = [ $self->read_patch_list($series, %opts) ]; | ||||
97 | } | ||||||
98 | |||||||
99 | sub series { | ||||||
100 | 1 | 0 | 1 | my $self = shift; | |||
101 | 1 1 | 1 1 | return @{$self->{series}}; | ||||
102 | } | ||||||
103 | |||||||
104 | sub applied { | ||||||
105 | 0 | 0 | 0 | my $self = shift; | |||
106 | 0 0 | 0 0 | return @{$self->{applied_patches}}; | ||||
107 | } | ||||||
108 | |||||||
109 | sub top { | ||||||
110 | 0 | 0 | 0 | my $self = shift; | |||
111 | 0 0 | 0 0 | my $count = scalar @{$self->{applied_patches}}; | ||||
112 | 0 | 0 | return $self->{applied_patches}[$count - 1] if $count; | ||||
113 | 0 | 0 | return; | ||||
114 | } | ||||||
115 | |||||||
116 | sub register { | ||||||
117 | 0 | 0 | 0 | my ($self, $patch_name) = @_; | |||
118 | |||||||
119 | 0 0 0 | 0 0 0 | return if any { $_ eq $patch_name } @{$self->{series}}; | ||||
120 | |||||||
121 | # Add patch to series files. | ||||||
122 | 0 | 0 | $self->setup_db(); | ||||
123 | 0 | 0 | $self->_file_add_line($self->get_series_file(), $patch_name); | ||||
124 | 0 | 0 | $self->_file_add_line($self->get_db_file('applied-patches'), $patch_name); | ||||
125 | 0 | 0 | $self->load_db(); | ||||
126 | 0 | 0 | $self->load_series(); | ||||
127 | |||||||
128 | # Ensure quilt meta-data is created and in sync with some trickery: | ||||||
129 | # Reverse-apply the patch, drop .pc/$patch, and re-apply it with the | ||||||
130 | # correct options to recreate the backup files. | ||||||
131 | 0 | 0 | $self->pop(reverse_apply => 1); | ||||
132 | 0 | 0 | $self->push(); | ||||
133 | } | ||||||
134 | |||||||
135 | sub unregister { | ||||||
136 | 0 | 0 | 0 | my ($self, $patch_name) = @_; | |||
137 | |||||||
138 | 0 0 0 | 0 0 0 | return if none { $_ eq $patch_name } @{$self->{series}}; | ||||
139 | |||||||
140 | 0 | 0 | my $series = $self->get_series_file(); | ||||
141 | |||||||
142 | 0 | 0 | $self->_file_drop_line($series, $patch_name); | ||||
143 | 0 | 0 | $self->_file_drop_line($self->get_db_file('applied-patches'), $patch_name); | ||||
144 | 0 | 0 | erasedir($self->get_db_file($patch_name)); | ||||
145 | 0 | 0 | $self->load_db(); | ||||
146 | 0 | 0 | $self->load_series(); | ||||
147 | |||||||
148 | # Clean up empty series. | ||||||
149 | 0 | 0 | unlink $series if -z $series; | ||||
150 | } | ||||||
151 | |||||||
152 | sub next { | ||||||
153 | 0 | 0 | 0 | my $self = shift; | |||
154 | 0 0 | 0 0 | my $count_applied = scalar @{$self->{applied_patches}}; | ||||
155 | 0 0 | 0 0 | my $count_series = scalar @{$self->{series}}; | ||||
156 | 0 | 0 | return $self->{series}[$count_applied] if ($count_series > $count_applied); | ||||
157 | 0 | 0 | return; | ||||
158 | } | ||||||
159 | |||||||
160 | sub push { | ||||||
161 | 0 | 0 | 0 | my ($self, %opts) = @_; | |||
162 | 0 | 0 | $opts{verbose} //= 0; | ||||
163 | 0 | 0 | $opts{timestamp} //= fs_time($self->{dir}); | ||||
164 | |||||||
165 | 0 | 0 | my $patch = $self->next(); | ||||
166 | 0 | 0 | return unless defined $patch; | ||||
167 | |||||||
168 | 0 | 0 | my $path = $self->get_patch_file($patch); | ||||
169 | 0 | 0 | my $obj = Dpkg::Source::Patch->new(filename => $path); | ||||
170 | |||||||
171 | 0 | 0 | info(g_('applying %s'), $patch) if $opts{verbose}; | ||||
172 | 0 | 0 | eval { | ||||
173 | $obj->apply($self->{dir}, timestamp => $opts{timestamp}, | ||||||
174 | verbose => $opts{verbose}, | ||||||
175 | 0 | 0 | force_timestamp => 1, create_dirs => 1, remove_backup => 0, | ||||
176 | options => [ '-t', '-F', '0', '-N', '-p1', '-u', | ||||||
177 | '-V', 'never', '-E', '-b', | ||||||
178 | '-B', ".pc/$patch/", '--reject-file=-' ]); | ||||||
179 | }; | ||||||
180 | 0 | 0 | if ($@) { | ||||
181 | 0 | 0 | info(g_('the patch has fuzz which is not allowed, or is malformed')); | ||||
182 | 0 | 0 | info(g_("if patch '%s' is correctly applied by quilt, use '%s' to update it"), | ||||
183 | $patch, 'quilt refresh'); | ||||||
184 | 0 | 0 | info(g_('if the file is present in the unpacked source, make sure it ' . | ||||
185 | 'is also present in the orig tarball')); | ||||||
186 | 0 | 0 | $self->restore_quilt_backup_files($patch, %opts); | ||||
187 | 0 | 0 | erasedir($self->get_db_file($patch)); | ||||
188 | 0 | 0 | die $@; | ||||
189 | } | ||||||
190 | 0 0 | 0 0 | CORE::push @{$self->{applied_patches}}, $patch; | ||||
191 | 0 | 0 | $self->save_db(); | ||||
192 | } | ||||||
193 | |||||||
194 | sub pop { | ||||||
195 | 0 | 0 | 0 | my ($self, %opts) = @_; | |||
196 | 0 | 0 | $opts{verbose} //= 0; | ||||
197 | 0 | 0 | $opts{timestamp} //= fs_time($self->{dir}); | ||||
198 | 0 | 0 | $opts{reverse_apply} //= 0; | ||||
199 | |||||||
200 | 0 | 0 | my $patch = $self->top(); | ||||
201 | 0 | 0 | return unless defined $patch; | ||||
202 | |||||||
203 | 0 | 0 | info(g_('unapplying %s'), $patch) if $opts{verbose}; | ||||
204 | 0 | 0 | my $backup_dir = $self->get_db_file($patch); | ||||
205 | 0 | 0 | if (-d $backup_dir and not $opts{reverse_apply}) { | ||||
206 | # Use the backup copies to restore | ||||||
207 | 0 | 0 | $self->restore_quilt_backup_files($patch); | ||||
208 | } else { | ||||||
209 | # Otherwise reverse-apply the patch | ||||||
210 | 0 | 0 | my $path = $self->get_patch_file($patch); | ||||
211 | 0 | 0 | my $obj = Dpkg::Source::Patch->new(filename => $path); | ||||
212 | |||||||
213 | $obj->apply($self->{dir}, timestamp => $opts{timestamp}, | ||||||
214 | 0 | 0 | verbose => 0, force_timestamp => 1, remove_backup => 0, | ||||
215 | options => [ '-R', '-t', '-N', '-p1', | ||||||
216 | '-u', '-V', 'never', '-E', | ||||||
217 | '--no-backup-if-mismatch' ]); | ||||||
218 | } | ||||||
219 | |||||||
220 | 0 | 0 | erasedir($backup_dir); | ||||
221 | 0 0 | 0 0 | pop @{$self->{applied_patches}}; | ||||
222 | 0 | 0 | $self->save_db(); | ||||
223 | } | ||||||
224 | |||||||
225 | sub get_db_version { | ||||||
226 | 0 | 0 | 0 | my $self = shift; | |||
227 | 0 | 0 | my $pc_ver = $self->get_db_file('.version'); | ||||
228 | 0 | 0 | if (-f $pc_ver) { | ||||
229 | 0 | 0 | my $version = file_slurp($pc_ver); | ||||
230 | 0 | 0 | chomp $version; | ||||
231 | 0 | 0 | return $version; | ||||
232 | } | ||||||
233 | 0 | 0 | return; | ||||
234 | } | ||||||
235 | |||||||
236 | sub find_problems { | ||||||
237 | 0 | 0 | 0 | my $self = shift; | |||
238 | 0 | 0 | my $patch_dir = $self->get_patch_file(); | ||||
239 | 0 | 0 | if (-e $patch_dir and not -d _) { | ||||
240 | 0 | 0 | return sprintf(g_('%s should be a directory or non-existing'), $patch_dir); | ||||
241 | } | ||||||
242 | 0 | 0 | my $series = $self->get_series_file(); | ||||
243 | 0 | 0 | if (-e $series and not -f _) { | ||||
244 | 0 | 0 | return sprintf(g_('%s should be a file or non-existing'), $series); | ||||
245 | } | ||||||
246 | 0 | 0 | return; | ||||
247 | } | ||||||
248 | |||||||
249 | sub get_series_file { | ||||||
250 | 1 | 0 | 1 | my $self = shift; | |||
251 | 1 | 2 | my $vendor = lc(get_current_vendor() || 'debian'); | ||||
252 | # Series files are stored alongside patches | ||||||
253 | 1 | 2 | my $default_series = $self->get_patch_file('series'); | ||||
254 | 1 | 7 | my $vendor_series = $self->get_patch_file("$vendor.series"); | ||||
255 | 1 | 13 | return $vendor_series if -e $vendor_series; | ||||
256 | 1 | 1 | return $default_series; | ||||
257 | } | ||||||
258 | |||||||
259 | sub get_db_file { | ||||||
260 | 1 | 0 | 1 | my $self = shift; | |||
261 | 1 | 5 | return File::Spec->catfile($self->{dir}, '.pc', @_); | ||||
262 | } | ||||||
263 | |||||||
264 | sub get_db_dir { | ||||||
265 | 0 | 0 | 0 | my $self = shift; | |||
266 | 0 | 0 | return $self->get_db_file(); | ||||
267 | } | ||||||
268 | |||||||
269 | sub get_patch_file { | ||||||
270 | 2 | 0 | 2 | my $self = shift; | |||
271 | 2 | 12 | return File::Spec->catfile($self->{dir}, 'debian', 'patches', @_); | ||||
272 | } | ||||||
273 | |||||||
274 | sub get_patch_dir { | ||||||
275 | 0 | 0 | 0 | my $self = shift; | |||
276 | 0 | 0 | return $self->get_patch_file(); | ||||
277 | } | ||||||
278 | |||||||
279 | ## METHODS BELOW ARE INTERNAL ## | ||||||
280 | |||||||
281 | sub _file_load { | ||||||
282 | 0 | 0 | my ($self, $file) = @_; | ||||
283 | |||||||
284 | 0 | 0 | open my $file_fh, '<', $file or syserr(g_('cannot read %s'), $file); | ||||
285 | 0 | 0 | my @lines = <$file_fh>; | ||||
286 | 0 | 0 | close $file_fh; | ||||
287 | |||||||
288 | 0 | 0 | return @lines; | ||||
289 | } | ||||||
290 | |||||||
291 | sub _file_add_line { | ||||||
292 | 0 | 0 | my ($self, $file, $line) = @_; | ||||
293 | |||||||
294 | 0 | 0 | my @lines; | ||||
295 | 0 | 0 | @lines = $self->_file_load($file) if -f $file; | ||||
296 | 0 | 0 | CORE::push @lines, $line; | ||||
297 | 0 | 0 | chomp @lines; | ||||
298 | |||||||
299 | 0 | 0 | open my $file_fh, '>', $file or syserr(g_('cannot write %s'), $file); | ||||
300 | 0 0 | 0 0 | print { $file_fh } "$_\n" foreach @lines; | ||||
301 | 0 | 0 | close $file_fh; | ||||
302 | } | ||||||
303 | |||||||
304 | sub _file_drop_line { | ||||||
305 | 0 | 0 | my ($self, $file, $re) = @_; | ||||
306 | |||||||
307 | 0 | 0 | my @lines = $self->_file_load($file); | ||||
308 | 0 | 0 | open my $file_fh, '>', $file or syserr(g_('cannot write %s'), $file); | ||||
309 | 0 0 0 | 0 0 0 | print { $file_fh } $_ foreach grep { not /^\Q$re\E\s*$/ } @lines; | ||||
310 | 0 | 0 | close $file_fh; | ||||
311 | } | ||||||
312 | |||||||
313 | sub read_patch_list { | ||||||
314 | 2 | 0 | 3 | my ($self, $file, %opts) = @_; | |||
315 | 2 | 14 | return () if not defined $file or not -f $file; | ||||
316 | 1 | 3 | $opts{warn_options} //= 0; | ||||
317 | 1 | 1 | my @patches; | ||||
318 | 1 | 9 | open(my $series_fh, '<' , $file) or syserr(g_('cannot read %s'), $file); | ||||
319 | 1 | 5 | while (defined(my $line = <$series_fh>)) { | ||||
320 | 9 | 6 | chomp $line; | ||||
321 | # Strip leading/trailing spaces | ||||||
322 | 9 | 5 | $line =~ s/^\s+//; | ||||
323 | 9 | 11 | $line =~ s/\s+$//; | ||||
324 | # Strip comment | ||||||
325 | 9 | 10 | $line =~ s/(?:^|\s+)#.*$//; | ||||
326 | 9 | 10 | next unless $line; | ||||
327 | 4 | 6 | if ($line =~ /^(\S+)\s+(.*)$/) { | ||||
328 | 2 | 2 | $line = $1; | ||||
329 | 2 | 2 | if ($2 ne '-p1') { | ||||
330 | warning(g_('the series file (%s) contains unsupported ' . | ||||||
331 | "options ('%s', line %s); dpkg-source might " . | ||||||
332 | 'fail when applying patches'), | ||||||
333 | 0 | 0 | $file, $2, $.) if $opts{warn_options}; | ||||
334 | } | ||||||
335 | } | ||||||
336 | 4 | 3 | if ($line =~ m{(^|/)\.\./}) { | ||||
337 | 0 | 0 | error(g_('%s contains an insecure path: %s'), $file, $line); | ||||
338 | } | ||||||
339 | 4 | 4 | CORE::push @patches, $line; | ||||
340 | } | ||||||
341 | 1 | 3 | close($series_fh); | ||||
342 | 1 | 3 | return @patches; | ||||
343 | } | ||||||
344 | |||||||
345 | sub write_patch_list { | ||||||
346 | 0 | 0 | my ($self, $series, $patches) = @_; | ||||
347 | |||||||
348 | 0 | open my $series_fh, '>', $series or syserr(g_('cannot write %s'), $series); | |||||
349 | 0 0 | foreach my $patch (@{$patches}) { | |||||
350 | 0 0 | print { $series_fh } "$patch\n"; | |||||
351 | } | ||||||
352 | 0 | close $series_fh; | |||||
353 | } | ||||||
354 | |||||||
355 | sub restore_quilt_backup_files { | ||||||
356 | 0 | 0 | my ($self, $patch, %opts) = @_; | ||||
357 | 0 | my $patch_dir = $self->get_db_file($patch); | |||||
358 | 0 | return unless -d $patch_dir; | |||||
359 | 0 | info(g_('restoring quilt backup files for %s'), $patch) if $opts{verbose}; | |||||
360 | find({ | ||||||
361 | no_chdir => 1, | ||||||
362 | wanted => sub { | ||||||
363 | 0 | return if -d; | |||||
364 | 0 | my $relpath_in_srcpkg = File::Spec->abs2rel($_, $patch_dir); | |||||
365 | 0 | my $target = File::Spec->catfile($self->{dir}, $relpath_in_srcpkg); | |||||
366 | 0 | if (-s) { | |||||
367 | 0 | unlink($target); | |||||
368 | 0 | make_path(dirname($target)); | |||||
369 | 0 | unless (link($_, $target)) { | |||||
370 | 0 | copy($_, $target) | |||||
371 | or syserr(g_('failed to copy %s to %s'), $_, $target); | ||||||
372 | 0 | chmod_if_needed((stat _)[2], $target) | |||||
373 | or syserr(g_("unable to change permission of '%s'"), $target); | ||||||
374 | } | ||||||
375 | } else { | ||||||
376 | # empty files are "backups" for new files that patch created | ||||||
377 | 0 | unlink($target); | |||||
378 | } | ||||||
379 | } | ||||||
380 | 0 | }, $patch_dir); | |||||
381 | } | ||||||
382 | |||||||
383 | 1; |