2015-10-30 00:04:31 +00:00
|
|
|
#!/usr/bin/env perl
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
use utf8;
|
2013-06-13 13:39:08 +00:00
|
|
|
use strict;
|
2015-10-30 00:04:31 +00:00
|
|
|
use warnings;
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
use CPAN::Meta();
|
|
|
|
use CPANPLUS::Backend();
|
2023-07-16 08:20:41 +00:00
|
|
|
use MIME::Base64;
|
2020-03-14 05:25:33 +00:00
|
|
|
use Module::CoreList;
|
2015-10-30 00:04:31 +00:00
|
|
|
use Getopt::Long::Descriptive qw( describe_options );
|
|
|
|
use JSON::PP qw( encode_json );
|
|
|
|
use Log::Log4perl qw(:easy);
|
|
|
|
use Readonly();
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
# Readonly hash that maps CPAN style license strings to information
|
|
|
|
# necessary to generate a Nixpkgs style license attribute.
|
|
|
|
Readonly::Hash my %LICENSE_MAP => (
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
# The Perl 5 License (Artistic 1 & GPL 1 or later).
|
|
|
|
perl_5 => {
|
|
|
|
licenses => [qw( artistic1 gpl1Plus )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# GNU Affero General Public License, Version 3.
|
|
|
|
agpl_3 => {
|
|
|
|
licenses => [qw( agpl3Plus )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# Apache Software License, Version 1.1.
|
|
|
|
apache_1_1 => {
|
|
|
|
licenses => ["Apache License 1.1"],
|
|
|
|
in_set => 0
|
|
|
|
},
|
|
|
|
|
|
|
|
# Apache License, Version 2.0.
|
|
|
|
apache_2_0 => {
|
|
|
|
licenses => [qw( asl20 )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# Artistic License, (Version 1).
|
|
|
|
artistic_1 => {
|
|
|
|
licenses => [qw( artistic1 )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# Artistic License, Version 2.0.
|
|
|
|
artistic_2 => {
|
|
|
|
licenses => [qw( artistic2 )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# BSD License (three-clause).
|
|
|
|
bsd => {
|
|
|
|
licenses => [qw( bsd3 )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# FreeBSD License (two-clause).
|
|
|
|
freebsd => {
|
|
|
|
licenses => [qw( bsd2 )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# GNU Free Documentation License, Version 1.2.
|
|
|
|
gfdl_1_2 => {
|
2022-10-27 20:47:29 +00:00
|
|
|
licenses => [qw( fdl12Plus )]
|
2015-10-30 00:04:31 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
# GNU Free Documentation License, Version 1.3.
|
|
|
|
gfdl_1_3 => {
|
2022-10-27 20:48:13 +00:00
|
|
|
licenses => [qw( fdl13Plus )]
|
2015-10-30 00:04:31 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
# GNU General Public License, Version 1.
|
|
|
|
gpl_1 => {
|
|
|
|
licenses => [qw( gpl1Plus )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# GNU General Public License, Version 2. Note, we will interpret
|
|
|
|
# "gpl" alone as GPL v2+.
|
|
|
|
gpl_2 => {
|
|
|
|
licenses => [qw( gpl2Plus )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# GNU General Public License, Version 3.
|
|
|
|
gpl_3 => {
|
|
|
|
licenses => [qw( gpl3Plus )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# GNU Lesser General Public License, Version 2.1. Note, we will
|
|
|
|
# interpret "gpl" alone as LGPL v2.1+.
|
|
|
|
lgpl_2_1 => {
|
|
|
|
licenses => [qw( lgpl21Plus )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# GNU Lesser General Public License, Version 3.0.
|
|
|
|
lgpl_3_0 => {
|
|
|
|
licenses => [qw( lgpl3Plus )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# MIT (aka X11) License.
|
|
|
|
mit => {
|
|
|
|
licenses => [qw( mit )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# Mozilla Public License, Version 1.0.
|
|
|
|
mozilla_1_0 => {
|
|
|
|
licenses => [qw( mpl10 )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# Mozilla Public License, Version 1.1.
|
|
|
|
mozilla_1_1 => {
|
|
|
|
licenses => [qw( mpl11 )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# OpenSSL License.
|
|
|
|
openssl => {
|
|
|
|
licenses => [qw( openssl )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# Q Public License, Version 1.0.
|
|
|
|
qpl_1_0 => {
|
|
|
|
licenses => [qw( qpl )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# Original SSLeay License.
|
|
|
|
ssleay => {
|
|
|
|
licenses => ["Original SSLeay License"],
|
|
|
|
in_set => 0
|
|
|
|
},
|
|
|
|
|
|
|
|
# Sun Internet Standards Source License (SISSL).
|
|
|
|
sun => {
|
|
|
|
licenses => ["Sun Industry Standards Source License v1.1"],
|
|
|
|
in_set => 0
|
|
|
|
},
|
|
|
|
|
|
|
|
# zlib License.
|
|
|
|
zlib => {
|
|
|
|
licenses => [qw( zlib )]
|
|
|
|
},
|
|
|
|
|
|
|
|
# Other Open Source Initiative (OSI) approved license.
|
|
|
|
open_source => {
|
|
|
|
licenses => [qw( free )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# Requires special permission from copyright holder.
|
|
|
|
restricted => {
|
|
|
|
licenses => [qw( unfree )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# Not an OSI approved license, but not restricted. Note, we
|
|
|
|
# currently map this to unfreeRedistributable, which is a
|
|
|
|
# conservative choice.
|
|
|
|
unrestricted => {
|
|
|
|
licenses => [qw( unfreeRedistributable )],
|
|
|
|
amb => 1
|
|
|
|
},
|
|
|
|
|
|
|
|
# License not provided in metadata.
|
|
|
|
unknown => {
|
2020-03-14 04:46:27 +00:00
|
|
|
licenses => [],
|
2015-10-30 00:04:31 +00:00
|
|
|
amb => 1
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
sub handle_opts {
|
|
|
|
my ( $opt, $usage ) = describe_options(
|
|
|
|
'usage: $0 %o MODULE',
|
|
|
|
[ 'maintainer|m=s', 'the package maintainer' ],
|
|
|
|
[ 'debug|d', 'enable debug output' ],
|
|
|
|
[ 'help', 'print usage message and exit' ]
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( $opt->help ) {
|
|
|
|
print $usage->text;
|
|
|
|
exit;
|
|
|
|
}
|
|
|
|
|
|
|
|
my $module_name = $ARGV[0];
|
|
|
|
|
|
|
|
if ( !defined $module_name ) {
|
|
|
|
print STDERR "Missing module name\n";
|
|
|
|
print STDERR $usage->text;
|
|
|
|
exit 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ( $opt, $module_name );
|
|
|
|
}
|
|
|
|
|
|
|
|
# Takes a Perl package attribute name and returns 1 if the name cannot
|
|
|
|
# be referred to as a bareword. This typically happens if the package
|
|
|
|
# name is a reserved Nix keyword.
|
|
|
|
sub is_reserved {
|
|
|
|
my ($pkg) = @_;
|
|
|
|
|
|
|
|
return $pkg =~ /^(?: assert |
|
|
|
|
else |
|
|
|
|
if |
|
|
|
|
import |
|
|
|
|
in |
|
|
|
|
inherit |
|
|
|
|
let |
|
|
|
|
rec |
|
|
|
|
then |
|
|
|
|
while |
|
|
|
|
with )$/x;
|
|
|
|
}
|
2013-06-13 13:39:08 +00:00
|
|
|
|
|
|
|
sub pkg_to_attr {
|
2015-10-30 00:04:31 +00:00
|
|
|
my ($module) = @_;
|
|
|
|
my $attr_name = $module->package_name;
|
|
|
|
if ( $attr_name eq "libwww-perl" ) {
|
|
|
|
return "LWP";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$attr_name =~ s/-//g;
|
|
|
|
return $attr_name;
|
|
|
|
}
|
2013-06-13 13:39:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
sub get_pkg_name {
|
|
|
|
my ($module) = @_;
|
2019-09-25 00:44:52 +00:00
|
|
|
return ( $module->package_name, $module->package_version =~ s/^v(\d)/$1/r );
|
2013-06-13 13:39:08 +00:00
|
|
|
}
|
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
sub read_meta {
|
|
|
|
my ($pkg_path) = @_;
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
my $yaml_path = "$pkg_path/META.yml";
|
|
|
|
my $json_path = "$pkg_path/META.json";
|
|
|
|
my $meta;
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
if ( -r $json_path ) {
|
|
|
|
$meta = CPAN::Meta->load_file($json_path);
|
|
|
|
}
|
|
|
|
elsif ( -r $yaml_path ) {
|
|
|
|
$meta = CPAN::Meta->load_file($yaml_path);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
WARN("package has no META.yml or META.json");
|
|
|
|
}
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
return $meta;
|
2013-06-13 13:39:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# Map a module to the attribute corresponding to its package
|
|
|
|
# (e.g. HTML::HeadParser will be mapped to HTMLParser, because that
|
|
|
|
# module is in the HTML-Parser package).
|
|
|
|
sub module_to_pkg {
|
2015-10-30 00:04:31 +00:00
|
|
|
my ( $cb, $module_name ) = @_;
|
|
|
|
my @modules = $cb->search( type => "name", allow => [$module_name] );
|
|
|
|
if ( scalar @modules == 0 ) {
|
|
|
|
|
2013-06-13 13:39:08 +00:00
|
|
|
# Fallback.
|
|
|
|
$module_name =~ s/:://g;
|
|
|
|
return $module_name;
|
|
|
|
}
|
2015-10-30 00:04:31 +00:00
|
|
|
my $module = $modules[0];
|
|
|
|
my $attr_name = pkg_to_attr($module);
|
|
|
|
DEBUG("mapped dep $module_name to $attr_name");
|
2013-06-13 13:39:08 +00:00
|
|
|
return $attr_name;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub get_deps {
|
2015-10-30 00:04:31 +00:00
|
|
|
my ( $cb, $meta, $type ) = @_;
|
|
|
|
|
|
|
|
return if !defined $meta;
|
|
|
|
|
|
|
|
my $prereqs = $meta->effective_prereqs;
|
|
|
|
my $deps = $prereqs->requirements_for( $type, "requires" );
|
2013-06-13 13:39:08 +00:00
|
|
|
my @res;
|
2015-10-30 00:04:31 +00:00
|
|
|
foreach my $n ( $deps->required_modules ) {
|
2013-06-13 13:39:08 +00:00
|
|
|
next if $n eq "perl";
|
2015-10-30 00:04:31 +00:00
|
|
|
|
2020-03-14 05:25:33 +00:00
|
|
|
my @core = Module::CoreList->find_modules(qr/^$n$/);
|
|
|
|
next if (@core);
|
2015-10-30 00:04:31 +00:00
|
|
|
|
|
|
|
my $pkg = module_to_pkg( $cb, $n );
|
|
|
|
|
|
|
|
# If the package name is reserved then we need to refer to it
|
|
|
|
# through the "self" variable.
|
|
|
|
$pkg = "self.\"$pkg\"" if is_reserved($pkg);
|
|
|
|
|
|
|
|
push @res, $pkg;
|
2013-06-13 13:39:08 +00:00
|
|
|
}
|
|
|
|
return @res;
|
|
|
|
}
|
|
|
|
|
|
|
|
sub uniq {
|
2015-10-30 00:04:31 +00:00
|
|
|
return keys %{ { map { $_ => 1 } @_ } };
|
2013-06-13 13:39:08 +00:00
|
|
|
}
|
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
sub render_license {
|
|
|
|
my ($cpan_license) = @_;
|
|
|
|
|
|
|
|
return if !defined $cpan_license;
|
|
|
|
|
|
|
|
my $licenses;
|
|
|
|
|
|
|
|
# If the license is ambiguous then we'll print an extra warning.
|
|
|
|
# For example, "gpl_2" is ambiguous since it may refer to exactly
|
|
|
|
# "GPL v2" or to "GPL v2 or later".
|
|
|
|
my $amb = 0;
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2021-01-10 15:13:57 +00:00
|
|
|
# Whether the license is available inside `lib.licenses`.
|
2015-10-30 00:04:31 +00:00
|
|
|
my $in_set = 1;
|
|
|
|
|
|
|
|
my $nix_license = $LICENSE_MAP{$cpan_license};
|
|
|
|
if ( !$nix_license ) {
|
|
|
|
WARN("Unknown license: $cpan_license");
|
|
|
|
$licenses = [$cpan_license];
|
|
|
|
$in_set = 0;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$licenses = $nix_license->{licenses};
|
|
|
|
$amb = $nix_license->{amb};
|
|
|
|
$in_set = !$nix_license->{in_set};
|
|
|
|
}
|
|
|
|
|
|
|
|
my $license_line;
|
|
|
|
|
|
|
|
if ( @$licenses == 0 ) {
|
|
|
|
|
|
|
|
# Avoid defining the license line.
|
|
|
|
}
|
|
|
|
elsif ($in_set) {
|
2021-01-10 15:13:57 +00:00
|
|
|
my $lic = 'lib.licenses';
|
2015-10-30 00:04:31 +00:00
|
|
|
if ( @$licenses == 1 ) {
|
|
|
|
$license_line = "$lic.$licenses->[0]";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$license_line = "with $lic; [ " . join( ' ', @$licenses ) . " ]";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if ( @$licenses == 1 ) {
|
|
|
|
$license_line = $licenses->[0];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$license_line = '[ ' . join( ' ', @$licenses ) . ' ]';
|
|
|
|
}
|
|
|
|
}
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
INFO("license: $cpan_license");
|
|
|
|
WARN("License '$cpan_license' is ambiguous, please verify") if $amb;
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
return $license_line;
|
|
|
|
}
|
|
|
|
|
2023-07-16 08:20:41 +00:00
|
|
|
sub sha256_to_sri {
|
|
|
|
my ($sha256) = @_;
|
|
|
|
return "sha256-" . encode_base64(pack("H*", $sha256), '');
|
|
|
|
}
|
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
my ( $opt, $module_name ) = handle_opts();
|
|
|
|
|
|
|
|
Log::Log4perl->easy_init(
|
|
|
|
{
|
|
|
|
level => $opt->debug ? $DEBUG : $INFO,
|
|
|
|
layout => '%m%n'
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
my $cb = CPANPLUS::Backend->new;
|
|
|
|
|
|
|
|
my @modules = $cb->search( type => "name", allow => [$module_name] );
|
|
|
|
die "module $module_name not found\n" if scalar @modules == 0;
|
|
|
|
die "multiple packages that match module $module_name\n" if scalar @modules > 1;
|
|
|
|
my $module = $modules[0];
|
|
|
|
|
2019-09-25 00:44:24 +00:00
|
|
|
my ($pkg_name, $pkg_version) = get_pkg_name $module;
|
2015-10-30 00:04:31 +00:00
|
|
|
my $attr_name = pkg_to_attr $module;
|
|
|
|
|
|
|
|
INFO( "attribute name: ", $attr_name );
|
|
|
|
INFO( "module: ", $module->module );
|
|
|
|
INFO( "version: ", $module->version );
|
2019-09-25 00:44:24 +00:00
|
|
|
INFO( "package: ", $module->package, " (", "$pkg_name-$pkg_version", ", ", $attr_name, ")" );
|
2015-10-30 00:04:31 +00:00
|
|
|
INFO( "path: ", $module->path );
|
|
|
|
|
|
|
|
my $tar_path = $module->fetch();
|
2023-07-16 08:20:41 +00:00
|
|
|
my $sri_hash = sha256_to_sri($module->status->checksum_value);
|
2015-10-30 00:04:31 +00:00
|
|
|
INFO( "downloaded to: ", $tar_path );
|
2023-07-16 08:20:41 +00:00
|
|
|
INFO( "hash: ", $sri_hash );
|
2015-10-30 00:04:31 +00:00
|
|
|
|
|
|
|
my $pkg_path = $module->extract();
|
|
|
|
INFO( "unpacked to: ", $pkg_path );
|
|
|
|
|
|
|
|
my $meta = read_meta($pkg_path);
|
|
|
|
|
|
|
|
DEBUG( "metadata: ", encode_json( $meta->as_struct ) ) if defined $meta;
|
|
|
|
|
2016-07-22 21:03:48 +00:00
|
|
|
my @runtime_deps = sort( uniq( get_deps( $cb, $meta, "runtime" ) ) );
|
|
|
|
INFO("runtime deps: @runtime_deps");
|
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
my @build_deps = sort( uniq(
|
|
|
|
get_deps( $cb, $meta, "configure" ),
|
|
|
|
get_deps( $cb, $meta, "build" ),
|
|
|
|
get_deps( $cb, $meta, "test" )
|
|
|
|
) );
|
|
|
|
|
2016-07-22 21:03:48 +00:00
|
|
|
# Filter out runtime dependencies since those are already handled.
|
|
|
|
my %in_runtime_deps = map { $_ => 1 } @runtime_deps;
|
|
|
|
@build_deps = grep { not $in_runtime_deps{$_} } @build_deps;
|
|
|
|
|
|
|
|
INFO("build deps: @build_deps");
|
2015-10-30 00:04:31 +00:00
|
|
|
|
|
|
|
my $homepage = $meta ? $meta->resources->{homepage} : undef;
|
|
|
|
INFO("homepage: $homepage") if defined $homepage;
|
|
|
|
|
|
|
|
my $description = $meta ? $meta->abstract : undef;
|
|
|
|
if ( defined $description ) {
|
|
|
|
$description = uc( substr( $description, 0, 1 ) )
|
|
|
|
. substr( $description, 1 ); # capitalise first letter
|
|
|
|
$description =~ s/\.$//; # remove period at the end
|
2013-07-16 11:44:55 +00:00
|
|
|
$description =~ s/\s*$//;
|
|
|
|
$description =~ s/^\s*//;
|
2015-10-30 00:04:31 +00:00
|
|
|
$description =~ s/\n+/ /; # Replace new lines by space.
|
|
|
|
INFO("description: $description");
|
2013-07-16 11:44:55 +00:00
|
|
|
}
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
#print(Data::Dumper::Dumper($meta->licenses) . "\n");
|
|
|
|
my $license = $meta ? render_license( $meta->licenses ) : undef;
|
|
|
|
|
|
|
|
INFO( "RSS feed: https://metacpan.org/feed/distribution/",
|
|
|
|
$module->package_name );
|
2013-06-13 13:39:08 +00:00
|
|
|
|
2015-10-30 00:04:31 +00:00
|
|
|
my $build_fun = -e "$pkg_path/Build.PL"
|
|
|
|
&& !-e "$pkg_path/Makefile.PL" ? "buildPerlModule" : "buildPerlPackage";
|
2013-06-13 13:39:08 +00:00
|
|
|
|
|
|
|
print STDERR "===\n";
|
|
|
|
|
|
|
|
print <<EOF;
|
2019-09-25 00:44:24 +00:00
|
|
|
${\(is_reserved($attr_name) ? "\"$attr_name\"" : $attr_name)} = $build_fun {
|
|
|
|
pname = "$pkg_name";
|
|
|
|
version = "$pkg_version";
|
2013-06-13 13:39:08 +00:00
|
|
|
src = fetchurl {
|
2019-09-25 00:44:24 +00:00
|
|
|
url = "mirror://cpan/${\$module->path}/${\$module->package}";
|
2023-07-16 08:20:41 +00:00
|
|
|
hash = "$sri_hash";
|
2013-06-13 13:39:08 +00:00
|
|
|
};
|
|
|
|
EOF
|
|
|
|
print <<EOF if scalar @build_deps > 0;
|
|
|
|
buildInputs = [ @build_deps ];
|
|
|
|
EOF
|
|
|
|
print <<EOF if scalar @runtime_deps > 0;
|
|
|
|
propagatedBuildInputs = [ @runtime_deps ];
|
|
|
|
EOF
|
|
|
|
print <<EOF;
|
|
|
|
meta = {
|
|
|
|
EOF
|
|
|
|
print <<EOF if defined $homepage;
|
2021-01-10 15:13:57 +00:00
|
|
|
homepage = "$homepage";
|
2013-06-13 13:39:08 +00:00
|
|
|
EOF
|
2016-03-03 10:51:49 +00:00
|
|
|
print <<EOF if defined $description && $description ne "Unknown";
|
2013-06-13 13:39:08 +00:00
|
|
|
description = "$description";
|
|
|
|
EOF
|
|
|
|
print <<EOF if defined $license;
|
2015-10-30 00:04:31 +00:00
|
|
|
license = $license;
|
|
|
|
EOF
|
|
|
|
print <<EOF if $opt->maintainer;
|
|
|
|
maintainers = [ maintainers.${\$opt->maintainer} ];
|
2013-06-13 13:39:08 +00:00
|
|
|
EOF
|
|
|
|
print <<EOF;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
EOF
|