| #!/usr/bin/perl -w | |
| =head1 NAME | |
| relicense.pl | |
| =head1 SYNOPSIS | |
| relicense.pl [options] -p <permissions file> <file> [<file>...] | |
| Option: | |
| -p,--permitted=FILE Specify file of emails with relicensing permission | |
| -f,--force Manually force relicensing | |
| -h,--help Display brief help message | |
| -v,--verbose Increase verbosity | |
| -q,--quiet Decrease verbosity | |
| =cut | |
| use File::Slurp; | |
| use IPC::Run qw ( run ); | |
| use Getopt::Long; | |
| use Pod::Usage; | |
| use strict; | |
| use warnings; | |
| # Parse command-line options | |
| my $verbosity = 0; | |
| my $permfile; | |
| my $force; | |
| Getopt::Long::Configure ( "bundling", "auto_abbrev" ); | |
| GetOptions ( | |
| 'permitted|p=s' => \$permfile, | |
| 'force|f' => \$force, | |
| 'verbose|v+' => sub { $verbosity++; }, | |
| 'quiet|q+' => sub { $verbosity--; }, | |
| 'help|h' => sub { pod2usage ( 1 ); }, | |
| ) or die "Could not parse command-line options"; | |
| pod2usage ( 1 ) unless @ARGV; | |
| # Read permitted emails file | |
| my @emails = ( $permfile ? read_file ( $permfile ) : () ); | |
| chomp @emails; | |
| my $permitted = { map { /^.*<(\S+)>$/; ( $1 || $_ ) => 1 } @emails }; | |
| # Define list of relicensable licences | |
| my $relicensable = { | |
| GPL2_OR_LATER => 1, | |
| }; | |
| # Define blurb to be added to copyright notice | |
| my $blurb = ' | |
| * | |
| * You can also choose to distribute this program under the terms of | |
| * the Unmodified Binary Distribution Licence (as given in the file | |
| * COPYING.UBDL), provided that you have satisfied its requirements.'; | |
| # Process files | |
| my @succeeded; | |
| my @failed; | |
| while ( my $filename = shift @ARGV ) { | |
| # Read file to determine existing licence | |
| my $file = read_file ( $filename ); | |
| my @licences = ( $file =~ /^\s*FILE_LICENCE\s*\(\s*(\S+)\s*\)\s*;?$/mg ); | |
| die "No licence declaration in $filename\n" unless @licences; | |
| die "Multiple licence declarations in $filename\n" if @licences > 1; | |
| my $licence = $licences[0]; | |
| # Skip if file is already UBDL-licensed | |
| next if $licence =~ /_OR_UBDL$/; | |
| # Fail immediately if file is not a candidate for relicensing | |
| if ( ! exists $relicensable->{$licence} ) { | |
| print "Non-relicensable licence $licence in $filename\n"; | |
| push @failed, $filename; | |
| next; | |
| } | |
| # Run git-blame | |
| my $stdout; | |
| my $stderr; | |
| run [ "git", "blame", "-M", "-C", "-p", "-w", $filename ], | |
| \undef, \$stdout, \$stderr | |
| or die "git-blame $filename: $?"; | |
| die $stderr if $stderr; | |
| # Process output | |
| my @stdout = split ( /\n/, $stdout ); | |
| chomp @stdout; | |
| my $details = {}; | |
| my $failures = 0; | |
| while ( @stdout ) { | |
| # Parse output | |
| my $commit_line = shift @stdout; | |
| ( my $commit, undef, my $lineno, undef, my $count ) = | |
| ( $commit_line =~ | |
| /^([0-9a-f]{40})\s+([0-9]+)\s+([0-9]+)(\s+([0-9]+))?$/ ) | |
| or die "Malformed commit line \"$commit_line\"\n"; | |
| if ( $count ) { | |
| $details->{$commit} ||= {}; | |
| while ( ! ( $stdout[0] =~ /^\t/ ) ) { | |
| my $detail_line = shift @stdout; | |
| ( my $key, undef, my $value ) = | |
| ( $detail_line =~ /^([a-z-]+)(\s+(.+))?$/ ) | |
| or die "Malformed detail line \"$detail_line\" for $commit_line\n"; | |
| $details->{$commit}->{$key} = $value; | |
| } | |
| } | |
| die "Missing commit details for $commit_line\n" | |
| unless %{$details->{$commit}}; | |
| my $code_line = shift @stdout; | |
| ( my $line ) = ( $code_line =~ /^\t(.*)$/ ) | |
| or die "Malformed code line \"$code_line\" for $commit_line\n"; | |
| # Skip trivial lines and lines so common that they are likely to | |
| # be misattributed by git-blame | |
| next if $line =~ /^\s*$/; # Empty lines | |
| next if $line =~ /^\s*\/\*/; # Start of comments | |
| next if $line =~ /^\s*\*/; # Middle (or end) of comments | |
| next if $line =~ /^\s*\{\s*$/; # Standalone opening braces | |
| next if $line =~ /^\s*\};?\s*$/; # Standalone closing braces | |
| next if $line =~ /^\#include/; # Header inclusions | |
| next if $line =~ /^\s*return\s+0;/; # return 0; | |
| next if $line =~ /^\s*return\s+rc;/; # return rc; | |
| next if $line =~ /^\s*PCI_ROM\s*\(.*\)\s*,\s*$/; # PCI IDs | |
| next if $line =~ /^\s*FILE_LICENCE\s*\(.*\)\s*;$/; # Licence declarations | |
| # Identify author | |
| my $author_mail = $details->{$commit}->{"author-mail"} | |
| or die "Missing author email for $commit_line\n"; | |
| ( my $email ) = ( $author_mail =~ /^<(\S+)>$/ ) | |
| or die "Malformed author email \"$author_mail\" for $commit_line\n"; | |
| undef $email if exists $details->{$commit}->{boundary}; | |
| # Check for relicensing permission | |
| next if defined $email && exists $permitted->{$email}; | |
| # Print out lines lacking permission | |
| printf $filename."\n" unless $failures; | |
| printf "%4d %-30s %s\n", $lineno, ( $email || "<root>" ), $line; | |
| $failures++; | |
| } | |
| # Fail if there are any non-trivial lines lacking relicensing permission | |
| if ( $failures && ! $force ) { | |
| push @failed, $filename; | |
| next; | |
| } | |
| # Modify FILE_LICENCE() line | |
| $file =~ s/(^\s*FILE_LICENCE\s*\(\s*${licence})(\s*\)\s*;?$)/$1_OR_UBDL$2/m | |
| or die "Could not modify FILE_LICENCE() in $filename\n"; | |
| # Modify copyright notice, if present | |
| if ( $file =~ /GNU General Public License/i ) { | |
| $file =~ s/(02110-1301, USA.$)/$1${blurb}/m | |
| or die "Could not modify copyright notice in $filename\n"; | |
| } | |
| # Write out modified file | |
| write_file ( $filename, { atomic => 1 }, $file ); | |
| push @succeeded, $filename; | |
| } | |
| print "Relicensed: ".join ( " ", @succeeded )."\n" if @succeeded; | |
| die "Cannot relicense: ".join ( " ", @failed )."\n" if @failed; |