From 1e6ec9dfdf1d9ed1e3e5fa6ec4b6e8bdeb4230ea Mon Sep 17 00:00:00 2001 From: Romain Reuillon Date: Tue, 2 Sep 2025 12:26:03 +0200 Subject: [PATCH 1/6] Cross compile to sbt2 --- build.sbt | 18 ++- .../com/github/sbt/osgi/SbtOsgi.scala | 8 +- .../scala-3/com/github/sbt/osgi/SbtOsgi.scala | 122 ++++++++++++++++++ .../scala-3/com/github/sbt/osgi/package.scala | 20 +++ src/main/scala/com/github/sbt/osgi/Osgi.scala | 2 +- .../com/github/sbt/osgi/OsgiSpec.scala | 9 +- .../com/github/sbt/osgi/OsgiSpec.scala | 115 +++++++++++++++++ 7 files changed, 284 insertions(+), 10 deletions(-) rename src/main/{scala => scala-2}/com/github/sbt/osgi/SbtOsgi.scala (97%) create mode 100644 src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala create mode 100644 src/main/scala-3/com/github/sbt/osgi/package.scala rename src/test/{scala => scala-2}/com/github/sbt/osgi/OsgiSpec.scala (97%) create mode 100644 src/test/scala-3/com/github/sbt/osgi/OsgiSpec.scala diff --git a/build.sbt b/build.sbt index 5ba8253..ac5f458 100644 --- a/build.sbt +++ b/build.sbt @@ -1,7 +1,23 @@ lazy val scala212 = "2.12.20" -ThisBuild / crossScalaVersions := Seq(scala212) +lazy val scala3 = "3.7.2" + +ThisBuild / crossScalaVersions := Seq(scala212, scala3) ThisBuild / scalaVersion := scala212 +ThisBuild / (pluginCrossBuild / sbtVersion) := { + scalaBinaryVersion.value match { + case "2.12" => "1.5.8" + case _ => "2.0.0-RC4" + } +} + +ThisBuild / scriptedSbt := { + scalaBinaryVersion.value match { + case "2.12" => "1.10.10" + case _ => "2.0.0-RC4" + } +} + // So that publishLocal doesn't continuously create new versions def versionFmt(out: sbtdynver.GitDescribeOutput): String = { val snapshotSuffix = diff --git a/src/main/scala/com/github/sbt/osgi/SbtOsgi.scala b/src/main/scala-2/com/github/sbt/osgi/SbtOsgi.scala similarity index 97% rename from src/main/scala/com/github/sbt/osgi/SbtOsgi.scala rename to src/main/scala-2/com/github/sbt/osgi/SbtOsgi.scala index 24da9de..1a590d4 100644 --- a/src/main/scala/com/github/sbt/osgi/SbtOsgi.scala +++ b/src/main/scala-2/com/github/sbt/osgi/SbtOsgi.scala @@ -16,8 +16,8 @@ package com.github.sbt.osgi -import sbt._ -import sbt.Keys._ +import sbt.* +import sbt.Keys.* import sbt.plugins.JvmPlugin object SbtOsgi extends AutoPlugin { @@ -43,7 +43,7 @@ object SbtOsgi extends AutoPlugin { } lazy val defaultOsgiSettings: Seq[Setting[_]] = { - import OsgiKeys._ + import OsgiKeys.* Seq( bundle := Osgi.bundleTask( manifestHeaders.value, @@ -86,7 +86,7 @@ object SbtOsgi extends AutoPlugin { } lazy val defaultGlobalSettings: Seq[Setting[_]] = { - import OsgiKeys._ + import OsgiKeys.* Seq( bundle := file(""), bundleActivator := None, diff --git a/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala b/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala new file mode 100644 index 0000000..df04536 --- /dev/null +++ b/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala @@ -0,0 +1,122 @@ +/* + * Copyright 2011-2013 Typesafe Inc. + * + * Licensed 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. + */ + +package com.github.sbt.osgi + +import sbt._ +import sbt.Keys._ +import sbt.plugins.JvmPlugin + +object SbtOsgi extends AutoPlugin { + + override lazy val trigger: PluginTrigger = noTrigger + + override lazy val requires: Plugins = JvmPlugin + + override lazy val projectSettings: Seq[Def.Setting[_]] = defaultOsgiSettings + + override lazy val globalSettings: Seq[Def.Setting[_]] = defaultGlobalSettings + + object autoImport { + type OsgiManifestHeaders = com.github.sbt.osgi.OsgiManifestHeaders + + val OsgiKeys = com.github.sbt.osgi.OsgiKeys + + lazy val osgiSettings: Seq[Setting[_]] = Seq( + Compile / packageBin / packagedArtifact := Def.uncached: + val conv = fileConverter.value + Scoped + .mkTuple2( + (Compile / packageBin / artifact).value, + conv.toVirtualFile(OsgiKeys.bundle.value.toPath) + ), + Compile / packageBin / artifact ~= (_.withType("bundle")) + ) + } + + lazy val defaultOsgiSettings: Seq[Setting[_]] = { + import OsgiKeys._ + Seq( + bundle := Def.uncached: + val conv = fileConverter.value + Osgi.bundleTask( + manifestHeaders.value, + additionalHeaders.value, + (Compile / dependencyClasspathAsJars).value.map(_.data).map(r => conv.toPath(r).toFile) ++ (Compile / products).value, + conv.toPath((Compile / packageBin / artifactPath).value).toFile, + (Compile / resourceDirectories).value, + embeddedJars.value, + explodedJars.value, + failOnUndecidedPackage.value, + (Compile / sourceDirectories).value, + (Compile / packageBin / packageOptions).value, + packageWithJVMJar.value, + cacheStrategy.value, + streams.value + ), + manifestHeaders := Def.uncached: + OsgiManifestHeaders( + bundleActivator.value, + description.value, + apiURL.value.map(_.toURL), + licenses.value.map(l => (l.spdxId, l.uri.toURL)), + name.value, + bundleRequiredExecutionEnvironment.value, + organizationName.value, + bundleSymbolicName.value, + bundleVersion.value, + dynamicImportPackage.value, + exportPackage.value, + importPackage.value, + fragmentHost.value, + privatePackage.value, + requireBundle.value, + requireCapability.value + ), + Compile / sbt.Keys.packageBin := Def.uncached: + val conv = fileConverter.value + conv.toVirtualFile(bundle.value.toPath) + , + bundleSymbolicName := Osgi.defaultBundleSymbolicName(organization.value, normalizedName.value), + privatePackage := bundleSymbolicName(name => List(name + ".*")).value, + bundleVersion := version.value + ) + } + + lazy val defaultGlobalSettings: Seq[Setting[_]] = { + import OsgiKeys._ + Seq( + bundle := file(""), + bundleActivator := None, + bundleSymbolicName := "", + bundleVersion := "", + bundleRequiredExecutionEnvironment := Nil, + dynamicImportPackage := Nil, + exportPackage := Nil, + importPackage := List("*"), + fragmentHost := None, + privatePackage := Nil, + requireBundle := Nil, + failOnUndecidedPackage := false, + requireCapability := Osgi.requireCapabilityTask, + additionalHeaders := Map.empty, + embeddedJars := Nil, + explodedJars := Nil, + packageWithJVMJar := false, + cacheStrategy := None + ) + } +} diff --git a/src/main/scala-3/com/github/sbt/osgi/package.scala b/src/main/scala-3/com/github/sbt/osgi/package.scala new file mode 100644 index 0000000..118361e --- /dev/null +++ b/src/main/scala-3/com/github/sbt/osgi/package.scala @@ -0,0 +1,20 @@ +package com.github.sbt.osgi + +/* + * Copyright (C) 2025 Romain Reuillon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +type ManifestAttributes = sbt.PackageOption.ManifestAttributes diff --git a/src/main/scala/com/github/sbt/osgi/Osgi.scala b/src/main/scala/com/github/sbt/osgi/Osgi.scala index 7072d43..2dc64d0 100644 --- a/src/main/scala/com/github/sbt/osgi/Osgi.scala +++ b/src/main/scala/com/github/sbt/osgi/Osgi.scala @@ -307,7 +307,7 @@ private object Osgi { def seqToStrOpt[A](seq: Seq[A])(f: A => String): Option[String] = if (seq.isEmpty) None else Some(seq map f mkString ",") - def strToStrOpt(s: String): Option[String] = Option(s).filter(_.trim nonEmpty) + def strToStrOpt(s: String): Option[String] = Option(s).filter(_.trim.nonEmpty) def includeResourceProperty(resourceDirectories: Seq[File], embeddedJars: Seq[File], explodedJars: Seq[File]) = { val paths: Seq[String] = diff --git a/src/test/scala/com/github/sbt/osgi/OsgiSpec.scala b/src/test/scala-2/com/github/sbt/osgi/OsgiSpec.scala similarity index 97% rename from src/test/scala/com/github/sbt/osgi/OsgiSpec.scala rename to src/test/scala-2/com/github/sbt/osgi/OsgiSpec.scala index 571996d..18ed8bf 100644 --- a/src/test/scala/com/github/sbt/osgi/OsgiSpec.scala +++ b/src/test/scala-2/com/github/sbt/osgi/OsgiSpec.scala @@ -16,14 +16,15 @@ package com.github.sbt.osgi -import aQute.bnd.osgi.Constants._ -import java.io.File +import aQute.bnd.osgi.Constants.* import org.specs2.mutable.Specification -import scala.collection.JavaConverters._ + +import java.io.File +import scala.collection.JavaConverters.* class OsgiSpec extends Specification { - import Osgi._ + import Osgi.* "Calling seqToStrOpt" should { "return None for an empty Seq" in { diff --git a/src/test/scala-3/com/github/sbt/osgi/OsgiSpec.scala b/src/test/scala-3/com/github/sbt/osgi/OsgiSpec.scala new file mode 100644 index 0000000..38dfe41 --- /dev/null +++ b/src/test/scala-3/com/github/sbt/osgi/OsgiSpec.scala @@ -0,0 +1,115 @@ +/* + * Copyright 2011 Typesafe Inc. + * + * Licensed 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. + */ + +package com.github.sbt.osgi + +import aQute.bnd.osgi.Constants.* +import org.specs2.mutable.Specification + +import java.io.File +import scala.collection.JavaConverters.* + +class OsgiSpec extends Specification { + + import Osgi.* + + "Calling seqToStrOpt" should { + "return None for an empty Seq" in { + seqToStrOpt(Nil)(id) must be(None) + } + "return Some wrapping a properly constructed String for a non-empty Seq" in { + seqToStrOpt(Seq("foo", "bar", "baz"))(id) must beSome("foo,bar,baz") + seqToStrOpt(Seq(1, 2, 3))(_.toString) must beSome("1,2,3") + } + } + + "Calling headersToProperties" should { + "return the proper properties" in { + val headers = OsgiManifestHeaders( + Some("bundleActivator"), + "bundleDescription", + Some(sbt.url("http://example.example").toURL), + Seq(("License", sbt.url("http://license.license").toURL)), + "bundleName", + Seq("req1", "req2"), + "bundleVendor", + "bundleSymbolicName", + "bundleVersion", + Seq("dynamicImportPackage"), + Seq("exportPackage1", "exportPackage2", "exportPackage3"), + Seq("importPackage"), + None, + Seq("privatePackage"), + Nil, + "requireCapability" + ) + val properties = headersToProperties(headers, Map.empty) + properties.asScala must havePairs( + BUNDLE_ACTIVATOR -> "bundleActivator", + BUNDLE_SYMBOLICNAME -> "bundleSymbolicName", + BUNDLE_DESCRIPTION -> "bundleDescription", + BUNDLE_DOCURL -> sbt.url("http://example.example").toString, + BUNDLE_LICENSE -> "http://license.license;description=License", + BUNDLE_NAME -> "bundleName", + BUNDLE_REQUIREDEXECUTIONENVIRONMENT -> "req1,req2", + BUNDLE_VENDOR -> "bundleVendor", + BUNDLE_VERSION -> "bundleVersion", + DYNAMICIMPORT_PACKAGE -> "dynamicImportPackage", + EXPORT_PACKAGE -> "exportPackage1,exportPackage2,exportPackage3", + IMPORT_PACKAGE -> "importPackage", + PRIVATE_PACKAGE -> "privatePackage", + REQUIRE_CAPABILITY -> "requireCapability" + ) + properties.asScala must not(haveKey(FRAGMENT_HOST)) + properties.asScala must not(haveKey(REQUIRE_BUNDLE)) + } + } + + "Calling defaultBundleSymbolicName" should { + "concatenate organization and name properly" in { + defaultBundleSymbolicName("a.b.c", "d.c") must beEqualTo("a.b.c.d.c") + defaultBundleSymbolicName("a.b.c", "d-c") must beEqualTo("a.b.c.d.c") + defaultBundleSymbolicName("a.b.c", "c.d") must beEqualTo("a.b.c.d") + defaultBundleSymbolicName("a.b.c", "c-d") must beEqualTo("a.b.c.d") + defaultBundleSymbolicName("", "a") must beEqualTo("a") + defaultBundleSymbolicName("a", "") must beEqualTo("a") + defaultBundleSymbolicName("", "") must beEqualTo("") + } + } + + "Calling includeResourceProperty" should { + "add resources and embedded jars and exploded jars to INCLUDERESOURCE" in { + val resourceDir = new File("/resource") + val jar = new File("/aJar.jar") + val anotherJar = new File("/anotherJar.jar") + val actual = includeResourceProperty(Seq(resourceDir), Seq(jar), Seq(anotherJar)) + actual must beSome(resourceDir.getAbsolutePath + "," + jar.getAbsolutePath + ",@" + anotherJar.getAbsolutePath) + } + } + + "Calling bundleClasspathProperty" should { + "add bundle classes and embedded jars to classpath" in { + val embeddedJars = Seq(new File("/aJar.jar"), new File("/bJar.jar")) + val actual = bundleClasspathProperty(embeddedJars) + actual must beSome(".,aJar.jar,bJar.jar") + } + "remain default if no embedded jars are specified" in { + val embeddedJars = Seq() + val actual = bundleClasspathProperty(embeddedJars) + actual must beNone + } + } +} From d376ed2ed2fdf2b59f0b0ab9c64ae77a6aaaf0ec Mon Sep 17 00:00:00 2001 From: Romain Reuillon Date: Tue, 2 Sep 2025 21:55:21 +0200 Subject: [PATCH 2/6] Generate github action yaml --- .github/workflows/ci.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c07fc4..d0e0e11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - scala: [2.12.20] + scala: [2.12.20, 3.7.2] java: [temurin@8, temurin@11, temurin@17, temurin@21] exclude: - java: temurin@8 @@ -173,6 +173,16 @@ jobs: tar xf targets.tar rm targets.tar + - name: Download target directories (3.7.2) + uses: actions/download-artifact@v4 + with: + name: target-${{ matrix.os }}-3.7.2-${{ matrix.java }} + + - name: Inflate target directories (3.7.2) + run: | + tar xf targets.tar + rm targets.tar + - name: Publish project env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} From dbdda615bce841f0216ed9a30e95ce1fc675b511 Mon Sep 17 00:00:00 2001 From: Romain Reuillon Date: Wed, 3 Sep 2025 08:51:53 +0200 Subject: [PATCH 3/6] Update scalafmt dialect --- .scalafmt.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scalafmt.conf b/.scalafmt.conf index 44fb28e..fdd3b13 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,5 +1,5 @@ version = 3.9.9 -runner.dialect = scala212 +runner.dialect = Scala212Source3 maxColumn = 120 project.git = true From 0d4569fc3c10ab2201d594f2a60c35546a35b827 Mon Sep 17 00:00:00 2001 From: Romain Reuillon Date: Wed, 3 Sep 2025 08:52:43 +0200 Subject: [PATCH 4/6] Fix sbt version --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ac5f458..9999f31 100644 --- a/build.sbt +++ b/build.sbt @@ -6,7 +6,7 @@ ThisBuild / scalaVersion := scala212 ThisBuild / (pluginCrossBuild / sbtVersion) := { scalaBinaryVersion.value match { - case "2.12" => "1.5.8" + case "2.12" => "1.9.9" case _ => "2.0.0-RC4" } } From 5c9765af7915d4d6f44cd9986e3736ff8da612a2 Mon Sep 17 00:00:00 2001 From: Romain Reuillon Date: Wed, 3 Sep 2025 08:57:51 +0200 Subject: [PATCH 5/6] =?UTF-8?q?Use=20scala=202=C2=A0compatible=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scala-3/com/github/sbt/osgi/SbtOsgi.scala | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala b/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala index df04536..a9f1f2b 100644 --- a/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala +++ b/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala @@ -36,13 +36,14 @@ object SbtOsgi extends AutoPlugin { val OsgiKeys = com.github.sbt.osgi.OsgiKeys lazy val osgiSettings: Seq[Setting[_]] = Seq( - Compile / packageBin / packagedArtifact := Def.uncached: + Compile / packageBin / packagedArtifact := Def.uncached { val conv = fileConverter.value Scoped .mkTuple2( (Compile / packageBin / artifact).value, conv.toVirtualFile(OsgiKeys.bundle.value.toPath) - ), + ) + }, Compile / packageBin / artifact ~= (_.withType("bundle")) ) } @@ -50,7 +51,7 @@ object SbtOsgi extends AutoPlugin { lazy val defaultOsgiSettings: Seq[Setting[_]] = { import OsgiKeys._ Seq( - bundle := Def.uncached: + bundle := Def.uncached { val conv = fileConverter.value Osgi.bundleTask( manifestHeaders.value, @@ -66,8 +67,9 @@ object SbtOsgi extends AutoPlugin { packageWithJVMJar.value, cacheStrategy.value, streams.value - ), - manifestHeaders := Def.uncached: + ) + }, + manifestHeaders := Def.uncached { OsgiManifestHeaders( bundleActivator.value, description.value, @@ -85,7 +87,8 @@ object SbtOsgi extends AutoPlugin { privatePackage.value, requireBundle.value, requireCapability.value - ), + ) + }, Compile / sbt.Keys.packageBin := Def.uncached: val conv = fileConverter.value conv.toVirtualFile(bundle.value.toPath) From 24079d028236c42a8c1d78d9136bdc5f8346fb91 Mon Sep 17 00:00:00 2001 From: Romain Reuillon Date: Wed, 3 Sep 2025 08:58:59 +0200 Subject: [PATCH 6/6] =?UTF-8?q?Use=20scala=202=C2=A0compatible=20syntax?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala b/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala index a9f1f2b..9f6db1e 100644 --- a/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala +++ b/src/main/scala-3/com/github/sbt/osgi/SbtOsgi.scala @@ -89,10 +89,10 @@ object SbtOsgi extends AutoPlugin { requireCapability.value ) }, - Compile / sbt.Keys.packageBin := Def.uncached: + Compile / sbt.Keys.packageBin := Def.uncached { val conv = fileConverter.value conv.toVirtualFile(bundle.value.toPath) - , + }, bundleSymbolicName := Osgi.defaultBundleSymbolicName(organization.value, normalizedName.value), privatePackage := bundleSymbolicName(name => List(name + ".*")).value, bundleVersion := version.value