SubwayTooter-Android-App/dependencyJson.pl

419 lines
13 KiB
Perl
Raw Normal View History

2024-03-17 11:05:30 +01:00
#!/usr/bin/perl --
# - カレントディレクトリで./gradlew :app:dependencies して依存関係を列挙する
# - ユーザフォルダの.gradle/ にあるpomファイルを探索する
# - 依存関係とpomファイルを突き合わせて json を出力する
use 5.32.1;
use strict;
use warnings;
use Getopt::Long;
use File::Basename;
use File::Find;
use File::Path qw(make_path remove_tree);
use File::Copy;
use JSON5;
use JSON::XS;
use Types::Serialiser;
use constant{
true =>Types::Serialiser::true,
false =>Types::Serialiser::false,
};
use XML::XPath;
use XML::XPath::XMLParser;
use Data::Dump qw(dump);
use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use LWP::UserAgent;
my $ua = LWP::UserAgent->new(timeout => 10);
$ua->env_proxy;
sub loadFile($){
my($file)=@_;
open(my $fh,"<:raw",$file) or die "$! $file";
local $/ = undef;
my $data = <$fh>;
close($fh) or die "$! $file";
return $data;
}
# 出力フォルダがなければ作る
sub prepareDirectory($){
my($dir)=@_;
return if -d $dir;
make_path($dir) or die "can't create directory. $dir";
}
#####################################################
# オプション解析、値の検証、出力フォルダの作成
# 設定ファイル
my $configFile = "config/dependencyJsonConfig.json5";
GetOptions ("configFile=s" => \$configFile) or die("bad options.\n");
my $config = decode_json5(loadFile $configFile);
# ライブラリのライセンス情報
my $initialLicenses = $config->{licenses}
or die "config.initialLicenses is missing.";
# POM解析の検証用データ
# - POMのXML解析時に取得漏れがあればエラーとしたい
# - しかしXMLに元々情報がない場合はエラーを出したくない
# - なので情報がないライブラリを列挙しておく
# 以下のライブラリはpomにDevelopersがなくても許容する
my $libsMissingDevelopers = $config->{libsMissingDevelopers}
or die"config.libsMissingDevelopers is missing.";
# 以下のライブラリはpomにライセンス指定がなくても許容する
my $libsMissingLicenses = $config->{libsMissingLicenses}
or die"config.libsMissingLicenses is missing.";
# 以下のライブラリはpomにライセンス名の指定がなくても許容する
my $libsMissingLicenseName = $config->{libsMissingLicenseName}
or die"config.libsMissingLicenseName is missing.";
# 以下のライブラリはpomにWebサイトがなくても許容する
my $libsMissingWebSite = $config->{libsMissingWebSite}
or die"config.libsMissingWebSite is missing.";
# idがprefixesリストのいずれかに前方一致するなら真
sub matchLibs($$){
my($id,$prefixes)=@_;
for my $prefix(@$prefixes){
return true if $id =~/\A$prefix/;
}
return false;
}
# デバッグ用。指定があればそのフォルダにpomファイルをコピーする。
my $pomDir = $config->{pomDumpDir};
$pomDir and prepareDirectory( $pomDir );
# pomのメタ情報を読む
sub readPomInfo($$){
my($name, $xp)=@_;
my $groupId = $xp->findvalue('/project/groupId')->value()
|| $xp->findvalue('/project/parent/groupId')->value()
|| die "missing groupId in $name";
my $artifactId = $xp->findvalue('/project/artifactId')->value()
|| $xp->findvalue('/project/parent/artifactId')->value()
|| die "missing artifactId in $name";
my $version = $xp->findvalue('/project/version')->value()
|| $xp->findvalue('/project/parent/version')->value()
|| die "missing version in $name";
return {
groupId => $groupId,
artifactId => $artifactId,
version => $version,
#
fullName => "$groupId:$artifactId:$version",
groupAndArtifact => "$groupId:$artifactId",
};
}
# pomを読んで出力用データに変換する
sub parsePom($$){
my($errors,$found) = @_;
my $pomInfo = $found->{pomInfo};
my $id = $found->{dep};
# デバッグ用pomファイルをコピーする
# スクリプトから使う訳ではない
if($pomDir){
# idの:を_に変更する
my $idSafe = $id;
$idSafe =~ s/:/_/g;
# ファイルがまだなければコピーする
my $outPomFile = "$pomDir/$idSafe.pom";
-e $outPomFile or copy($pomInfo->{pomFile}, $outPomFile);
}
my $info = {
id => $id,
artifactVersion => $pomInfo->{version},
};
# xpathを使ってXMLからデータを読む
my $xp = XML::XPath->new(filename => $pomInfo->{pomFile});
my $developers = $info->{developers} = [];
for my $node( $xp->findnodes("/project/developers/developer") ){
my $name = $node->findvalue("name")->value()
|| $node->findvalue("id")->value();
if(not $name){
push @$errors,"[$id]missing developer.name";
next;
}
push @$developers,{ name => $name, };
}
if( not @$developers
and not matchLibs($id,$libsMissingDevelopers)
){
push @$errors,"[$id]missing developers.";
}
my $licenses = $info->{licenses} = [];
for my $node( $xp->findnodes("/project/licenses/license") ){
my $url = $node->findvalue('url')->value();
if(not $url){
push @$errors,"[$id]missing license.url";
next;
}
my $name = $node->findvalue('name')->value();
if( not $name){
if( matchLibs($id,$libsMissingLicenseName) ){
$name = "Unknown license";
}else{
push @$errors,"[$id]missing license.name";
next;
}
}
push @$licenses, { name => $name, url => $url, };
}
if( not @$licenses
and not matchLibs($id,$libsMissingLicenses)
){
push @$errors,"[$id]missing licenses.";
}
my $name = $xp->findvalue('/project/name')->value();
$name and $info->{name} = $name;
my $description = $xp->findvalue('/project/description')->value();
if($description){
$description =~ s/\A\s+//;
$description =~ s/\s+\z//;
$description and $info->{description} = $description;
}
my $webSite = $info->{website} = $xp->findvalue('/project/url')->value()
|| $xp->findvalue('/project/scm/url')->value();
if($webSite){
$info->{website} = $webSite;
}elsif( not matchLibs($id,$libsMissingWebSite) ){
push @$errors,"[$id]missing website.";
}
return $info;
}
# aarファイルにはpom.xmlが含まれないのでmvnコマンドで取得する。
sub downloadPom($){
my($dep)=@_;
# ダウンロードしたjarの保存フォルダ
my $dirDlSave = ".depCheck/download";
prepareDirectory( $dirDlSave );
# ダウンロードしたファイル
my $file = "$dirDlSave/$dep.pom";
$file =~ s/:/_/g;
if( not -f $file){
say "downloading pom for $dep";
$dep =~ m|^([^:]+):([^:]+):([^:]+)$|;
my($groupId,$artifactId,$version)=($1,$2,$3);
my $groupIdSlashed = $groupId;
$groupIdSlashed =~ s|\.|/|g;
my $successResponse;
my @errorResponses;
for my $repo(@{$config->{repos}}){
my $url = "$repo/$groupIdSlashed/$artifactId/$version/$artifactId-$version.pom";
my $response = $ua->get($url);
if( $response->is_success) {
$successResponse = $response;
last;
}else{
push @errorResponses,$response;
}
}
if(!$successResponse){
for(@errorResponses){
say $_->status_line ," ", $_->request->uri;
}
die "can't download $dep.";
}
open(my $fh,">:raw",$file) or die "$! $file";
print $fh $successResponse->content;
close($fh) or die "$! $file";
}
my $xp = XML::XPath->new(filename => $file);
my $pomInfo = readPomInfo($file,$xp);
$pomInfo->{pomFile} = $file;
return $pomInfo;
}
# gradleで依存関係を列挙する
sub listingDependencies($){
my($configuration)=@_;
my $cmd = "./gradlew -q --no-configuration-cache :app:dependencies --configuration $configuration";
say $cmd;
open(my $fh,"-|",$cmd) or die "failed to get dependencies: $!";
my %deps;
while(<$fh>){
s/[\x0d\x0a]+//;
s/\s+\z//;
# 依存関係は5文字単位でインデントされる
next if not s/\A[ \\|+-]{5,}//;
# 子プロジェクトは対象外
next if /^project :/;
# 末尾の注釈を除去
s/\s*\([c*]\)$//;
# "->" の対応:バージョンのみが変わる場合
s/([^ :]+?) -> ([^ :]+?)$/$2/;
# "->" の対応:パッケージごと変わる場合
s/(\S+?) -> (\S+?)$/$2/;
$_ and $deps{$_} = 1;
}
close($fh) or die "failed to get dependencies: $!";
my $depsCount = 0+(keys %deps);
$depsCount or die "ERROR: dependencies not found!";
say "$depsCount dependencies found.";
return \%deps;
}
# 依存関係とpomを照合してライブラリ毎の出力データを読み取る
sub mergeDepsAndPoms($){
my($depMap)=@_;
# 依存関係とpomを照合して @founds と @missings に分類する
my @founds;
for my $dep (sort keys %$depMap){
my $pomInfo = downloadPom($dep);
push @founds, {
dep => $dep,
pomInfo=>$pomInfo,
}
}
# pomのパース
my @errors;
my @info = map{ parsePom(\@errors, $_) } @founds;
if(@errors){
say $_ for @errors;
exit 1;
}
my $size = 0 + @info;
say "$size library information parsed.";
return \@info;
}
# @$licenses の要素でURLがマッチするものを返す
sub findLisenceByUrl($$){
my($licenses,$url) = @_;
for( @$licenses){
return $_ if grep{ $_ eq $url } @{$_->{urls}};
}
return;
}
# ライセンスのshortNameを返す
# @$licensesにデータがなければ追加する
sub licenseShortName($$){
my($licenses,$json)=@_;
my($item) = findLisenceByUrl($licenses,$json->{url});
if(not $item){
$item = {
shortName => $json->{name},
name => $json->{name},
urls =>[ $json->{url} ],
};
push @$licenses,$item;
}
return $item->{shortName};
}
# ライセンス情報をまとめる
sub compactLisences($$){
my($initialLicenseList,$libs)=@_;
# 変更するライセンスリスト
# ディープコピーする
my $licenses = decode_json encode_json $initialLicenseList;
# ライブラリごとにライセンスのリストがあるので、それをshortNameのリストに変換する
for my $lib (@$libs){
@{$lib->{licenses}} = map{ licenseShortName($licenses,$_) } @{$lib->{licenses}};
}
# 出力結果の並び順を安定させるため、ライセンス一覧をshortNameでソートする
@$licenses = sort {$a->{shortName} cmp $b->{shortName} } @$licenses;
say "licenses:";
for(@$licenses){
my $url = $_->{urls}[0];
say " [$_->{shortName}] name='$_->{name}' url=$url";
}
return $licenses;
}
# 情報をJSONファイルに出力
sub outputDepJson($$$){
my($outFile,$libs,$licences)=@_;
open(my $fh,">:raw",$outFile) or die "$outFile $!";
print $fh encode_json {
libs => $libs,
licenses => $licences,
};
close($fh) or die "$outFile $!";
}
##################################################
# - 出力ファイルごとの処理
# - ただしGradleキャッシュのスキャンは1回だけ
my $outputs = $config->{outputs} or die "contif.outputs is missing.";
@$outputs or die "contif.outputs is empty.";
# validation
my $outIndex = 0;
for my $out (@$outputs){
my $name = $out->{name} or die "config.outputs[$outIndex].name is missing.";
$out->{outFile} or die "config.outputs[$name].outFile is missing.";
$out->{configuration} or die "config.outputs[$name].configuration is missing.";
prepareDirectory( dirname($out->{outFile}) );
# gradleで依存関係を列挙する
say "# [$name] listing dependencies ...";
$out->{deps} = listingDependencies $out->{configuration};
# 依存関係とpomを照合してライブラリ毎の出力データを読み取る
say "# [$name] read lib data from dependencies and pom data.";
my $libs = mergeDepsAndPoms($out->{deps});
# 追加の依存関係
my $addItems = decode_json encode_json $config->{additionalLibs};
@$libs = ( @$addItems , @$libs );
# ライセンス情報をまとめる
say "# [$name] compacting licenses ...";
my $licenses = compactLisences($initialLicenses,$libs);
# 情報をJSONファイルに出力
say "# [$name] save to json $out->{outFile}";
outputDepJson($out->{outFile},$libs,$licenses);
++$outIndex;
}
say "complete!!";