GUACAMOLE-2139: Automatically derive license information without manually formatted README.

This commit is contained in:
Michael Jumper committed 2026-02-10 22:44:19 -08:00
1 parent 88a4c60409
commit a1ba3abdc8
1 file changed
+283 -47
+283 -47
View File
@@ -17,12 +17,6 @@
# specific language governing permissions and limitations
# under the License.
#
# NOTE: Parts of this file (Makefile.am) are automatically transcluded verbatim
# into Makefile.in. Though the build system (GNU Autotools) automatically adds
# its own license boilerplate to the generated Makefile.in, that boilerplate
# does not apply to the transcluded portions of Makefile.am which are licensed
# to you by the ASF under the Apache License, Version 2.0, as described above.
#
#
# generate-license-files.sh
@@ -84,19 +78,55 @@
#
# Regardless of whether a dependency is documented within the top-level
# guacamole-client project or within the relevant subproject, the subdirectory
# for each dependency must contain a "README" file describing the dependency,
# the version used, the copyright holder(s), and the license. Examples of the
# formatting expected for these README files can be found within the existing
# dependency license directories.
# for each dependency must contain at least one file containing headers describing
# the dependency, the copyright holder(s), and the license.
#
# Files that contain the word "notice", regardless of case, are considered by
# this script to be the notice file mentioned within the Apache License, and
# Filenames that contain the word "notice", regardless of case, are considered
# by this script to be the notice file mentioned within the Apache License, and
# will be copied verbatim into the generated NOTICE file.
#
# Files that that are not recognized as any other type of file are considered
# by this script to be the license and will be included verbatim within the
# generated LICENSE if they are less than 50 lines.
#
# ** PLEASE SEE THE EXISTING DEPENDENCY LICENSE DIRECTORIES FOR EXAMPLES OF
# THIS IN PRACTICE **
#
# License header fields
# ---------------------
#
# The possible header fields are:
#
# Field | Description
# ------- | -------------------------------------------------------------------
# Name | REQUIRED. The name of the component.
# License | REQUIRED. The human-readable name of the copyright license (Apache v2.0, MIT, etc.).
# From | REQUIRED. The original developer, current maintainer, or copyright holder.
# URL | REQUIRED. The URL of the component's web page.
# Version | The version number of the component (if not determined by Maven/NPM).
# Source | The URL that the current file may be downloaded from.
# + | Arbitrary comments or information that should be included with the license.
#
# The value for the Source header may contain any of the following variables in
# shell format ($VARIABLE or ${VARIABLE}), to allow for URLs that vary by version:
#
# Variable | Description
# --------- | ----------------------------------------------------------------
# VERSION | The exact version number provided.
# _VERSION_ | The version number provided, with all periods replaced with underscore.
# MAJOR | The major (leftmost) component of the version number.
# MINOR | The minor (second from the left) component of the version number.
# PATCH | The patch (third from the left) component of the version number.
# REV | The "revision" (fourth from the left) component of the version number.
#
# If a field is included multiple times within the same header, each value is
# considered to be one line of that field. If multiple "Source" headers are
# provided, each is tried in order until the download succeeds.
#
# ** IMPORTANT: ** Except for "Source", which may be included in any license
# information file to permit dynamic updates of that information, all of these
# fields MUST be located within the same file.
#
##
## The filename of the file containing whether any errors have been logged. The
@@ -151,14 +181,19 @@ list_dependency_license_info() {
# List the license directories of all runtime dependencies, as dictated by
# the "dep-coordinates.txt" files included within those directories
sed 's/^[[:space:]]\+//' | grep -o '^[^: ]\+\(:[^: ]*\)\{1,3\}' | while read DEPENDENCY; do
sed 's/^[[:space:]]\+//' | grep -o '^[^: ]\+\(:[^: ]*\)\{1,3\}' \
| while read DEPENDENCY; do
if ! grep -l "$DEPENDENCY[[:space:]]*$" "$LICENSES_DIR"/*/dep-coordinates.txt; then
local DEPENDENCY_REGEX="`echo "$DEPENDENCY" | sed 's/[^:]*/\\\\(&\\\\|\*\\\\)/g'`"
local VERSION="`echo "$DEPENDENCY" | sed 's/.*://'`"
if ! grep -l "^$DEPENDENCY_REGEX[[:space:]]*$" "$LICENSES_DIR"/*/dep-coordinates.txt \
| sed "s/$/ $VERSION/g"; then
error "License information missing for $DEPENDENCY"
fi
done | sort -u | while read LICENSE_INFO_COORDS_FILE; do
dirname "$LICENSE_INFO_COORDS_FILE"
done | sort -u -k 1,1 | while read LICENSE_INFO_COORDS_FILE VERSION; do
printf '%s %s\n' "`dirname "$LICENSE_INFO_COORDS_FILE"`" "$VERSION"
done
# Include license directories for all dependencies not pulled in automatically
@@ -175,15 +210,16 @@ list_dependency_license_info() {
## written to STDOUT. Each directory must be on its own line.
##
sort_dependency_license_info() {
while read LICENSE_INFO; do
while read LICENSE_INFO VERSION; do
if [ ! -e "$LICENSE_INFO/README" ]; then
error "Missing license README in $LICENSE_INFO"
continue
fi
local PRIMARY_FILE="`primary_file "$LICENSE_INFO"`"
NAME="`grep . < "$LICENSE_INFO/README" | head -n1`"
printf "%s\t%s\n" "$NAME" "$LICENSE_INFO"
# Skip if primary file missing (error will be logged by primary_file
# function call above)
[ -n "$PRIMARY_FILE" ] || continue
local NAME="`get_header_value "$PRIMARY_FILE" Name`"
printf "%s\t%s %s\n" "$NAME" "$LICENSE_INFO" "$VERSION"
done | sort -f | cut -f2
}
@@ -236,7 +272,6 @@ license_file() {
find "$DIR" -mindepth 1 \
-a \! -iname "*notice*" \
-a \! -name "dep-coordinates.txt" \
-a \! -name "README" \
| if ! single_result; then
error "Multiple license files found within $DIR"
fi
@@ -258,6 +293,29 @@ notice_file() {
fi
}
##
## Determines which file within the given directory is the primary source of
## license information, dictated by whichever file contains the "License"
## field. If such a file exists, it is printed to STDOUT. If not, an error is
## logged.
##
## @param DIR
## The directory to search.
##
primary_file() {
DIR="$1"
for FILENAME in "$DIR"/*; do
if [ ! -d "$FILENAME" ] && [ -n "`get_header_value "$FILENAME" License`" ]; then
echo "$FILENAME"
return
fi
done
error "No file containing \"License\" field found within $DIR"
}
##
## Prints the contents of the given file, excluding any blank lines at the
## beginning and end of the file.
@@ -278,6 +336,134 @@ trim_file() {
}
##
## Reads text from STDIN, printing that text to STDOUT with leading spaces. If
## the text consists of only a single line, a single leading space is added. If
## the text consists of multiple lines, each line is indented by two levels,
## with four spaces per level.
##
format_multiline() {
local INDENT=" "
local NEXT_LINE
read FIRST_LINE
if read NEXT_LINE; then
printf '\n'
printf "%s%s\n" "$INDENT" "$FIRST_LINE"
printf "%s%s\n" "$INDENT" "$NEXT_LINE"
while read NEXT_LINE; do
printf "%s%s\n" "$INDENT" "$NEXT_LINE"
done
else
printf ' %s\n' "$FIRST_LINE"
fi
}
##
## Performs variable substitution on STDIN, replacing version-related variables
## with values derived from the given version number. Variables must be
## formatted as "$VARIABLE" or "${VARIABLE}".
##
## Supported variables are:
##
## @param VERSION
## The version number to substitute.
##
subst_version() {
local VERSION="$1"
local _VERSION_="`echo "$VERSION" | sed 's/\./_/g'`"
local MAJOR MINOR PATCH REV
read -r MAJOR MINOR PATCH REV <<EOF
`echo "$VERSION" | sed 's/[^0-9]\+/ /g'`
EOF
env "VERSION=$VERSION" \
"_VERSION_=$_VERSION_" \
"MAJOR=$MAJOR" \
"MINOR=$MINOR" \
"PATCH=$PATCH" \
"REV=$REV" \
envsubst '$VERSION $_VERSION_ $MAJOR $MINOR $PATCH $REV'
}
##
## Retrieves all header lines from the given file. The header lines of a
## license-related file are all lines that precede the first "BEGIN LICENSE
## FILE" marker.
##
## @param FILENAME
## The file to retrieve the header from.
##
get_header() {
local FILENAME="$1"
sed -n \
-e '/^# --- BEGIN LICENSE FILE \(\[[^]]*\] \)\?---$/q' \
-e 's/^#[[:space:]]*//p' \
"$FILENAME" | grep .
}
##
## Retrieves the header field having the given name from the given file. The
## header lines of a license-related file are all lines that precede the first
## "BEGIN LICENSE FILE" marker, and a header field is a name/value pair
## included within this header on its own line in the form "NAME: VALUE", where
## "NAME" is the field name and "VALUE" is the desired value.
##
## If a field is included multiple times within the same header, each value is
## considered to be one line of that field.
##
get_header_value() {
local FILENAME="$1"
local FIELD="$2"
get_header "$FILENAME" | grep "^$FIELD:" \
| sed 's/^[^:]*:[[:space:]]*//' \
| sed 's/[[:space:]]\+$//' \
| grep .
}
##
## Retrieves the full license contents from the given license file. If the
## license information is version-specific, the provided version is used to
## determine which contents are returned based the versions noted in "BEGIN
## LICENSE FILE [VERSION]" and "END LICENSE FILE [VERSION]" markers. If no
## version is provided, only unversioned markers will be used.
##
## @param FILENAME
## The file to retrieve license contents from.
##
## @param VERSION
## The version of the component that the license applies to.
##
get_license() {
local FILENAME="$1"
local VERSION="$2"
local VERSION_MATCH='[^]]*'
if [ -n "$VERSION" ]; then
VERSION_MATCH="$(echo "$VERSION" | sed 's:[/$.*\[\\^]:\\&:g')"
fi
sed -n \
-e '1,/^# --- BEGIN LICENSE FILE \(\['"$VERSION_MATCH"'\] \)\?---$/d' \
-e '/^# --- END LICENSE FILE \(\['"$VERSION_MATCH"'\] \)\?---$/q' \
-e 'p' \
"$FILENAME"
}
# Verify that an output directory was provided
if [ -z "$DEPENDENCY_LIST" -o -z "$OUTPUT_DIR" ]; then
error "USAGE: $0 DEPENDENCY_LIST OUTPUT_DIRECTORY"
@@ -311,7 +497,7 @@ cp "$BASEDIR/NOTICE" "$OUTPUT_DIR/"
PREAMBLE_ADDED=0
find "$DEPENDENCY_LIST" -type f -exec cat '{}' + | \
list_dependency_license_info | sort_dependency_license_info | \
while read LICENSE_INFO_DIR; do
while read LICENSE_INFO_DIR VERSION; do
# Add subcomponent license preamble if not already added
if [ "$PREAMBLE_ADDED" = 0 ]; then
@@ -323,6 +509,7 @@ APACHE GUACAMOLE SUBCOMPONENTS
Apache Guacamole includes a number of subcomponents with separate copyright
notices and license terms. Your use of these subcomponents is subject to the
terms and conditions of the following licenses.
EOF
PREAMBLE_ADDED=1
fi
@@ -330,32 +517,82 @@ EOF
# Locate LICENSE and NOTICE files
LICENSE_FILE="`license_file "$LICENSE_INFO_DIR"`"
NOTICE_FILE="`notice_file "$LICENSE_INFO_DIR"`"
PRIMARY_FILE="`primary_file "$LICENSE_INFO_DIR"`"
# Extract component name from README
COMPONENT_NAME="`trim_file "$LICENSE_INFO_DIR/README" | head -n1 | grep -o '^[^(]*[^([:space:]]'`"
# Skip if primary file missing (error will be logged by primary_file
# function call above)
[ -n "$PRIMARY_FILE" ] || continue
# Add license information to LICENSE
printf '\n\n' >> "$OUTPUT_DIR/LICENSE"
trim_file "$LICENSE_INFO_DIR/README" >> "$OUTPUT_DIR/LICENSE"
# Append verbatim copy of license if small enough
if [ -n "$LICENSE_FILE" ]; then
if [ "`wc -l < "$LICENSE_FILE"`" -le "$LICENSE_COPY_LIMIT" ]; then
echo >> "$OUTPUT_DIR/LICENSE"
trim_file "$LICENSE_FILE" >> "$OUTPUT_DIR/LICENSE"
fi
fi
# Copy NOTICE, if provided
if [ -n "$NOTICE_FILE" ]; then
printf '\n======== NOTICE for "%s" ========\n\n' "$COMPONENT_NAME" >> "$OUTPUT_DIR/NOTICE"
trim_file "$NOTICE_FILE" >> "$OUTPUT_DIR/NOTICE"
fi
BUNDLED_LICENSE_FILE="bundled/`basename "$LICENSE_INFO_DIR"`/`basename "$LICENSE_FILE"`"
BUNDLED_NOTICE_FILE="bundled/`basename "$LICENSE_INFO_DIR"`/`basename "$NOTICE_FILE"`"
# Include verbatim copy of license information
mkdir -p "$OUTPUT_DIR/bundled/"
cp -Lr "$LICENSE_INFO_DIR" "$OUTPUT_DIR/bundled/"
TITLE="`get_header_value "$PRIMARY_FILE" Name`"
TITLE="$TITLE (`get_header_value "$PRIMARY_FILE" URL`)"
# Get VERSION from header field if not otherwise available (for
# dependencies not pulled in by Maven or NPM, like fonts)
if [ -z "$VERSION" ]; then
VERSION="`get_header_value "$PRIMARY_FILE" Version`"
if [ -z "$VERSION" ]; then
VERSION="N/A"
fi
fi
(
# Add license information to LICENSE
printf '\n'
cat <<EOF
$TITLE
`printf '%s' "$TITLE" | sed 's/./-/g'`
Version: $VERSION
From:`get_header_value "$PRIMARY_FILE" From | format_multiline`
License(s):
EOF
# Copy LICENSE, if provided (we omit the local copy of the LICENSE if the
# software is Apache-licensed, as we already bundle a copy ourselves)
printf ' '
if [ -n "$LICENSE_FILE" ] && get_license "$LICENSE_FILE" "$VERSION" | grep -q .; then
printf '%s (%s)\n\n' \
"`get_header_value "$PRIMARY_FILE" License`" \
"$BUNDLED_LICENSE_FILE"
get_license "$LICENSE_FILE" "$VERSION" > "$OUTPUT_DIR/$BUNDLED_LICENSE_FILE"
printf '\n' >> "$OUTPUT_DIR/$BUNDLED_LICENSE_FILE"
get_header_value "$PRIMARY_FILE" "+" >> "$OUTPUT_DIR/$BUNDLED_LICENSE_FILE"
# Append verbatim copy of license if small enough
LICENSE_LINE_COUNT="`get_license "$LICENSE_FILE" "$VERSION" | wc -l`"
if [ "$LICENSE_LINE_COUNT" -gt 0 -a "$LICENSE_LINE_COUNT" -le "$LICENSE_COPY_LIMIT" ]; then
printf '>\n'
get_license "$LICENSE_FILE" "$VERSION" | sed 's/^/> /' | sed 's/[[:space:]]\+$//'
printf '>\n\n'
get_header_value "$PRIMARY_FILE" "+" && printf '\n'
fi
else
printf '%s\n\n' "`get_header_value "$PRIMARY_FILE" License`"
fi
) | subst_version "$VERSION" >> "$OUTPUT_DIR/LICENSE"
# Copy NOTICE, if provided
if [ -n "$NOTICE_FILE" ]; then
COMPONENT_NAME="`get_header_value "$PRIMARY_FILE" Name`"
printf '\n======== NOTICE for "%s" ========\n\n' "$COMPONENT_NAME" >> "$OUTPUT_DIR/NOTICE"
get_license "$NOTICE_FILE" "$VERSION" >> "$OUTPUT_DIR/NOTICE"
get_license "$NOTICE_FILE" "$VERSION" > "$OUTPUT_DIR/$BUNDLED_NOTICE_FILE"
fi
# Add README describing nature of the "bundled/" directory
cat > "$OUTPUT_DIR/bundled/README" <<EOF
Apache Guacamole includes a number of subcomponents with separate copyright
@@ -366,9 +603,8 @@ EOF
done
# Do not double-bundle the information contained in the README files (it's
# already in LICENSE), nor internal mappings like dep-coordinates.txt
for EXCLUDED_FILE in README dep-coordinates.txt; do
# Do not include internal dep-coordinates.txt mappings
for EXCLUDED_FILE in dep-coordinates.txt; do
rm -f "$OUTPUT_DIR/bundled"/*/"$EXCLUDED_FILE"
done