#!/usr/bin/env perl # notices.pl writes NOTICE.md in the repository root: the full licence text of # every Go module the binary is compiled from, and the terms of the artwork and # the fonts embedded in it. The repository is the record, so the file lives in # the tree and travels with every source archive rather than being attached to # a release. The rows are ordered by provenance, not alphabet: the owner's own # modules on the forge first, then golang.org, then every other source. # # Copy this file to scripts/notices.pl, edit the configuration block, and run: # # perl scripts/notices.pl # # The rule that decides whether the project has the file at all: a notice # exists to reproduce somebody else's terms. A project whose every embedded # thing is the owner's own modules, artwork and code has nobody to notice and # carries no NOTICE.md; the script says so and writes nothing. # # Perl builtins only. The one external binary is the Go toolchain, driven in # list form so that no path is word-split or globbed, and every failure names # what was being attempted. use strict; use warnings; # --------------------------------------------------------------------------- # Configuration. Replace the values; everything below the block is the # standard script and is identical in every repository. # --------------------------------------------------------------------------- my $binary = "volumen"; # Directories whose files ship inside the binary, relative to the repository # root. A directory the project does not have simply adds no section. my $artwork_dir = "internal/web/static"; my $fonts_dir = "internal/web/static/fonts"; # The faces the fonts directory carries, when it carries third-party ones. my $faces_note = <<'FACES'; Ubuntu and Ubuntu Mono (latin and latin-ext subsets), the interface faces of the admin. Copyright 2010, 2011 Canonical Ltd, with the reserved font names "Ubuntu" and "Ubuntu Mono"; distributed under the [Ubuntu Font Licence 1.0](https://ubuntu.com/legal/font-licence), whose terms follow the SIL Open Font Licence 1.1. FACES # --------------------------------------------------------------------------- (my $script_dir = $0) =~ s{/[^/]*$}{}; # Run as `perl notices.pl` and $0 carries no slash: the substitution is a # no-op, and the script's own directory is the working directory. $script_dir = '.' if !length($script_dir) || $0 !~ m{/}; my $root = -f "$script_dir/../go.mod" ? "$script_dir/.." : $script_dir;-f "$root/go.mod" or die "no go.mod in $root, so this is not the module root\n"; my $out_path = "$root/NOTICE.md"; sub slurp { my ($path) = @_; open my $fh, '<', $path or die "cannot read $path: $!\n"; my $text = do { local $/; <$fh> }; close $fh; return $text; } sub spew { my ($path, $text) = @_; open my $fh, '>', $path or die "cannot write $path: $!\n"; print $fh $text; close $fh or die "cannot flush $path: $!\n"; } # licence_files returns the licence texts a module directory carries, in a # stable order, and dies when there are none: a dependency whose terms cannot # be reproduced cannot ship. sub licence_files { my ($dir) = @_; my @found; for my $subdir ('', 'LICENSES/', 'licenses/') { my $at = length $subdir ? "$dir/$subdir" : $dir; next unless -d $at; opendir my $dh, $at or die "cannot list $at: $!\n"; my @names = grep { m{^(?:LICEN[CS]E|COPYING|COPYRIGHT|NOTICE|UNLICENSE)}i && -f "$at/$_" } readdir $dh; closedir $dh; push @found, map { "$at/$_" } sort @names; } return @found; } # provenance orders the modules: the owner's own forge first, golang.org next, # every other source last; alphabetical inside each group. sub provenance { my ($path) = @_; return 0 if $path =~ m{^sourcedock\.dev/}; return 1 if $path =~ m{^golang\.org/}; return 2; } # The modules the binary is built from: the modules that provide the packages # the program imports, which is the same list the toolchain records in the # binary. The build list is wider, because it also holds modules that no # package of this program compiles, and those need no notice. my $main = do { open my $m, '-|', qw{go -C}, $root, qw{list -m -f}, '{{.Path}}' or die "cannot run go list -m: $!\n"; my $line = <$m>; close $m or die "go list -m failed\n"; defined $line or die "go list -m named no main module\n"; chomp $line; $line; }; length $main or die "go list -m named no main module\n"; my @lines; open my $go, '-|', qw{go -C}, $root, qw{list -deps -f}, "{{if .Module}}{{.Module.Path}}\t{{.Module.Version}}\t{{.Module.Dir}}{{end}}", "./cmd/$binary" or die "cannot run go list: $!\n"; while (my $line = <$go>) { push @lines, $line } close $go or die "go list -deps failed\n"; my (%seen, @modules); for my $line (sort @lines) { chomp $line; next unless length $line; my ($path, $version, $dir) = split /\t/, $line, 3; $path = '' unless defined $path; $version = '' unless defined $version; $dir = '' unless defined $dir; next if $path eq $main; next if $seen{$path}++; length $dir or die "no module directory for $path, run go mod download all first\n"; -d $dir or die "module directory $dir for $path does not exist\n"; push @modules, { path => $path, version => $version, dir => $dir }; } my @faces; if (opendir my $dh, "$root/$fonts_dir") { @faces = sort grep { /\.woff2?\z/ } readdir $dh; closedir $dh; } # The exemption: everything the artefact carries is the owner's own. The # owner's modules (provenance 0) and the owner's artwork need no notice, and a # project with nothing else has no file. The script writes nothing, and says # why; delete a NOTICE.md left over from a heavier dependency set by hand. my @external = grep { provenance($_->{path}) != 0 } @modules; if (!@external && !@faces) { print "NOTICE.md not needed: every dependency of ./cmd/$binary is the owner's own\n"; print "and an old NOTICE.md should be removed\n" if -f $out_path; exit 0; } @modules = sort { provenance($a->{path}) <=> provenance($b->{path}) || $a->{path} cmp $b->{path} } @modules; my $licence_url = { 'PolyForm-Noncommercial-1.0.0' => 'https://polyformproject.org/licenses/noncommercial/1.0.0', 'CC-BY-NC-4.0' => 'https://creativecommons.org/licenses/by-nc/4.0/', 'MIT' => 'https://opensource.org/license/mit', }; my @t = gmtime; my $stamp = sprintf '%04d-%02d-%02d', $t[5] + 1900, $t[4] + 1, $t[3]; my $out = <<'HEAD'; # Third-party notices The project is under the licence in [LICENSE](LICENSE). Everything else that a built binary carries is here: the Go modules it is compiled from, with each licence reproduced verbatim, and the terms of the artwork and the fonts embedded in it. The owner's own modules come first, golang.org next, every other source last. Every licence text below is quoted inside a code block, so a Markdown renderer cannot reflow it. HEAD $out .= <<"HEAD"; Generated on $stamp by `perl scripts/notices.pl`, which takes the module list from \`go list -deps ./cmd/$binary\`, so it names what the toolchain actually links rather than the wider build list. Run it again after a dependency changes. HEAD for my $module (@modules) { my @files = licence_files($module->{dir}); @files or die "no licence file found for $module->{path} in $module->{dir}\n"; my $name = length $module->{version} ? "$module->{path} $module->{version}" : $module->{path}; $out .= "## $name\n\n"; for my $file (@files) { if (@files > 1) { (my $label = $file) =~ s{^\Q$module->{dir}\E/?}{}; $out .= "**$label**\n\n"; } my $text = slurp($file); $text =~ s/\s+\z//; $out .= "```text\n$text\n```\n\n"; } } my @svgs; if (opendir my $ah, "$root/$artwork_dir") { @svgs = sort grep { /\.svg\z/ } readdir $ah; closedir $ah; } if (@svgs) { $out .= "## Artwork embedded in the binary\n\n"; for my $name (@svgs) { my $head = slurp("$root/$artwork_dir/$name"); my ($copy) = $head =~ m{^\s*$}m; my ($spdx) = $head =~ m{^$}m; defined $copy or die "no copyright line in $artwork_dir/$name\n"; defined $spdx or die "no SPDX line in $artwork_dir/$name\n"; my $url = $licence_url->{$spdx} or die "no licence URL known for $spdx\n"; $out .= "### $name\n\n$copy\n\n[$spdx]($url)\n\n"; } } if (@faces) { $out .= "## Fonts embedded in the binary\n\n"; (my $note = $faces_note) =~ s/\s+\z//; $out .= "$note\n\nThe files carried:\n\n"; $out .= "- $_\n" for @faces; $out .= "\n"; } spew($out_path, $out); printf "wrote NOTICE.md (%d module(s), %d artwork file(s), %d font file(s))\n", scalar @modules, scalar @svgs, scalar @faces;