419 lines
13 KiB
Perl
419 lines
13 KiB
Perl
|
#!/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!!";
|