This is an automated email from the ASF dual-hosted git repository.

pjfanning pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/pekko-grpc.git


The following commit(s) were added to refs/heads/main by this push:
     new 1d2a2f6f Ship a complete LICENSE and NOTICE in the assembly jars (#865)
1d2a2f6f is described below

commit 1d2a2f6f00d39de1a1f71976d2d8f66628d81d74
Author: PJ Fanning <[email protected]>
AuthorDate: Mon Aug 31 12:52:13 2026 +0100

    Ship a complete LICENSE and NOTICE in the assembly jars (#865)
    
    * Ship a complete LICENSE and NOTICE in the assembly jars
    
    Motivation:
    pekko-grpc-codegen and pekko-grpc-scalapb-protoc-plugin publish assembly 
jars
    and bat archives that bundle classes from 3rd party projects, so their
    META-INF/LICENSE and META-INF/NOTICE have to account for those projects.
    legal/AssemblyLicense.txt and legal/AssemblyNotice.txt exist for that, but:
    
    - The codegen assembly jar and bat shipped no META-INF/LICENSE at all. The
      merge strategy discarded META-INF/LICENSE to stop guava/failureaccess
      colliding, and that also discarded the copy the ApacheSonatypePlugin
      resource generator puts in the jar.
    - legal/AssemblyLicense.txt had drifted from what is actually bundled. It
      listed org.checkerframework:checker-qual, which guava dropped in favour of
      org.jspecify:jspecify, and jspecify was not listed anywhere.
    - The scalapb-protoc-plugin assembly bundled protobuf-java 4.35.0, pulled in
      transitively by compilerplugin, rather than the pinned 4.36.0 that codegen
      and runtime bundle.
    
    Nothing failed the build when the legal files fell behind a dependency 
change.
    
    Modification:
    Merge META-INF/LICENSE and META-INF/NOTICE with MergeStrategy.preferProject 
in
    both assemblies, so our own files win over the ones the dependencies bring 
in.
    
    Swap checker-qual for jspecify in legal/AssemblyLicense.txt and drop the MIT
    text that was only there for checker-qual.
    
    Pin protobuf-java in scalapb-protoc-plugin via a new Dependencies group, the
    same way codegen and runtime already do.
    
    Add project/AssemblyLicenseCheck.scala with two tasks, wired into both
    assembly projects and run in CI as the checkAssemblyLicenses alias:
    
    - assemblyLicenseCheck fails when a module on the assembly classpath is not
      named in the assembly LICENSE file, and warns about listed modules that 
are
      not bundled for the Scala version being built.
    - assemblyMetaInfCheck opens the published assembly jar and bat archive and
      fails unless each carries META-INF/LICENSE and META-INF/NOTICE matching 
the
      files in legal/.
    
    Result:
    Both assembly jars and both bat archives ship a LICENSE and NOTICE covering
    the 3rd party classes they bundle, both bundle protobuf-java 4.36.0, and CI
    fails if a dependency change leaves the legal files behind.
    
    Tests:
    - sbt checkAssemblyLicenses - passed. assemblyMetaInfCheck fails on the
      codegen assembly before the merge strategy fix (META-INF/LICENSE is
      missing), and assemblyLicenseCheck fails before the AssemblyLicense.txt 
fix
      (org.jspecify:jspecify is not accounted for).
    - sbt scalafmtSbtCheck - passed.
    
    References:
    None - assembly jar license completeness
    
    * Update AssemblyLicenseCheck.scala
    
    * Name the archive when it cannot be read as a zip
    
    Motivation:
    `assemblyMetaInfCheck` opened each archive with `new ZipFile`, which throws
    `ZipException: error in opening zip file` without naming the file, so a 
corrupt
    or truncated archive in CI gave no clue which one it was.
    
    Modification:
    Open the archive and its entries through `sbt.io.Using`, and report 
`IOException`
    with the archive name.
    
    Result:
    A failure to read an archive names it, like the license problems reported 
below it.
    
    Tests:
    - sbt checkAssemblyLicenses - passed for both assembly projects (jar and 
bat)
    
    References:
    Refs #865
    
    * Assert the shipped LICENSE and NOTICE still say something
    
    Motivation:
    `assemblyMetaInfCheck` only compared the archived files with the curated 
ones in
    `legal/`, which says the merge strategy picked our copy but not that the 
copy is
    still a real LICENSE or NOTICE. An emptied or truncated curated file would 
ship
    and pass.
    
    Modification:
    Check the shipped bytes for the markers each file has to carry: the Apache
    License 2.0 heading and terms plus the 3rd party section in LICENSE, and the
    product name, the ASF attribution and the 3rd party section in NOTICE. 
Missing
    markers are reported alongside the existing problems for that archive.
    
    Result:
    An assembly that ships a gutted LICENSE or NOTICE fails the check.
    
    Tests:
    - sbt checkAssemblyLicenses - passed for both assembly projects (jar and 
bat)
    - With legal/AssemblyLicense.txt cut down to its first two lines,
      codegen/assemblyMetaInfCheck fails: "META-INF/LICENSE does not mention
      'TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION',
      'contain classes from 3rd party projects', 'Apache License Version 2.0:'"
    
    References:
    Refs #865
---
 .github/workflows/build-test.yml   |   5 ++
 build.sbt                          |  19 +++--
 legal/AssemblyLicense.txt          |  27 +------
 project/AssemblyLicenseCheck.scala | 142 +++++++++++++++++++++++++++++++++++++
 project/Dependencies.scala         |   4 ++
 5 files changed, 167 insertions(+), 30 deletions(-)

diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index a0b73d21..4b03e635 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -52,6 +52,11 @@ jobs:
           cp .jvmopts-ci .jvmopts
           sbt grpcVersionSyncCheck googleProtobufVersionSyncCheck 
mimaReportBinaryIssues
 
+      - name: Assembly license check
+        run: |-
+          cp .jvmopts-ci .jvmopts
+          sbt checkAssemblyLicenses
+
   compile-benchmarks:
     name: Compile Benchmarks
     runs-on: ubuntu-latest
diff --git a/build.sbt b/build.sbt
index a0ecbfb5..8cf424ee 100644
--- a/build.sbt
+++ b/build.sbt
@@ -49,6 +49,10 @@ ThisBuild / javafmtFormatterCompatibleJavaVersion := 17
 
 addCommandAlias("checkCodeStyle", "scalafmtCheckAll; scalafmtSbtCheck; 
javafmtCheckAll; +headerCheckAll")
 addCommandAlias("applyCodeStyle", "+headerCreateAll; scalafmtAll; scalafmtSbt; 
javafmtAll")
+addCommandAlias(
+  "checkAssemblyLicenses",
+  "codegen/assemblyLicenseCheck; codegen/assemblyMetaInfCheck; " +
+  "scalapb-protoc-plugin/assemblyLicenseCheck; 
scalapb-protoc-plugin/assemblyMetaInfCheck")
 
 val pekkoGrpcCodegenId = s"$pekkoPrefix-codegen"
 lazy val codegen = Project(id = "codegen", base = file("codegen"))
@@ -58,12 +62,14 @@ lazy val codegen = Project(id = "codegen", base = 
file("codegen"))
   .settings(Dependencies.codegen)
   .settings(resolvers += Resolver.sbtPluginRepo("releases"))
   .settings(MetaInfLicenseNoticeCopy.assemblySettings)
+  .settings(AssemblyLicenseCheck.settings)
   .settings(
     name := s"$pekkoPrefix-codegen",
     mkBatAssemblyTask := {
       val file = assembly.value
       Assemblies.mkBatAssembly(file)
     },
+    AssemblyLicenseCheck.assemblyMetaInfArchives += mkBatAssemblyTask.value,
     buildInfoKeys ++= Seq[BuildInfoKey](organization, name, version, 
scalaVersion, sbtVersion),
     buildInfoKeys += "runtimeArtifactName" -> pekkoGrpcRuntimeName,
     buildInfoKeys += "pekkoVersion" -> Dependencies.Versions.pekko,
@@ -80,7 +86,9 @@ lazy val codegen = Project(id = "codegen", base = 
file("codegen"))
     (assembly / assemblyOption) := (assembly / 
assemblyOption).value.withPrependShellScript(
       Some(sbtassembly.AssemblyPlugin.defaultUniversalScript(shebang = true))),
     (assembly / assemblyMergeStrategy) := {
-      case PathList("META-INF", "LICENSE")                          => 
MergeStrategy.discard
+      // our own META-INF/LICENSE and META-INF/NOTICE cover the bundled 3rd 
party classes,
+      // so they win over the ones the dependencies bring in
+      case PathList("META-INF", "LICENSE" | "NOTICE")               => 
MergeStrategy.preferProject
       case PathList("META-INF", "MANIFEST.MF")                      => 
MergeStrategy.discard
       case PathList("META-INF", "versions", _, "module-info.class") => 
MergeStrategy.discard
       case "LICENSE" | "LICENSE.txt" | "NOTICE"                     => 
MergeStrategy.discard
@@ -130,15 +138,15 @@ val pekkoGrpcProtocPluginId = 
s"$pekkoPrefix-scalapb-protoc-plugin"
 lazy val scalapbProtocPlugin = Project(id = "scalapb-protoc-plugin", base = 
file("scalapb-protoc-plugin"))
   .disablePlugins(MimaPlugin)
   .settings(MetaInfLicenseNoticeCopy.assemblySettings)
+  .settings(AssemblyLicenseCheck.settings)
+  .settings(Dependencies.scalapbProtocPlugin)
   .settings(
     name := s"$pekkoPrefix-scalapb-protoc-plugin",
-    libraryDependencies += {
-      Dependencies.Compile.scalapbCompilerPlugin
-    },
     mkBatAssemblyTask := {
       val file = assembly.value
       Assemblies.mkBatAssembly(file)
     },
+    AssemblyLicenseCheck.assemblyMetaInfArchives += mkBatAssemblyTask.value,
     (Compile / assembly / artifact) := {
       val art = (Compile / assembly / artifact).value
       art.withClassifier(Some("assembly"))
@@ -147,6 +155,9 @@ lazy val scalapbProtocPlugin = Project(id = 
"scalapb-protoc-plugin", base = file
     (assembly / assemblyOption) := (assembly / 
assemblyOption).value.withPrependShellScript(
       Some(sbtassembly.AssemblyPlugin.defaultUniversalScript(shebang = true))),
     (assembly / assemblyMergeStrategy) := {
+      // our own META-INF/LICENSE and META-INF/NOTICE cover the bundled 3rd 
party classes,
+      // so they win over the ones the dependencies bring in
+      case PathList("META-INF", "LICENSE" | "NOTICE")               => 
MergeStrategy.preferProject
       case PathList("META-INF", "MANIFEST.MF")                      => 
MergeStrategy.discard
       case PathList("META-INF", "versions", _, "module-info.class") => 
MergeStrategy.discard
       case "LICENSE" | "LICENSE.txt" | "NOTICE"                     => 
MergeStrategy.discard
diff --git a/legal/AssemblyLicense.txt b/legal/AssemblyLicense.txt
index 685edcc5..a8316b4b 100644
--- a/legal/AssemblyLicense.txt
+++ b/legal/AssemblyLicense.txt
@@ -220,7 +220,7 @@ com.google.j2objc:j2objc-annotations
 io.grpc:grpc-api
 io.grpc:grpc-protobuf
 io.grpc:grpc-protobuf-lite
-org.checkerframework:checker-qual
+org.jspecify:jspecify
 org.playframework.twirl:twirl-api
 org.scala-lang:scala-library
 org.scala-lang:scala3-library
@@ -229,31 +229,6 @@ org.scala-lang.modules:scala-xml
 
 ----
 
-Checker Framework qualifiers
-Copyright 2004-present by the Checker Framework developers
-
-MIT License:
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-----
-
 com.google.protobuf:protobuf-java
 
 Copyright 2008 Google Inc.  All rights reserved.
diff --git a/project/AssemblyLicenseCheck.scala 
b/project/AssemblyLicenseCheck.scala
new file mode 100644
index 00000000..092da9f6
--- /dev/null
+++ b/project/AssemblyLicenseCheck.scala
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.io.IOException
+import java.nio.charset.StandardCharsets
+
+import sbt._
+import sbt.io.Using
+import sbt.Keys._
+import sbtassembly.AssemblyKeys._
+import org.mdedetrich.apache.sonatype.ApacheSonatypePlugin.autoImport.{
+  apacheSonatypeLicenseFile,
+  apacheSonatypeNoticeFile
+}
+
+/**
+ * Guards the LICENSE and NOTICE files that ship in the assembly jars.
+ *
+ * The assembly jars bundle classes from 3rd party projects, so their 
`META-INF/LICENSE` and
+ * `META-INF/NOTICE` have to account for those projects. Those files are 
curated by hand in
+ * `legal/`, because most of the bundled jars carry no NOTICE of their own and 
the text has to be
+ * taken from the upstream projects. These tasks stop the curated files from 
drifting away from
+ * what the jars actually bundle.
+ */
+object AssemblyLicenseCheck {
+
+  val assemblyBundledModules =
+    taskKey[Seq[String]]("The `organization:name` ids of the 3rd party modules 
bundled into the assembly jar")
+  val assemblyLicenseCheck =
+    taskKey[Unit]("Check that every module bundled into the assembly jar is 
named in the assembly LICENSE file")
+  val assemblyMetaInfArchives =
+    taskKey[Seq[File]]("The published archives that should carry the assembly 
LICENSE and NOTICE files")
+  val assemblyMetaInfCheck =
+    taskKey[Unit]("Check that the published archives ship the assembly LICENSE 
and NOTICE files in META-INF")
+
+  /**
+   * Text that has to appear in the shipped files. Matching the curated file 
byte for byte only says
+   * the merge strategy picked our copy; these markers say that copy is still 
the file it should be,
+   * and not one that has been emptied or truncated.
+   */
+  private val requiredContent = Map(
+    "META-INF/LICENSE" -> Seq(
+      "Apache License",
+      "Version 2.0, January 2004",
+      "TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION",
+      // the 3rd party section this file exists for, see 
legal/AssemblyLicense.txt
+      "contain classes from 3rd party projects",
+      "Apache License Version 2.0:"),
+    "META-INF/NOTICE" -> Seq(
+      "Apache Pekko gRPC",
+      "The Apache Software Foundation",
+      // the 3rd party section this file exists for, see 
legal/AssemblyNotice.txt
+      "contain classes from 3rd party projects"))
+
+  /** A module id as listed in the assembly LICENSE file: `organization:name`, 
optionally `:version`. */
+  private val ModuleId = 
"""^([a-zA-Z0-9][\w.\-]*):([a-zA-Z0-9][\w.\-]*)(?::.*)?$""".r
+
+  lazy val settings: Seq[Setting[?]] = Seq(
+    assemblyBundledModules := {
+      val suffix = "_" + CrossVersion.binaryScalaVersion(scalaVersion.value)
+      val modules = (assembly / 
fullClasspath).value.flatMap(_.get(moduleID.key))
+      modules
+        .filterNot(_.organization == "org.apache.pekko") // our own modules, 
covered by the ASF header
+        .map { module =>
+          val name = if (module.name.endsWith(suffix)) 
module.name.dropRight(suffix.length) else module.name
+          s"${module.organization}:$name"
+        }
+        .distinct
+        .sorted
+    },
+    assemblyLicenseCheck := {
+      val log = streams.value.log
+      val licenseFile = apacheSonatypeLicenseFile.value
+      val listed = IO.readLines(licenseFile).map(_.trim).collect { case 
ModuleId(org, module) => s"$org:$module" }.toSet
+      val bundled = assemblyBundledModules.value
+
+      val extra = (listed -- bundled).toSeq.sorted
+      if (extra.nonEmpty)
+        // the assembly jars are cross built, so a module can be listed for a 
Scala version other than this one
+        log.warn(
+          s"${licenseFile.getName} lists modules that ${name.value} does not 
bundle for Scala ${scalaVersion.value}, " +
+          s"check whether they are still needed: ${extra.mkString(", ")}")
+
+      val missing = bundled.filterNot(listed)
+      if (missing.nonEmpty)
+        sys.error(
+          s"${name.value} bundles modules that ${licenseFile.getName} does not 
account for: ${missing.mkString(", ")}")
+
+      log.info(s"${licenseFile.getName} accounts for all ${bundled.size} 
modules bundled by ${name.value}")
+    },
+    assemblyMetaInfArchives := Seq(assembly.value),
+    assemblyMetaInfCheck := {
+      val log = streams.value.log
+      val expected =
+        Seq("META-INF/LICENSE" -> apacheSonatypeLicenseFile.value, 
"META-INF/NOTICE" -> apacheSonatypeNoticeFile.value)
+
+      assemblyMetaInfArchives.value.foreach { archive =>
+        val problems =
+          try Using.zipFile(archive) { zip =>
+              expected.flatMap { case (path, file) =>
+                Option(zip.getEntry(path)) match {
+                  case None =>
+                    Some(s"$path is missing, expected a copy of $file")
+                  case Some(entry) =>
+                    val actual = Using.zipEntry(zip)(entry)(_.readAllBytes())
+                    if (!actual.sameElements(IO.readBytes(file))) Some(s"$path 
does not match $file")
+                    else {
+                      val text = new String(actual, StandardCharsets.UTF_8)
+                      val absent = requiredContent.getOrElse(path, 
Nil).filterNot(text.contains)
+                      if (absent.isEmpty) None
+                      else Some(s"$path does not mention ${absent.map(marker 
=> s"'$marker'").mkString(", ")}")
+                    }
+                }
+              }
+            }
+          catch {
+            // the archive name is not part of the messages the zip classes 
throw
+            case e: IOException => sys.error(s"${archive.getName} could not be 
read as a zip archive: $e")
+          }
+
+        if (problems.nonEmpty)
+          sys.error(s"${archive.getName} has license problems: 
${problems.mkString("; ")}")
+
+        log.info(s"${archive.getName} ships ${expected.map(_._1).mkString(" 
and ")}")
+      }
+    })
+
+}
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index d6f57793..999d0be8 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -110,6 +110,10 @@ object Dependencies {
     Compile.grpcProtobuf,
     Test.scalaTest)
 
+  lazy val scalapbProtocPlugin = l ++= Seq(
+    Compile.scalapbCompilerPlugin,
+    Protobuf.protobufJava) // or else scalapb pulls older version in 
transitively
+
   lazy val runtime = l ++= Seq(
     Compile.scalapbRuntime,
     Protobuf.protobufJava, // or else scalapb pulls older version in 
transitively


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to