├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .scalafmt.conf ├── README.md ├── build.sbt ├── modules ├── cli │ └── src │ │ ├── main │ │ └── scala │ │ │ └── packager │ │ │ └── cli │ │ │ ├── PackagerCli.scala │ │ │ └── commands │ │ │ ├── Build.scala │ │ │ ├── BuildOptions.scala │ │ │ ├── DebianOptions.scala │ │ │ ├── DockerOptions.scala │ │ │ ├── MacOSOptions.scala │ │ │ ├── RedHatOptions.scala │ │ │ ├── SettingsHelpers.scala │ │ │ ├── SharedOptions.scala │ │ │ └── WindowsOptions.scala │ │ └── test │ │ └── scala │ │ └── packager │ │ └── cli │ │ └── DummyTests.scala ├── image-resizer │ └── src │ │ ├── main │ │ └── scala │ │ │ └── packager │ │ │ └── windows │ │ │ └── DefaultImageResizer.scala │ │ └── test │ │ └── scala │ │ └── packager │ │ └── windows │ │ ├── WindowsPackageTests.scala │ │ └── WixLogoSpec.scala └── packager │ └── src │ ├── main │ └── scala │ │ └── packager │ │ ├── FileUtils.scala │ │ ├── NativePackager.scala │ │ ├── Packager.scala │ │ ├── config │ │ ├── BuildSettings.scala │ │ ├── DebianSettings.scala │ │ ├── DockerSettings.scala │ │ ├── MacOSSettings.scala │ │ ├── NativeSettings.scala │ │ ├── RedHatSettings.scala │ │ ├── SharedSettings.scala │ │ └── WindowsSettings.scala │ │ ├── deb │ │ ├── DebianMetaData.scala │ │ ├── DebianPackage.scala │ │ └── DebianPackageInfo.scala │ │ ├── docker │ │ └── DockerPackage.scala │ │ ├── mac │ │ ├── MacOSInfoPlist.scala │ │ ├── MacOSNativePackager.scala │ │ ├── dmg │ │ │ └── DmgPackage.scala │ │ └── pkg │ │ │ └── PkgPackage.scala │ │ ├── rpm │ │ ├── RedHatPackage.scala │ │ └── RedHatSpecPackage.scala │ │ └── windows │ │ ├── ImageResizer.scala │ │ ├── WindowsPackage.scala │ │ ├── WindowsUtils.scala │ │ ├── WindowsWixConfig.scala │ │ └── wix │ │ ├── WixComponent.scala │ │ └── WixId.scala │ └── test │ ├── resources │ └── packager │ │ └── apache-2.0 │ └── scala │ └── packager │ ├── NativePackageHelper.scala │ ├── PackagerHelper.scala │ ├── TestUtils.scala │ ├── deb │ └── DebianPackageTests.scala │ ├── dmg │ └── DmgPackageTests.scala │ ├── docker │ └── DockerPackageTests.scala │ ├── pkg │ └── PkgPackageTests.scala │ └── rpm │ └── RedHatPackageTests.scala └── project ├── Deps.scala ├── ScalaVersions.scala ├── Settings.scala ├── build.properties └── plugins.sbt /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.9.7 2 | 968f7ac8ba831e5beb5ad37db771c0c431e8a349 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - "v*" 8 | pull_request: 9 | 10 | jobs: 11 | test: 12 | runs-on: ${{ matrix.OS }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | OS: [ubuntu-24.04, macos-13, macos-latest, windows-latest] 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | submodules: true 22 | - uses: coursier/cache-action@v6.4 23 | - uses: coursier/setup-action@v1 24 | with: 25 | jvm: 17 26 | apps: 'sbt' 27 | - name: Test 28 | run: sbt +test 29 | - name: Print help 30 | run: sbt "cli/run --help" 31 | 32 | publish: 33 | needs: test 34 | if: github.event_name == 'push' 35 | runs-on: ubuntu-24.04 36 | steps: 37 | - uses: actions/checkout@v4 38 | with: 39 | fetch-depth: 0 40 | submodules: true 41 | - uses: coursier/cache-action@v6.4 42 | - uses: coursier/setup-action@v1 43 | with: 44 | jvm: 17 45 | apps: 'sbt' 46 | - uses: olafurpg/setup-gpg@v3 47 | - name: Release 48 | run: sbt ci-release 49 | env: 50 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 51 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 52 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 53 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .bsp/ 3 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.7" 2 | 3 | align.preset = more 4 | maxColumn = 100 5 | assumeStandardLibraryStripMargin = true 6 | indent.defnSite = 2 7 | indentOperator.topLevelOnly = false 8 | align.preset = more 9 | align.openParenCallSite = false 10 | newlines.source = keep 11 | newlines.beforeMultiline = keep 12 | newlines.afterCurlyLambdaParams = keep 13 | newlines.alwaysBeforeElseAfterCurlyIf = true 14 | 15 | runner.dialect = scala213source3 16 | 17 | rewrite.rules = [ 18 | RedundantBraces 19 | RedundantParens 20 | SortModifiers 21 | ] 22 | 23 | rewrite.redundantBraces { 24 | ifElseExpressions = true 25 | includeUnitMethods = false 26 | stringInterpolation = true 27 | } 28 | 29 | rewrite.sortModifiers.order = [ 30 | "private", "final", "override", "protected", 31 | "implicit", "sealed", "abstract", "lazy" 32 | ] 33 | 34 | project.excludeFilters = [ 35 | ".bloop" 36 | ".metals" 37 | ".scala-build" 38 | "examples" # Scala 3 scripts and using directives not supported yet 39 | "out" 40 | "target" 41 | "scala-version.scala" 42 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scala Packager 2 | Submodule of [Scala CLI](https://github.com/Virtuslab/scala-cli) used to package applications in native formats. 3 | 4 | The main goal of this project is to create simple scala library which packages binary apps in the following formats: 5 | * Linux 6 | * RedHat 7 | * Debian 8 | * MacOS 9 | * Pkg 10 | * Dmg 11 | * Windows 12 | * MSI 13 | * Docker 14 | 15 | ## Modules 16 | The project consists of two dependent modules 17 | 18 | ### Cli 19 | Provides the command line application interface to building native formats. It is used in [scala-cli](https://github.com/VirtusLab/scala-cli/blob/main/.github/scripts/generate-os-packages.sh) for generating os package. 20 | 21 | ### Packager 22 | Core library for generating specific native package. 23 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Settings.project 2 | 3 | inThisBuild( 4 | List( 5 | organization := "org.virtuslab", 6 | homepage := Some(url("https://github.com/VirtusLab/scala-packager")), 7 | licenses := List( 8 | "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") 9 | ), 10 | developers := List( 11 | Developer( 12 | "lwronski", 13 | "Łukasz Wroński", 14 | "", 15 | url("https://github.com/lwronski") 16 | ), 17 | Developer( 18 | "Gedochao", 19 | "Piotr Chabelski", 20 | "pchabelski@virtuslab.com", 21 | url("https://github.com/Gedochao") 22 | ), 23 | Developer( 24 | "tgodzik", 25 | "Tomasz Godzik", 26 | "tgodzik@virtuslab.com", 27 | url("https://github.com/tgodzik") 28 | ) 29 | ) 30 | ) 31 | ) 32 | 33 | lazy val coreDependencies = Seq( 34 | libraryDependencies ++= Seq( 35 | Deps.commonsIo, 36 | Deps.jib, 37 | Deps.osLib 38 | ) 39 | ) 40 | 41 | lazy val imageResizerDependencies = Seq( 42 | libraryDependencies ++= Seq( 43 | Deps.image4j, 44 | Deps.thumbnailator 45 | ) 46 | ) 47 | 48 | lazy val testFramework = Seq( 49 | testFrameworks += new TestFramework("munit.Framework") 50 | ) 51 | 52 | lazy val cliMainClass = Seq( 53 | Compile / mainClass := Some("packager.cli.PackagerCli") 54 | ) 55 | 56 | lazy val compileOptions: Seq[Setting[_]] = Seq( 57 | scalacOptions ++= Seq("-Xfatal-warnings", "-deprecation") 58 | ) 59 | 60 | lazy val packagerProjectSettings = Seq( 61 | name := "scala-packager", 62 | scalaVersion := ScalaVersions.scala213, 63 | crossScalaVersions := ScalaVersions.all 64 | ) 65 | 66 | lazy val imageResizerProjectSettings = Seq( 67 | name := "scala-packager-image-resizer", 68 | scalaVersion := ScalaVersions.scala213, 69 | crossScalaVersions := ScalaVersions.all 70 | ) 71 | 72 | lazy val cliProjectSettings = Seq( 73 | name := "scala-packager-cli", 74 | scalaVersion := ScalaVersions.scala213, 75 | crossScalaVersions := ScalaVersions.all, 76 | libraryDependencies ++= Seq(Deps.caseApp) 77 | ) 78 | 79 | lazy val utest: Seq[Setting[_]] = Seq( 80 | libraryDependencies ++= Seq(Deps.munit % Test, Deps.expecty % Test), 81 | testFrameworks += new TestFramework("munit.Framework") 82 | ) 83 | 84 | lazy val cli = project("cli") 85 | .dependsOn(packager, `image-resizer`) 86 | .settings( 87 | cliProjectSettings, 88 | cliMainClass, 89 | utest, 90 | compileOptions 91 | ) 92 | 93 | lazy val packager = project("packager") 94 | .settings( 95 | packagerProjectSettings, 96 | coreDependencies, 97 | utest, 98 | compileOptions 99 | ) 100 | 101 | lazy val `image-resizer` = project("image-resizer") 102 | .dependsOn(packager, packager % "test->test") 103 | .settings( 104 | imageResizerProjectSettings, 105 | imageResizerDependencies, 106 | utest, 107 | compileOptions 108 | ) 109 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/PackagerCli.scala: -------------------------------------------------------------------------------- 1 | package packager.cli 2 | 3 | import caseapp.core.app.{Command, CommandsEntryPoint} 4 | import packager.cli.commands.Build 5 | 6 | object PackagerCli extends CommandsEntryPoint { 7 | 8 | final override def defaultCommand: Option[Command[_]] = Some(Build) 9 | 10 | override def enableCompleteCommand = true 11 | override def enableCompletionsCommand = true 12 | 13 | def commands: Seq[Command[_]] = 14 | Seq(Build) 15 | 16 | override def progName: String = "packager" 17 | } 18 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/Build.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.core.RemainingArgs 4 | import caseapp.core.app.Command 5 | import BuildOptions.NativePackagerType._ 6 | import caseapp.core.parser.Parser 7 | import packager.cli.commands.BuildOptions.PackagerType.Docker 8 | import packager.config.SharedSettings 9 | import packager.deb.DebianPackage 10 | import packager.docker.DockerPackage 11 | import packager.mac.dmg.DmgPackage 12 | import packager.mac.pkg.PkgPackage 13 | import packager.rpm.RedHatPackage 14 | import packager.windows.{DefaultImageResizer, WindowsPackage} 15 | 16 | object Build extends Command[BuildOptions] { 17 | override def name: String = "build" 18 | override def run( 19 | options: BuildOptions, 20 | remainingArgs: RemainingArgs 21 | ): Unit = { 22 | 23 | val pwd = os.pwd 24 | val destinationFileName = options.output.getOrElse(options.defaultName) 25 | 26 | val sourceAppPath: os.Path = os.Path(options.sourceAppPath, pwd) 27 | val destinationPath: os.Path = os.Path(destinationFileName, pwd) 28 | val workingDirectoryPath = options.workingDirectory.map(os.Path(_, pwd)) 29 | 30 | val sharedSettings: SharedSettings = SharedSettings( 31 | sourceAppPath = sourceAppPath, 32 | force = options.force, 33 | version = options.sharedOptions.version, 34 | workingDirectoryPath = workingDirectoryPath, 35 | outputPath = destinationPath, 36 | launcherApp = options.sharedOptions.launcherApp, 37 | logoPath = options.sharedOptions.logoPath.map(os.Path(_, pwd)) 38 | ) 39 | 40 | def alreadyExistsCheck(): Unit = 41 | if (!options.force && os.exists(destinationPath)) { 42 | System.err.println( 43 | s"Error: $destinationPath already exists. Pass -f or --force to force erasing it." 44 | ) 45 | sys.exit(1) 46 | } 47 | 48 | alreadyExistsCheck() 49 | 50 | options.packagerType match { 51 | case Some(Debian) => 52 | DebianPackage(options.toDebianSettings(sharedSettings)).build() 53 | case Some(Msi) => 54 | WindowsPackage( 55 | options.toWindowsSettings(sharedSettings), 56 | imageResizerOpt = Some(DefaultImageResizer) 57 | ).build() 58 | case Some(Dmg) => 59 | DmgPackage(options.toMacOSSettings(sharedSettings)).build() 60 | case Some(Pkg) => 61 | PkgPackage(options.toMacOSSettings(sharedSettings)).build() 62 | case Some(Rpm) => 63 | RedHatPackage(options.toRedHatSettings(sharedSettings)).build() 64 | case Some(Docker) => 65 | DockerPackage(sourceAppPath, options.toDockerSettings).build() 66 | case None => () 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/BuildOptions.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.{Group, HelpMessage, Name, Parser, Recurse} 4 | import caseapp.core.help.Help 5 | import packager.cli.commands.BuildOptions.PackagerType 6 | import packager.config._ 7 | 8 | final case class BuildOptions( 9 | @Group("Packager") 10 | @HelpMessage("Set destination path") 11 | @Name("o") 12 | output: Option[String] = None, 13 | @Recurse 14 | sharedOptions: SharedOptions = SharedOptions(), 15 | @Recurse 16 | debian: DebianOptions = DebianOptions(), 17 | @Recurse 18 | redHat: RedHatOptions = RedHatOptions(), 19 | @Recurse 20 | macOS: MacOSOptions = MacOSOptions(), 21 | @Recurse 22 | windows: WindowsOptions = WindowsOptions(), 23 | @Recurse 24 | dockerOptions: DockerOptions = DockerOptions(), 25 | @Group("Packager") 26 | @HelpMessage("Overwrite destination file if it exists") 27 | @Name("f") 28 | force: Boolean = false, 29 | @Group("Packager") 30 | @HelpMessage("Set working directory path") 31 | @Name("w") 32 | workingDirectory: Option[String] = None, 33 | @Group("Packager") 34 | @HelpMessage("Source app path") 35 | @Name("a") 36 | sourceAppPath: String, 37 | @Group("Packager") 38 | @HelpMessage("Build debian package, available only on linux") 39 | deb: Boolean = false, 40 | @Group("Packager") 41 | @HelpMessage("Build rpm package, available only on linux") 42 | rpm: Boolean = false, 43 | @Group("Packager") 44 | @HelpMessage("Build msi package, available only on windows") 45 | msi: Boolean = false, 46 | @Group("Packager") 47 | @HelpMessage("Build dmg package, available only on centOS") 48 | dmg: Boolean = false, 49 | @Group("Packager") 50 | @HelpMessage("Build pkg package, available only on centOS") 51 | pkg: Boolean = false, 52 | @Group("Packager") 53 | @HelpMessage("Build docker image") 54 | docker: Boolean = false 55 | ) { 56 | 57 | import BuildOptions.NativePackagerType 58 | def packagerType: Option[PackagerType] = 59 | if (deb) Some(NativePackagerType.Debian) 60 | else if (rpm) Some(NativePackagerType.Rpm) 61 | else if (msi) Some(NativePackagerType.Msi) 62 | else if (dmg) Some(NativePackagerType.Dmg) 63 | else if (pkg) Some(NativePackagerType.Pkg) 64 | else if (docker) Some(PackagerType.Docker) 65 | else None 66 | 67 | def defaultName: String = 68 | if (deb) "app.deb" 69 | else if (rpm) "app.rpm" 70 | else if (msi) "app.msi" 71 | else if (dmg) "app.dmg" 72 | else if (pkg) "app.pkg" 73 | else if (msi) "app.msi" 74 | else "app" 75 | 76 | def toDebianSettings(sharedSettings: SharedSettings): DebianSettings = 77 | debian.toDebianSettings( 78 | sharedSettings, 79 | sharedOptions.maintainer, 80 | sharedOptions.description 81 | ) 82 | 83 | def toWindowsSettings(sharedSettings: SharedSettings): WindowsSettings = 84 | windows.toWindowsSettings(sharedSettings, sharedOptions.maintainer) 85 | 86 | def toMacOSSettings(sharedSettings: SharedSettings): MacOSSettings = 87 | macOS.toMacOSSettings(sharedSettings) 88 | 89 | def toRedHatSettings(sharedSettings: SharedSettings): RedHatSettings = 90 | redHat.toRedHatSettings(sharedSettings, sharedOptions.description) 91 | 92 | def toDockerSettings: DockerSettings = dockerOptions.toDockerSettings 93 | } 94 | 95 | object BuildOptions { 96 | 97 | sealed abstract class PackagerType extends Product with Serializable 98 | case object PackagerType { 99 | case object Docker extends PackagerType 100 | } 101 | sealed abstract class NativePackagerType extends PackagerType 102 | case object NativePackagerType { 103 | case object Debian extends NativePackagerType 104 | case object Msi extends NativePackagerType 105 | case object Dmg extends NativePackagerType 106 | case object Pkg extends NativePackagerType 107 | case object Rpm extends NativePackagerType 108 | } 109 | 110 | lazy val parser: Parser[BuildOptions] = Parser.derive 111 | implicit lazy val parserAux: Parser.Aux[BuildOptions, parser.D] = parser 112 | implicit lazy val help: Help[BuildOptions] = Help.derive 113 | } 114 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/DebianOptions.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.core.help.Help 4 | import caseapp.{Group, HelpMessage, Parser, ValueDescription} 5 | import packager.cli.commands.SettingsHelpers.Mandatory 6 | import packager.config.{DebianSettings, SharedSettings} 7 | 8 | final case class DebianOptions( 9 | @Group("Debian") 10 | @HelpMessage( 11 | "The list of debian package that this package is absolute incompatibility" 12 | ) 13 | @ValueDescription("debian dependencies conflicts") 14 | debianConflicts: List[String] = Nil, 15 | @Group("Debian") 16 | @HelpMessage("The list of debian package that this package depends on") 17 | @ValueDescription("debian dependencies") 18 | debianDependencies: List[String] = Nil, 19 | @Group("Debian") 20 | @HelpMessage( 21 | "Architecture that are supported by the repository, default: all" 22 | ) 23 | debArchitecture: String = "all", 24 | @Group("Debian") 25 | @HelpMessage( 26 | "This field represents how important it is that the user have the package installed" 27 | ) 28 | priority: Option[String] = None, 29 | @Group("Debian") 30 | @HelpMessage( 31 | "This field specifies an application area into which the package has been classified" 32 | ) 33 | section: Option[String] = None 34 | ) { 35 | def toDebianSettings( 36 | sharedSettings: SharedSettings, 37 | maintainer: Option[String], 38 | description: Option[String] 39 | ): DebianSettings = 40 | DebianSettings( 41 | shared = sharedSettings, 42 | maintainer = maintainer.mandatory( 43 | "Maintainer parameter is mandatory for debian package" 44 | ), 45 | description = description.mandatory( 46 | "Description parameter is mandatory for debian package" 47 | ), 48 | debianConflicts = debianConflicts, 49 | debianDependencies = debianDependencies, 50 | architecture = debArchitecture, 51 | priority = priority, 52 | section = section 53 | ) 54 | } 55 | 56 | case object DebianOptions { 57 | lazy val parser: Parser[DebianOptions] = Parser.derive 58 | implicit lazy val parserAux: Parser.Aux[DebianOptions, parser.D] = parser 59 | implicit lazy val help: Help[DebianOptions] = Help.derive 60 | } 61 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/DockerOptions.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.core.help.Help 4 | import caseapp.{Group, HelpMessage, Parser, ValueDescription} 5 | import packager.cli.commands.SettingsHelpers.Mandatory 6 | import packager.config.DockerSettings 7 | import java.nio.file.Path; 8 | import java.nio.file.Paths; 9 | 10 | final case class DockerOptions( 11 | @Group("Docker") 12 | @HelpMessage( 13 | "Building the container from base image" 14 | ) 15 | @ValueDescription("ubuntu|ubuntu:latest|adoptopenjdk/openjdk8:debian-jre|…") 16 | from: Option[String] = None, 17 | @Group("Docker") 18 | @HelpMessage( 19 | "The image registry, if empty the default registry will be used (Docker Hub)" 20 | ) 21 | registry: Option[String] = None, 22 | @Group("Docker") 23 | @HelpMessage( 24 | "The image repository" 25 | ) 26 | repository: Option[String] = None, 27 | @Group("Docker") 28 | @HelpMessage( 29 | "The image tag, the default tag is latest" 30 | ) 31 | tag: Option[String] = None, 32 | @Group("Docker") 33 | @HelpMessage( 34 | "Executable that will run an application, default sh" 35 | ) 36 | exec: Option[String] = Some("sh"), 37 | @Group("Docker") 38 | @HelpMessage( 39 | "docker executable that will be used. ex. docker, podman" 40 | ) 41 | @ValueDescription("docker") 42 | dockerExecutable: Option[Path] = Some(Paths.get("docker")) 43 | ) { 44 | def toDockerSettings: DockerSettings = 45 | DockerSettings( 46 | from = from.mandatory( 47 | "Maintainer parameter is mandatory for docker image" 48 | ), 49 | registry = registry, 50 | repository = repository.mandatory( 51 | "Repository parameter is mandatory for docker image" 52 | ), 53 | tag = tag, 54 | exec = exec, 55 | dockerExecutable = dockerExecutable 56 | ) 57 | } 58 | 59 | case object DockerOptions { 60 | 61 | lazy val parser: Parser[DockerOptions] = Parser.derive 62 | implicit lazy val parserAux: Parser.Aux[DockerOptions, parser.D] = parser 63 | implicit lazy val help: Help[DockerOptions] = Help.derive 64 | 65 | } 66 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/MacOSOptions.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.{Group, HelpMessage, Parser} 4 | import caseapp.core.help.Help 5 | import packager.config.{MacOSSettings, SharedSettings} 6 | import SettingsHelpers._ 7 | 8 | final case class MacOSOptions( 9 | @Group("MacOS") 10 | @HelpMessage( 11 | "CF Bundle Identifier" 12 | ) 13 | identifier: Option[String] = None 14 | ) { 15 | def toMacOSSettings(sharedSettings: SharedSettings): MacOSSettings = 16 | MacOSSettings( 17 | shared = sharedSettings, 18 | identifier = identifier.mandatory( 19 | "Identifier parameter is mandatory for macOS packages" 20 | ) 21 | ) 22 | } 23 | 24 | case object MacOSOptions { 25 | lazy val parser: Parser[MacOSOptions] = Parser.derive 26 | implicit lazy val parserAux: Parser.Aux[MacOSOptions, parser.D] = parser 27 | implicit lazy val help: Help[MacOSOptions] = Help.derive 28 | } 29 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/RedHatOptions.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.{Group, HelpMessage, Parser} 4 | import caseapp.core.help.Help 5 | import packager.config.{RedHatSettings, SharedSettings} 6 | import SettingsHelpers._ 7 | 8 | final case class RedHatOptions( 9 | @Group("RedHat") 10 | @HelpMessage( 11 | "License that are supported by the repository - list of licenses https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing" 12 | ) 13 | license: Option[String] = None, 14 | @Group("RedHat") 15 | @HelpMessage( 16 | "The number of times this version of the software was released, default: 1" 17 | ) 18 | release: String = "1", 19 | @HelpMessage( 20 | "Architecture that are supported by the repository, default: noarch" 21 | ) 22 | rpmArchitecture: String = "noarch" 23 | ) { 24 | 25 | def toRedHatSettings( 26 | sharedSettings: SharedSettings, 27 | description: Option[String] 28 | ): RedHatSettings = 29 | RedHatSettings( 30 | shared = sharedSettings, 31 | description = description.mandatory( 32 | "Description parameter is mandatory for debian package" 33 | ), 34 | license = license.mandatory( 35 | "License path parameter is mandatory for redHat package" 36 | ), 37 | release = release, 38 | rpmArchitecture = rpmArchitecture 39 | ) 40 | } 41 | 42 | case object RedHatOptions { 43 | lazy val parser: Parser[RedHatOptions] = Parser.derive 44 | implicit lazy val parserAux: Parser.Aux[RedHatOptions, parser.D] = parser 45 | implicit lazy val help: Help[RedHatOptions] = Help.derive 46 | } 47 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/SettingsHelpers.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | object SettingsHelpers { 4 | implicit class Mandatory[A](x: Option[A]) { 5 | def mandatory(error: String): A = 6 | x match { 7 | case Some(v) => v 8 | case None => 9 | System.err.println(error) 10 | sys.exit(1) 11 | } 12 | } 13 | 14 | implicit class Validate[A](x: Option[A]) { 15 | def validate(cond: A => Boolean, error: String): Option[A] = 16 | x match { 17 | case Some(v) => 18 | if (cond(v)) Some(v) 19 | else { 20 | System.err.println(error) 21 | sys.exit(1) 22 | } 23 | case None => None 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/SharedOptions.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.{Group, HelpMessage, Name, Parser, ValueDescription} 4 | import caseapp.core.help.Help 5 | 6 | final case class SharedOptions( 7 | @Group("Shared") 8 | @HelpMessage("Set launcher app name, default: name of source app") 9 | @ValueDescription("launcher-app-name") 10 | launcherApp: Option[String] = None, 11 | @Group("Shared") 12 | @HelpMessage("The version is set to 1.0.0 by default") 13 | @Name("v") 14 | version: String = "1.0.0", 15 | @Group("Shared") 16 | @HelpMessage( 17 | "Set package description, default: Native package building by scala-packager" 18 | ) 19 | @ValueDescription("Description") 20 | @Name("d") 21 | description: Option[String] = None, 22 | @Group("Shared") 23 | @HelpMessage( 24 | "It should contains names and email addresses of co-maintainers of the package" 25 | ) 26 | @Name("m") 27 | maintainer: Option[String] = None, 28 | @Group("Windows") 29 | @HelpMessage( 30 | "Path to application logo in png format, it will be used to generate icon and banner/dialog in msi installer" 31 | ) 32 | logoPath: Option[String] = None 33 | ) 34 | case object SharedOptions { 35 | lazy val parser: Parser[SharedOptions] = Parser.derive 36 | implicit lazy val parserAux: Parser.Aux[SharedOptions, parser.D] = parser 37 | implicit lazy val help: Help[SharedOptions] = Help.derive 38 | } 39 | -------------------------------------------------------------------------------- /modules/cli/src/main/scala/packager/cli/commands/WindowsOptions.scala: -------------------------------------------------------------------------------- 1 | package packager.cli.commands 2 | 3 | import caseapp.core.help.Help 4 | import caseapp.{Group, HelpMessage, Name, Parser, ValueDescription} 5 | import SettingsHelpers.{Mandatory, Validate} 6 | import packager.config.{SharedSettings, WindowsSettings} 7 | 8 | final case class WindowsOptions( 9 | @Group("Windows") 10 | @HelpMessage("Path to license file") 11 | licensePath: Option[String] = None, 12 | @Group("Windows") 13 | @HelpMessage("Name of product, default: Scala packager") 14 | productName: String = "Scala packager", 15 | @Group("Windows") 16 | @HelpMessage("Text will be displayed on exit dialog") 17 | exitDialog: Option[String] = None, 18 | @Group("Windows") 19 | @HelpMessage( 20 | "Suppress Wix ICE validation (required for users that are neither interactive, not local administrators)" 21 | ) 22 | suppressValidation: Boolean = false, 23 | @Group("Windows") 24 | @HelpMessage("Path to extra WIX config content") 25 | @ValueDescription("path") 26 | extraConfigs: List[String] = Nil, 27 | @Group("Windows") 28 | @HelpMessage("Whether a 64-bit executable is getting packaged") 29 | @Name("64") 30 | is64Bits: Boolean = true, 31 | @Group("Windows") 32 | @HelpMessage("WIX installer version") 33 | installerVersion: Option[String] = None, 34 | @Group("Windows") 35 | @HelpMessage("The GUID to identify that the windows package can be upgraded.") 36 | wixUpgradeCodeGuid: Option[String] = None 37 | ) { 38 | 39 | def toWindowsSettings( 40 | sharedSettings: SharedSettings, 41 | maintainer: Option[String] 42 | ): WindowsSettings = 43 | WindowsSettings( 44 | shared = sharedSettings, 45 | maintainer = maintainer.mandatory( 46 | "Maintainer parameter is mandatory for debian package" 47 | ), 48 | licencePath = os.Path( 49 | licensePath.mandatory( 50 | "License path parameter is mandatory for windows packages" 51 | ), 52 | os.pwd 53 | ), 54 | productName = productName, 55 | exitDialog = exitDialog, 56 | suppressValidation = suppressValidation, 57 | extraConfigs = extraConfigs, 58 | is64Bits = is64Bits, 59 | installerVersion = installerVersion, 60 | wixUpgradeCodeGuid = wixUpgradeCodeGuid 61 | ) 62 | } 63 | 64 | case object WindowsOptions { 65 | lazy val parser: Parser[WindowsOptions] = Parser.derive 66 | implicit lazy val parserAux: Parser.Aux[WindowsOptions, parser.D] = parser 67 | implicit lazy val help: Help[WindowsOptions] = Help.derive 68 | } 69 | -------------------------------------------------------------------------------- /modules/cli/src/test/scala/packager/cli/DummyTests.scala: -------------------------------------------------------------------------------- 1 | package packager.cli 2 | 3 | class DummyTests extends munit.FunSuite { 4 | test("nothing") { 5 | println("nothing") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /modules/image-resizer/src/main/scala/packager/windows/DefaultImageResizer.scala: -------------------------------------------------------------------------------- 1 | package packager.windows 2 | 3 | import net.coobird.thumbnailator.Thumbnails 4 | import net.coobird.thumbnailator.filters.Canvas 5 | import net.coobird.thumbnailator.geometry.Positions 6 | import net.sf.image4j.codec.ico.ICOEncoder 7 | 8 | import java.awt.Color 9 | import java.awt.image.BufferedImage 10 | import java.io.File 11 | import javax.imageio.ImageIO 12 | 13 | case object DefaultImageResizer extends ImageResizer { 14 | 15 | def generateIcon(logoPath: os.Path, workDirPath: os.Path): os.Path = { 16 | val icoTmpPath = workDirPath / "logo_tmp.ico" 17 | val resizedLogoPath = resizeLogo(logoPath, 32, 32, workDirPath) 18 | val iconImage: BufferedImage = 19 | ImageIO.read(new File(resizedLogoPath.toString())); 20 | ICOEncoder.write(iconImage, new File(icoTmpPath.toString())); 21 | val icoPath = workDirPath / "logo.ico" 22 | os.copy.over(icoTmpPath, icoPath) 23 | icoPath 24 | } 25 | 26 | private def generateEmptyImage(width: Int, height: Int): BufferedImage = 27 | new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) 28 | 29 | private def resizeLogo( 30 | logoPath: os.Path, 31 | width: Int, 32 | height: Int, 33 | workDir: os.Path 34 | ) = { 35 | val resizedPath = workDir / "resized-logo.png" 36 | Thumbnails 37 | .of(logoPath.toString()) 38 | .size(width, height) 39 | .toFile(new File(resizedPath.toString())) 40 | 41 | resizedPath 42 | } 43 | 44 | def generateBanner(logoPath: os.Path, workDirPath: os.Path): os.Path = { 45 | val emptyBanner = generateEmptyImage(493, 58) 46 | 47 | val bannerPath = workDirPath / s"banner.bmp" 48 | val resizedLogoPath = resizeLogo(logoPath, 55, 55, workDirPath) 49 | 50 | Thumbnails 51 | .of(emptyBanner) 52 | .addFilter(new Canvas(493, 58, Positions.CENTER, Color.WHITE)) 53 | .size(493, 58) 54 | .watermark( 55 | Positions.CENTER_RIGHT, 56 | ImageIO.read(new File(resizedLogoPath.toString())), 57 | 1 58 | ) 59 | .toFile(new File(bannerPath.toString())); 60 | 61 | bannerPath 62 | } 63 | 64 | def generateDialog(logoPath: os.Path, workDirPath: os.Path): os.Path = { 65 | 66 | val emptyDialog = generateEmptyImage(493, 312) 67 | val dialogPath = workDirPath / s"dialog.bmp" 68 | 69 | val resizedLogoPath = resizeLogo(logoPath, 165, 330, workDirPath) 70 | 71 | Thumbnails 72 | .of(emptyDialog) 73 | .size(493, 312) 74 | .addFilter(new Canvas(493, 312, Positions.CENTER, Color.WHITE)) 75 | .watermark( 76 | Positions.CENTER_LEFT, 77 | ImageIO.read(new File(resizedLogoPath.toString())), 78 | 1 79 | ) 80 | .toFile(new File(dialogPath.toString())); 81 | 82 | dialogPath 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /modules/image-resizer/src/test/scala/packager/windows/WindowsPackageTests.scala: -------------------------------------------------------------------------------- 1 | package packager.windows 2 | 3 | import com.eed3si9n.expecty.Expecty.expect 4 | import packager.NativePackageHelper 5 | import packager.config.WindowsSettings 6 | 7 | import scala.util.Properties 8 | 9 | class WindowsPackageTests extends munit.FunSuite with NativePackageHelper { 10 | 11 | override def outputPackagePath: os.Path = tmpDir / s"scalafmt.msi" 12 | 13 | if (Properties.isWin) { 14 | 15 | test("should generate msi package") { 16 | 17 | val msiPackage = WindowsPackage( 18 | buildSettings, 19 | imageResizerOpt = Some(DefaultImageResizer) 20 | ) 21 | 22 | // create msi package 23 | msiPackage.build() 24 | 25 | val expectedMsiPath = tmpDir / s"$packageName.msi" 26 | expect(os.exists(expectedMsiPath)) 27 | } 28 | test("should override generated msi package") { 29 | 30 | val msiPackage = WindowsPackage( 31 | buildSettings, 32 | imageResizerOpt = Some(DefaultImageResizer) 33 | ) 34 | 35 | // create twice msi package 36 | msiPackage.build() 37 | msiPackage.build() 38 | 39 | val expectedMsiPath = tmpDir / s"$packageName.msi" 40 | expect(os.exists(expectedMsiPath)) 41 | } 42 | } 43 | 44 | test("should exists default licence file for msi package") { 45 | val msiPackage = WindowsPackage( 46 | buildSettings, 47 | imageResizerOpt = Some(DefaultImageResizer) 48 | ) 49 | 50 | val licencePath = msiPackage.buildSettings.licencePath 51 | 52 | expect(os.read(licencePath).nonEmpty) 53 | } 54 | 55 | override def buildSettings: WindowsSettings = 56 | WindowsSettings( 57 | shared = sharedSettings, 58 | maintainer = "Scala Packager", 59 | licencePath = os.resource / "packager" / "apache-2.0", 60 | productName = "Scala packager product", 61 | exitDialog = None, 62 | suppressValidation = true, 63 | extraConfigs = Nil, 64 | is64Bits = false, 65 | installerVersion = None, 66 | wixUpgradeCodeGuid = None 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /modules/image-resizer/src/test/scala/packager/windows/WixLogoSpec.scala: -------------------------------------------------------------------------------- 1 | package packager.windows 2 | 3 | import com.eed3si9n.expecty.Expecty.expect 4 | import packager.TestUtils 5 | 6 | class WixLogoSpec extends munit.FunSuite { 7 | 8 | private lazy val workDirPath = TestUtils.tmpUtilDir 9 | private lazy val logoPath: os.Path = TestUtils.logo(workDirPath) 10 | 11 | test("should prepare icon for wix installer") { 12 | val iconPath = DefaultImageResizer.generateIcon(logoPath, workDirPath) 13 | 14 | val expectedIconPath = workDirPath / "logo.ico" 15 | 16 | assertEquals(iconPath, expectedIconPath) 17 | expect(os.exists(expectedIconPath)) 18 | } 19 | 20 | test("should prepare banner for wix installer") { 21 | val bannerPath = DefaultImageResizer.generateBanner(logoPath, workDirPath) 22 | 23 | val expectedBannerPath = workDirPath / "banner.bmp" 24 | 25 | assertEquals(bannerPath, expectedBannerPath) 26 | expect(os.exists(expectedBannerPath)) 27 | } 28 | 29 | test("should prepare dialog for wix installer") { 30 | 31 | val dialogPath = DefaultImageResizer.generateDialog(logoPath, workDirPath) 32 | 33 | val expectedDialogPath = workDirPath / "dialog.bmp" 34 | 35 | assertEquals(dialogPath, expectedDialogPath) 36 | expect(os.exists(expectedDialogPath)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/FileUtils.scala: -------------------------------------------------------------------------------- 1 | package packager 2 | 3 | import os.PermSet 4 | import packager.config.{BuildSettings, NativeSettings} 5 | 6 | object FileUtils { 7 | 8 | def alreadyExistsCheck( 9 | destPath: os.Path 10 | )(implicit buildOptions: NativeSettings): Unit = 11 | if (!buildOptions.shared.force && os.exists(destPath)) { 12 | System.err.println( 13 | s"Error: $destPath already exists. Pass -f or --force to force erasing it." 14 | ) 15 | sys.exit(1) 16 | } 17 | 18 | def copy(from: os.Path, to: os.Path)(implicit 19 | buildOptions: NativeSettings 20 | ): Unit = { 21 | alreadyExistsCheck(to) 22 | os.copy.over(from, to) 23 | } 24 | 25 | def move(from: os.Path, to: os.Path)(implicit 26 | buildOptions: NativeSettings 27 | ): Unit = { 28 | alreadyExistsCheck(to) 29 | os.move.over(from, to) 30 | } 31 | 32 | def write(destPath: os.Path, content: String, perms: PermSet = null)(implicit 33 | buildOptions: NativeSettings 34 | ): Unit = { 35 | alreadyExistsCheck(destPath) 36 | os.write.over(destPath, content, perms) 37 | } 38 | 39 | lazy val executablePerms: PermSet = PermSet.fromString("rwxrwxr-x") 40 | 41 | } 42 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/NativePackager.scala: -------------------------------------------------------------------------------- 1 | package packager 2 | 3 | import packager.config.NativeSettings 4 | 5 | import org.apache.commons.io.FilenameUtils 6 | 7 | trait NativePackager extends Packager { 8 | 9 | implicit def options: NativeSettings = buildSettings 10 | override def buildSettings: NativeSettings 11 | 12 | lazy val sourceAppPath: os.Path = buildSettings.shared.sourceAppPath 13 | 14 | override def launcherApp: String = 15 | buildSettings.shared.launcherApp.getOrElse(super.launcherApp).toLowerCase() 16 | 17 | lazy val outputPath: os.Path = buildSettings.shared.outputPath 18 | 19 | lazy val packageName = 20 | FilenameUtils.removeExtension(buildSettings.shared.outputPath.last) 21 | 22 | lazy val basePath: os.Path = 23 | buildSettings.shared.workingDirectoryPath.getOrElse( 24 | os.temp.dir(prefix = packageName) 25 | ) 26 | 27 | } 28 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/Packager.scala: -------------------------------------------------------------------------------- 1 | package packager 2 | 3 | import org.apache.commons.io.FilenameUtils 4 | import packager.config.BuildSettings 5 | 6 | trait Packager { 7 | 8 | def sourceAppPath: os.Path 9 | def buildSettings: BuildSettings 10 | 11 | def build(): Unit 12 | def launcherApp = 13 | FilenameUtils.removeExtension(sourceAppPath.last) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/BuildSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | trait BuildSettings 4 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/DebianSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | case class DebianSettings( 4 | shared: SharedSettings, 5 | maintainer: String, 6 | description: String, 7 | debianConflicts: List[String], 8 | debianDependencies: List[String], 9 | architecture: String, 10 | priority: Option[String], 11 | section: Option[String] 12 | ) extends NativeSettings 13 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/DockerSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | import java.nio.file.Path; 4 | 5 | case class DockerSettings( 6 | from: String, 7 | registry: Option[String], 8 | repository: String, 9 | tag: Option[String], 10 | exec: Option[String], 11 | dockerExecutable: Option[Path] 12 | ) extends BuildSettings 13 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/MacOSSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | case class MacOSSettings( 4 | shared: SharedSettings, 5 | identifier: String 6 | ) extends NativeSettings 7 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/NativeSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | trait NativeSettings extends BuildSettings { 4 | def shared: SharedSettings 5 | } 6 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/RedHatSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | case class RedHatSettings( 4 | shared: SharedSettings, 5 | description: String, 6 | license: String, 7 | release: String, 8 | rpmArchitecture: String 9 | ) extends NativeSettings 10 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/SharedSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | case class SharedSettings( 4 | sourceAppPath: os.Path, 5 | version: String, 6 | force: Boolean = false, 7 | workingDirectoryPath: Option[os.Path] = None, 8 | outputPath: os.Path, 9 | logoPath: Option[os.Path], 10 | launcherApp: Option[String] = None 11 | ) 12 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/config/WindowsSettings.scala: -------------------------------------------------------------------------------- 1 | package packager.config 2 | 3 | case class WindowsSettings( 4 | shared: SharedSettings, 5 | maintainer: String, 6 | licencePath: os.ReadablePath, 7 | productName: String, 8 | exitDialog: Option[String], 9 | suppressValidation: Boolean, 10 | extraConfigs: List[String], 11 | is64Bits: Boolean, 12 | installerVersion: Option[String], 13 | wixUpgradeCodeGuid: Option[String] 14 | ) extends NativeSettings 15 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/deb/DebianMetaData.scala: -------------------------------------------------------------------------------- 1 | package packager.deb 2 | 3 | case class DebianMetaData( 4 | debianInfo: DebianPackageInfo, 5 | architecture: String = "all", 6 | dependsOn: List[String] = Nil, 7 | conflicts: List[String] = Nil, 8 | priority: Option[String], 9 | section: Option[String] 10 | ) { 11 | 12 | def generateContent(): String = { 13 | val flags = List( 14 | s"Package: ${debianInfo.packageName}", 15 | s"Version: ${debianInfo.version}", 16 | s"Maintainer: ${debianInfo.maintainer}", 17 | s"Description: ${debianInfo.description}", 18 | s"Architecture: $architecture" 19 | ) ++ 20 | (if (priority.nonEmpty) List("Priority: " + priority.getOrElse("")) else Nil) ++ 21 | (if (section.nonEmpty) List("Section: " + section.getOrElse("")) else Nil) ++ 22 | (if (dependsOn.nonEmpty) List("Depends: " + dependsOn.mkString(", ")) else Nil) ++ 23 | (if (conflicts.nonEmpty) List("Conflicts: " + conflicts.mkString(", ")) else Nil) 24 | 25 | flags.mkString("", System.lineSeparator(), System.lineSeparator()) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/deb/DebianPackage.scala: -------------------------------------------------------------------------------- 1 | package packager.deb 2 | 3 | import packager.{FileUtils, NativePackager} 4 | import packager.config.DebianSettings 5 | 6 | case class DebianPackage(buildSettings: DebianSettings) extends NativePackager { 7 | 8 | private val debianBasePath = basePath / "debian" 9 | private val usrDirectory = debianBasePath / "usr" 10 | private val packageInfo = buildDebianInfo() 11 | private val metaData = buildDebianMetaData(packageInfo) 12 | private val mainDebianDirectory = debianBasePath / "DEBIAN" 13 | 14 | override def build(): Unit = { 15 | createDebianDir() 16 | 17 | os.proc("dpkg", "-b", debianBasePath) 18 | .call(cwd = basePath) 19 | 20 | FileUtils.move(basePath / "debian.deb", outputPath) 21 | 22 | postInstallClean() 23 | } 24 | 25 | private def postInstallClean(): Unit = { 26 | os.remove.all(debianBasePath) 27 | } 28 | 29 | def createDebianDir(): Unit = { 30 | os.makeDir.all(mainDebianDirectory) 31 | 32 | createConfFile() 33 | createScriptFile() 34 | copyExecutableFile() 35 | } 36 | 37 | private def buildDebianMetaData(info: DebianPackageInfo): DebianMetaData = 38 | DebianMetaData( 39 | debianInfo = info, 40 | architecture = buildSettings.architecture, 41 | dependsOn = buildSettings.debianDependencies, 42 | conflicts = buildSettings.debianConflicts, 43 | priority = buildSettings.priority, 44 | section = buildSettings.section 45 | ) 46 | 47 | private def buildDebianInfo(): DebianPackageInfo = 48 | DebianPackageInfo( 49 | packageName = packageName, 50 | version = buildSettings.shared.version, 51 | maintainer = buildSettings.maintainer, 52 | description = buildSettings.description 53 | ) 54 | 55 | private def copyExecutableFile(): Unit = { 56 | val scalaDirectory = usrDirectory / "share" / "scala" 57 | os.makeDir.all(scalaDirectory) 58 | FileUtils.copy(sourceAppPath, scalaDirectory / launcherApp) 59 | } 60 | 61 | private def createConfFile(): Unit = { 62 | FileUtils.write(mainDebianDirectory / "control", metaData.generateContent()) 63 | } 64 | 65 | private def createScriptFile(): Unit = { 66 | val binDirectory = usrDirectory / "bin" 67 | os.makeDir.all(binDirectory) 68 | val launchScriptFile = binDirectory / launcherApp 69 | val content = s"""#!/bin/bash 70 | |/usr/share/scala/$launcherApp \"$$@\" 71 | |""".stripMargin 72 | FileUtils.write(launchScriptFile, content, FileUtils.executablePerms) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/deb/DebianPackageInfo.scala: -------------------------------------------------------------------------------- 1 | package packager.deb 2 | 3 | case class DebianPackageInfo( 4 | packageName: String, 5 | version: String, 6 | maintainer: String, 7 | description: String 8 | ) 9 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/docker/DockerPackage.scala: -------------------------------------------------------------------------------- 1 | package packager.docker 2 | 3 | import com.google.cloud.tools.jib.api.{Containerizer, DockerDaemonImage, ImageReference, Jib} 4 | import com.google.cloud.tools.jib.api.buildplan.{ 5 | AbsoluteUnixPath, 6 | FileEntriesLayer, 7 | FilePermissions 8 | } 9 | import packager.Packager 10 | import packager.config.DockerSettings 11 | 12 | import java.time.Instant 13 | 14 | case class DockerPackage(sourceAppPath: os.Path, buildSettings: DockerSettings) 15 | extends Packager { 16 | 17 | override def build(): Unit = { 18 | 19 | lazy val targetImageReference: ImageReference = 20 | ImageReference.of( 21 | buildSettings.registry.orNull, 22 | buildSettings.repository, 23 | buildSettings.tag.orNull 24 | ) 25 | 26 | val targetImage = DockerDaemonImage 27 | .named(targetImageReference) 28 | val entrypoint = buildSettings.exec 29 | .map(e => List(s"$e", s"/$launcherApp")) 30 | .getOrElse(List(s"/$launcherApp")) 31 | 32 | def makeFileEntryLayerConfiguration( 33 | resourcePath: os.Path, 34 | dest: String 35 | ): FileEntriesLayer = { 36 | val layerConfigurationBuilder = FileEntriesLayer.builder 37 | layerConfigurationBuilder.addEntry( 38 | resourcePath.toNIO, 39 | AbsoluteUnixPath.get(dest), 40 | FilePermissions.fromOctalString("755") 41 | ) 42 | layerConfigurationBuilder.build() 43 | } 44 | 45 | Jib 46 | .from(buildSettings.from) 47 | .setFileEntriesLayers( 48 | makeFileEntryLayerConfiguration(sourceAppPath, s"/$launcherApp") 49 | ) 50 | .setCreationTime(Instant.now()) 51 | .setEntrypoint(entrypoint: _*) 52 | .containerize( 53 | Containerizer.to( 54 | buildSettings.dockerExecutable.map(targetImage.setDockerExecutable).getOrElse(targetImage) 55 | ) 56 | ) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/mac/MacOSInfoPlist.scala: -------------------------------------------------------------------------------- 1 | package packager.mac 2 | 3 | case class MacOSInfoPlist(executableName: String, identifier: String) { 4 | 5 | def generateContent: String = { 6 | val content = 7 | s""" 8 | | 9 | | 10 | | 11 | | CFBundleExecutable 12 | | $executableName 13 | | CFBundleIdentifier 14 | | $identifier 15 | | 16 | |""".stripMargin 17 | content 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/mac/MacOSNativePackager.scala: -------------------------------------------------------------------------------- 1 | package packager.mac 2 | 3 | import packager.{FileUtils, NativePackager} 4 | import packager.config.MacOSSettings 5 | 6 | trait MacOSNativePackager extends NativePackager { 7 | 8 | protected val macOSAppPath: os.Path = basePath / s"$packageName.app" 9 | protected val contentPath: os.Path = macOSAppPath / "Contents" 10 | protected val macOsPath: os.Path = contentPath / "MacOS" 11 | protected val infoPlist: MacOSInfoPlist = 12 | MacOSInfoPlist(packageName, buildSettings.identifier) 13 | 14 | override def buildSettings: MacOSSettings 15 | 16 | def createAppDirectory(): Unit = { 17 | os.makeDir.all(macOsPath) 18 | 19 | val appPath = macOsPath / launcherApp 20 | FileUtils.copy(sourceAppPath, appPath) 21 | } 22 | 23 | protected def createInfoPlist(): Unit = { 24 | val infoPlistPath = contentPath / "Info.plist" 25 | 26 | FileUtils.write(infoPlistPath, infoPlist.generateContent) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/mac/dmg/DmgPackage.scala: -------------------------------------------------------------------------------- 1 | package packager.mac.dmg 2 | 3 | import packager.config.MacOSSettings 4 | import packager.mac.MacOSNativePackager 5 | 6 | case class DmgPackage(buildSettings: MacOSSettings) 7 | extends MacOSNativePackager { 8 | 9 | private val tmpPackageName = s"$packageName-tmp" 10 | private val mountpointPath = basePath / "mountpoint" 11 | private val appSize: Long = os.size(sourceAppPath) / (1024L * 1024L) + 1 12 | 13 | override def build(): Unit = { 14 | os.proc( 15 | "hdiutil", 16 | "create", 17 | "-megabytes", 18 | appSize, 19 | "-fs", 20 | "HFS+", 21 | "-volname", 22 | packageName, 23 | tmpPackageName 24 | ).call(cwd = basePath) 25 | 26 | createAppDirectory() 27 | createInfoPlist() 28 | 29 | os.proc( 30 | "hdiutil", 31 | "attach", 32 | s"$tmpPackageName.dmg", 33 | "-readwrite", 34 | "-mountpoint", 35 | "mountpoint/" 36 | ).call(cwd = basePath) 37 | 38 | copyAppDirectory() 39 | removeDmgIfExists() 40 | 41 | os.proc("hdiutil", "detach", "mountpoint/").call(cwd = basePath) 42 | os.proc( 43 | "hdiutil", 44 | "convert", 45 | s"$tmpPackageName.dmg", 46 | "-format", 47 | "UDZO", 48 | "-o", 49 | outputPath 50 | ).call(cwd = basePath) 51 | 52 | postInstallClean() 53 | } 54 | 55 | private def removeDmgIfExists(): Unit = { 56 | if (options.shared.force && os.exists(outputPath)) os.remove(outputPath) 57 | } 58 | 59 | private def postInstallClean(): Unit = { 60 | os.remove(basePath / s"$tmpPackageName.dmg") 61 | os.remove.all(macOSAppPath) 62 | } 63 | 64 | private def copyAppDirectory(): Unit = { 65 | os.copy(macOSAppPath, mountpointPath / s"$packageName.app") 66 | os.symlink(mountpointPath / "Applications", os.root / "Applications") 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/mac/pkg/PkgPackage.scala: -------------------------------------------------------------------------------- 1 | package packager.mac.pkg 2 | 3 | import packager.FileUtils 4 | import packager.config.MacOSSettings 5 | import packager.mac.MacOSNativePackager 6 | 7 | case class PkgPackage(buildSettings: MacOSSettings) 8 | extends MacOSNativePackager { 9 | 10 | private val scriptsPath = basePath / "scripts" 11 | 12 | override def build(): Unit = { 13 | 14 | createAppDirectory() 15 | createInfoPlist() 16 | createScriptFile() 17 | 18 | os.proc( 19 | "pkgbuild", 20 | "--install-location", 21 | "/Applications", 22 | "--component", 23 | s"$packageName.app", 24 | outputPath, 25 | "--scripts", 26 | scriptsPath 27 | ).call(cwd = basePath) 28 | 29 | postInstallClean() 30 | } 31 | 32 | private def postInstallClean(): Unit = { 33 | os.remove.all(macOSAppPath) 34 | os.remove.all(scriptsPath) 35 | } 36 | 37 | private def createScriptFile(): Unit = { 38 | val content = 39 | s"""#!/bin/bash 40 | |rm -f /usr/local/bin/$launcherApp 41 | |ln -s /Applications/$packageName.app/Contents/MacOS/$launcherApp /usr/local/bin/$launcherApp""".stripMargin 42 | os.makeDir.all(scriptsPath) 43 | val postInstallPath = scriptsPath / "postinstall" 44 | FileUtils.write(postInstallPath, content, FileUtils.executablePerms) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/rpm/RedHatPackage.scala: -------------------------------------------------------------------------------- 1 | package packager.rpm 2 | 3 | import packager.{FileUtils, NativePackager} 4 | import packager.config.RedHatSettings 5 | 6 | case class RedHatPackage(buildSettings: RedHatSettings) extends NativePackager { 7 | 8 | private val redHatBasePath = basePath / "rpmbuild" 9 | private val sourcesDirectory = redHatBasePath / "SOURCES" 10 | private val specsDirectory = redHatBasePath / "SPECS" 11 | private val rpmsDirectory = redHatBasePath / "RPMS" 12 | private val redHatSpec = buildRedHatSpec() 13 | 14 | override def build(): Unit = { 15 | createRedHatDir() 16 | 17 | os.proc( 18 | "rpmbuild", 19 | "-bb", 20 | "--build-in-place", 21 | "--define", 22 | s"_topdir $redHatBasePath", 23 | s"$specsDirectory/$packageName.spec" 24 | ).call(cwd = basePath) 25 | FileUtils.move(rpmsDirectory / s"$launcherApp.rpm", outputPath) 26 | 27 | postInstallClean() 28 | } 29 | 30 | private def postInstallClean(): Unit = { 31 | os.remove.all(redHatBasePath) 32 | } 33 | 34 | def createRedHatDir(): Unit = { 35 | os.makeDir.all(sourcesDirectory) 36 | os.makeDir.all(specsDirectory) 37 | 38 | copyExecutableFile() 39 | createSpecFile() 40 | } 41 | 42 | private def createSpecFile(): Unit = { 43 | val content = redHatSpec.generateContent 44 | val specFilePath = specsDirectory / s"$packageName.spec" 45 | FileUtils.write(specFilePath, content) 46 | } 47 | 48 | private def buildRedHatSpec(): RedHatSpecPackage = 49 | RedHatSpecPackage( 50 | launcherAppName = launcherApp, 51 | version = buildSettings.shared.version, 52 | description = buildSettings.description, 53 | buildArch = buildSettings.rpmArchitecture, 54 | license = buildSettings.license, 55 | release = buildSettings.release 56 | ) 57 | 58 | private def copyExecutableFile(): Unit = { 59 | FileUtils.copy(sourceAppPath, sourcesDirectory / launcherApp) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/rpm/RedHatSpecPackage.scala: -------------------------------------------------------------------------------- 1 | package packager.rpm 2 | 3 | case class RedHatSpecPackage( 4 | launcherAppName: String, 5 | version: String, 6 | description: String, 7 | buildArch: String, 8 | license: String, 9 | release: String 10 | ) { 11 | 12 | def generateContent: String = 13 | s"""Name: $launcherAppName 14 | |Version: $version 15 | |Release: $release 16 | |Summary: $description 17 | |BuildArch: $buildArch 18 | | 19 | |License: $license 20 | | 21 | |#BuildRequires: 22 | |Requires: bash 23 | | 24 | |%define _rpmfilename %%{NAME}.rpm 25 | | 26 | |%description 27 | |RedHat package 28 | | 29 | |%define _binaries_in_noarch_packages_terminate_build 0 30 | | 31 | |%install 32 | |rm -rf $$RPM_BUILD_ROOT 33 | |mkdir -p $$RPM_BUILD_ROOT/%{_bindir} 34 | |cp ./rpmbuild/SOURCES/$launcherAppName $$RPM_BUILD_ROOT/%{_bindir} 35 | | 36 | |%clean 37 | |rm -rf $$RPM_BUILD_ROOT 38 | | 39 | |%files 40 | |%{_bindir}/$launcherAppName 41 | |""".stripMargin 42 | 43 | } 44 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/windows/ImageResizer.scala: -------------------------------------------------------------------------------- 1 | package packager.windows 2 | 3 | trait ImageResizer { 4 | def generateIcon(logoPath: os.Path, workDirPath: os.Path): os.Path 5 | def generateBanner(logoPath: os.Path, workDirPath: os.Path): os.Path 6 | def generateDialog(logoPath: os.Path, workDirPath: os.Path): os.Path 7 | } 8 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/windows/WindowsPackage.scala: -------------------------------------------------------------------------------- 1 | package packager.windows 2 | 3 | import packager.{FileUtils, NativePackager} 4 | import packager.config.WindowsSettings 5 | 6 | case class WindowsPackage( 7 | buildSettings: WindowsSettings, 8 | imageResizerOpt: Option[ImageResizer] = None 9 | ) extends NativePackager { 10 | 11 | private val wixConfigPath: os.Path = basePath / s"$packageName.wxs" 12 | private val licensePath: os.Path = basePath / s"license.rtf" 13 | 14 | override def build(): Unit = { 15 | 16 | val iconPath = buildSettings.shared.logoPath.flatMap { logoPath => 17 | imageResizerOpt.map(_.generateIcon(logoPath, basePath)) 18 | } 19 | val bannerPath = buildSettings.shared.logoPath.flatMap { logoPath => 20 | imageResizerOpt.map(_.generateBanner(logoPath, basePath)) 21 | } 22 | val dialogPath = buildSettings.shared.logoPath.flatMap { logoPath => 23 | imageResizerOpt.map(_.generateDialog(logoPath, basePath)) 24 | } 25 | 26 | def postInstallClean() = { 27 | iconPath.foreach(os.remove) 28 | bannerPath.foreach(os.remove) 29 | dialogPath.foreach(os.remove) 30 | } 31 | 32 | val wixConfig = WindowsWixConfig( 33 | packageName = packageName, 34 | sourcePath = sourceAppPath, 35 | iconPath = iconPath, 36 | bannerPath = bannerPath, 37 | dialogPath = dialogPath, 38 | licensePath = licensePath, 39 | exitDialog = buildSettings.exitDialog, 40 | productName = buildSettings.productName, 41 | version = buildSettings.shared.version, 42 | maintainer = buildSettings.maintainer, 43 | launcherAppName = launcherApp, 44 | extraConfigs = buildSettings.extraConfigs, 45 | is64Bits = buildSettings.is64Bits, 46 | installerVersion = buildSettings.installerVersion, 47 | wixUpgradeCodeGuid = buildSettings.wixUpgradeCodeGuid 48 | ) 49 | 50 | createConfFile(wixConfig) 51 | copyLicenseToBasePath() 52 | 53 | val wixBin = Option(System.getenv("WIX")).getOrElse("\"%WIX%bin\"") 54 | val candleBinPath = os.Path(wixBin) / "bin" / "candle.exe" 55 | val lightBinPath = os.Path(wixBin) / "bin" / "light.exe" 56 | 57 | val lightExtraArguments = 58 | if (buildSettings.suppressValidation) Seq("-sval") 59 | else Nil 60 | 61 | val extraCandleOptions = 62 | if (buildSettings.is64Bits) Seq("-arch", "x64") 63 | else Nil 64 | 65 | os.proc( 66 | candleBinPath, 67 | wixConfigPath, 68 | extraCandleOptions, 69 | "-ext", 70 | "WixUIExtension" 71 | ).call(cwd = basePath) 72 | 73 | os.proc( 74 | lightBinPath, 75 | s"$packageName.wixobj", 76 | "-o", 77 | outputPath, 78 | "-ext", 79 | "WixUIExtension", 80 | lightExtraArguments 81 | ).call(cwd = basePath) 82 | 83 | postInstallClean() 84 | } 85 | 86 | private def copyLicenseToBasePath() = { 87 | val license = 88 | WindowsUtils.convertLicenseToRtfFormat(buildSettings.licencePath) 89 | os.write.over(licensePath, license) 90 | } 91 | 92 | private def createConfFile(wixConfig: WindowsWixConfig): Unit = { 93 | FileUtils.write(wixConfigPath, wixConfig.generateContent()) 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/windows/WindowsUtils.scala: -------------------------------------------------------------------------------- 1 | package packager.windows 2 | 3 | case object WindowsUtils { 4 | 5 | def convertLicenseToRtfFormat(licensePath: os.ReadablePath): String = { 6 | val license = os.read(licensePath) 7 | val rtfLicense = 8 | s"""{\\rtf1\\ansi{\\fonttbl{\\f0\\fcharset0 Times New Roman;}} 9 | |\\ 10 | |${license.replaceAll("\n", "\\\\\n")} 11 | |} 12 | |""".stripMargin 13 | rtfLicense 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/windows/WindowsWixConfig.scala: -------------------------------------------------------------------------------- 1 | package packager.windows 2 | 3 | import packager.windows.wix._ 4 | import java.nio.charset.Charset 5 | 6 | import scala.io.Codec 7 | 8 | case class WindowsWixConfig( 9 | packageName: String, 10 | sourcePath: os.Path, 11 | iconPath: Option[os.Path], 12 | bannerPath: Option[os.Path], 13 | dialogPath: Option[os.Path], 14 | licensePath: os.Path, 15 | exitDialog: Option[String], 16 | productName: String, 17 | version: String, 18 | maintainer: String, 19 | launcherAppName: String, 20 | extraConfigs: List[String], 21 | is64Bits: Boolean, 22 | installerVersion: Option[String], 23 | wixUpgradeCodeGuid: Option[String] 24 | ) { 25 | 26 | lazy val extraConfig: Option[String] = 27 | if (extraConfigs.isEmpty) None 28 | else 29 | Some { 30 | extraConfigs 31 | .map { path => 32 | val path0 = os.Path(path, os.pwd) 33 | os.read(path0, Codec(Charset.defaultCharset())) 34 | } 35 | .mkString(System.lineSeparator()) 36 | } 37 | 38 | lazy val wixExitDialog = 39 | exitDialog 40 | .map(txt => Property(id = WIXUI_EXITDIALOGOPTIONALTEXT, value = txt)) 41 | .map(_.generate) 42 | .getOrElse("") 43 | 44 | lazy val wixBannerBmp = bannerPath 45 | .map(path => WixVariable(id = WixUIBannerBmp, value = path.toString())) 46 | .map(_.generate) 47 | .getOrElse("") 48 | 49 | lazy val wixDialogBmp = dialogPath 50 | .map(path => WixVariable(id = WixUIDialogBmp, value = path.toString())) 51 | .map(_.generate) 52 | .getOrElse("") 53 | 54 | lazy val wixPropertyIcon = iconPath 55 | .map(_ => Property(id = ARPPRODUCTICON, value = "logo_ico")) 56 | .map(_.generate) 57 | .getOrElse("") 58 | 59 | lazy val wixIcon = iconPath 60 | .map(path => Icon(id = "logo_ico", sourceFile = path.toString())) 61 | .map(_.generate) 62 | .getOrElse("") 63 | 64 | def randomGuid: String = java.util.UUID.randomUUID.toString 65 | 66 | private def extraPackage = 67 | if (is64Bits) """Platform="x64"""" 68 | else "" 69 | private def programFiles = 70 | if (is64Bits) "ProgramFiles64Folder" 71 | else "ProgramFilesFolder" 72 | 73 | def generateContent(): String = 74 | s""" 75 | 76 | 78 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 98 | 99 | 106 | 107 | 108 | 109 | 113 | 114 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | $wixExitDialog 126 | $wixBannerBmp 127 | $wixDialogBmp 128 | $wixPropertyIcon 129 | $wixIcon 130 | 131 | 132 | 133 | ${extraConfig.getOrElse("")} 134 | 135 | 136 | 137 | """ 138 | 139 | } 140 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/windows/wix/WixComponent.scala: -------------------------------------------------------------------------------- 1 | package packager.windows.wix 2 | 3 | trait WixComponent { 4 | def generate: String 5 | } 6 | 7 | case class WixVariable(id: WixVariableId, value: String) extends WixComponent { 8 | override def generate: String = 9 | s"""""" 10 | } 11 | 12 | case class Property(id: PropertyId, value: String) extends WixComponent { 13 | override def generate: String = 14 | s"""""" 15 | } 16 | 17 | case class Icon(id: String, sourceFile: String) extends WixComponent { 18 | override def generate: String = 19 | s"""""" 20 | } 21 | -------------------------------------------------------------------------------- /modules/packager/src/main/scala/packager/windows/wix/WixId.scala: -------------------------------------------------------------------------------- 1 | package packager.windows.wix 2 | 3 | sealed trait WixId 4 | 5 | sealed trait WixVariableId extends WixId 6 | 7 | case object WixUIBannerBmp extends WixVariableId 8 | case object WixUIDialogBmp extends WixVariableId 9 | 10 | sealed trait PropertyId extends WixId 11 | 12 | case object WIXUI_EXITDIALOGOPTIONALTEXT extends PropertyId 13 | case object ARPPRODUCTICON extends PropertyId 14 | -------------------------------------------------------------------------------- /modules/packager/src/test/resources/packager/apache-2.0: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/NativePackageHelper.scala: -------------------------------------------------------------------------------- 1 | package packager 2 | 3 | import packager.config.{NativeSettings, SharedSettings} 4 | 5 | trait NativePackageHelper extends PackagerHelper { 6 | 7 | def buildSettings: NativeSettings 8 | def outputPackagePath: os.Path 9 | 10 | lazy val sharedSettings: SharedSettings = 11 | SharedSettings( 12 | sourceAppPath = scalafmtLauncherPath, 13 | force = true, 14 | version = "1.0.0", 15 | workingDirectoryPath = Some(tmpDir), 16 | outputPath = outputPackagePath, 17 | launcherApp = None, 18 | logoPath = Some(TestUtils.logo(tmpDir)) 19 | ) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/PackagerHelper.scala: -------------------------------------------------------------------------------- 1 | package packager 2 | 3 | import packager.config.BuildSettings 4 | 5 | trait PackagerHelper { 6 | lazy val packageName: String = "scalafmt" 7 | lazy val tmpDir: os.Path = TestUtils.tmpUtilDir 8 | lazy val scalafmtNativePath: os.Path = TestUtils.scalafmtNative(tmpDir) 9 | lazy val scalafmtLauncherPath: os.Path = TestUtils.scalafmtLauncher(tmpDir) 10 | lazy val scalafmtVersion: String = TestUtils.scalafmtVersion 11 | 12 | def buildSettings: BuildSettings 13 | } 14 | -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/TestUtils.scala: -------------------------------------------------------------------------------- 1 | package packager 2 | 3 | import java.awt.image.BufferedImage 4 | import java.io.File 5 | import javax.imageio.ImageIO 6 | import scala.annotation.tailrec 7 | import scala.concurrent.duration.{DurationInt, FiniteDuration} 8 | 9 | object TestUtils { 10 | lazy val isAarch64: Boolean = sys.props.get("os.arch").contains("aarch64") 11 | 12 | def scalafmtVersion = "3.9.1" 13 | 14 | def tmpUtilDir: os.Path = os.temp.dir(prefix = "scala-packager-tests") 15 | 16 | def scalafmtNative(tmpDir: os.Path): os.Path = { 17 | val dest = tmpDir / "scalafmt-native" 18 | os.proc( 19 | "curl", 20 | "-L", 21 | "-o", 22 | dest, 23 | s"https://github.com/scalameta/scalafmt/releases/download/v$scalafmtVersion/scalafmt-x86_64-pc-linux" 24 | ).call() 25 | dest 26 | } 27 | 28 | def scalafmtLauncher(tmpDir: os.Path): os.Path = { 29 | val dest = tmpDir / "scalafmt" 30 | os.proc( 31 | "cs", 32 | "bootstrap", 33 | "-o", 34 | dest.toString, 35 | s"org.scalameta:scalafmt-cli_2.13:$scalafmtVersion" 36 | ).call() 37 | dest 38 | } 39 | 40 | def logo(tmpDir: os.Path): os.Path = { 41 | val logoPath = tmpDir / "logo.png" 42 | val logo: BufferedImage = 43 | new BufferedImage(231, 250, BufferedImage.TYPE_INT_ARGB) 44 | ImageIO.write(logo, "png", new File(logoPath.toString())) 45 | logoPath 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/deb/DebianPackageTests.scala: -------------------------------------------------------------------------------- 1 | package packager.deb 2 | 3 | import com.eed3si9n.expecty.Expecty.expect 4 | import packager.NativePackageHelper 5 | import packager.config.DebianSettings 6 | 7 | import scala.util.Properties 8 | 9 | class DebianPackageTests extends munit.FunSuite with NativePackageHelper { 10 | 11 | override def outputPackagePath: os.Path = tmpDir / s"scalafmt.deb" 12 | 13 | if (Properties.isLinux) { 14 | test("should create DEBIAN directory ") { 15 | val dmgPackage = DebianPackage(buildSettings) 16 | 17 | // create app directory 18 | dmgPackage.createDebianDir() 19 | 20 | val debianDirectoryPath = tmpDir / "debian" 21 | val expectedAppDirectoryPath = debianDirectoryPath / "DEBIAN" 22 | val expectedLauncherPath = 23 | debianDirectoryPath / "usr" / "share" / "scala" / packageName 24 | expect(os.isDir(expectedAppDirectoryPath)) 25 | expect(os.isFile(expectedLauncherPath)) 26 | } 27 | 28 | test("should generate dep package") { 29 | 30 | val depPackage = DebianPackage(buildSettings) 31 | 32 | // create dmg package 33 | depPackage.build() 34 | 35 | expect(os.exists(outputPackagePath)) 36 | 37 | // list files which will be installed 38 | val payloadFiles = 39 | os.proc("dpkg", "--contents", outputPackagePath).call().out.text().trim 40 | val expectedScriptPath = os.RelPath("usr") / "bin" / packageName 41 | val expectedLauncherPath = 42 | os.RelPath("usr") / "share" / "scala" / packageName 43 | 44 | expect(payloadFiles contains s"./$expectedScriptPath") 45 | expect(payloadFiles contains s"./$expectedLauncherPath") 46 | } 47 | 48 | test("should override generated dep package") { 49 | 50 | val depPackage = DebianPackage(buildSettings) 51 | 52 | // create twice dmg package 53 | depPackage.build() 54 | depPackage.build() 55 | 56 | expect(os.exists(outputPackagePath)) 57 | } 58 | 59 | test("should set given launcher name explicitly for debian package") { 60 | 61 | val launcherApp = "launcher-test" 62 | 63 | val buildSettingsWithLauncherName: DebianSettings = buildSettings.copy( 64 | shared = sharedSettings.copy( 65 | launcherApp = Some(launcherApp) 66 | ) 67 | ) 68 | 69 | val depPackage = 70 | DebianPackage(buildSettingsWithLauncherName) 71 | 72 | // create dmg package 73 | depPackage.build() 74 | 75 | expect(os.exists(outputPackagePath)) 76 | 77 | // list files which will be installed 78 | val payloadFiles = 79 | os.proc("dpkg", "--contents", outputPackagePath).call().out.text().trim 80 | val expectedScriptPath = os.RelPath("usr") / "bin" / launcherApp 81 | val expectedLauncherPath = 82 | os.RelPath("usr") / "share" / "scala" / launcherApp 83 | 84 | expect(payloadFiles contains s"./$expectedScriptPath") 85 | expect(payloadFiles contains s"./$expectedLauncherPath") 86 | } 87 | 88 | test("should contain priority and section flags") { 89 | 90 | val depPackage = DebianPackage(buildSettings) 91 | 92 | // create dmg package 93 | depPackage.build() 94 | 95 | // list files which will be installed 96 | val payloadFiles = os.proc("dpkg", "--info", outputPackagePath).call().out.text().trim 97 | 98 | expect(payloadFiles contains "Priority: optional") 99 | expect(payloadFiles contains "Section: devel") 100 | } 101 | } 102 | 103 | override def buildSettings: DebianSettings = 104 | DebianSettings( 105 | shared = sharedSettings, 106 | maintainer = "Scala Packager", 107 | description = "Scala Packager Test", 108 | debianConflicts = Nil, 109 | debianDependencies = Nil, 110 | architecture = "all", 111 | priority = Some("optional"), 112 | section = Some("devel") 113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/dmg/DmgPackageTests.scala: -------------------------------------------------------------------------------- 1 | package packager.dmg 2 | 3 | import com.eed3si9n.expecty.Expecty.expect 4 | import munit.Tag 5 | import packager.{NativePackageHelper, TestUtils} 6 | import packager.config.MacOSSettings 7 | import packager.mac.dmg.DmgPackage 8 | 9 | import scala.util.Properties 10 | 11 | class DmgPackageTests extends munit.FunSuite with NativePackageHelper { 12 | override def munitFlakyOK: Boolean = true 13 | 14 | private val FlakyMacOSx86_64 = new Tag("FlakyMacOSx86_64") 15 | 16 | // flakiness on x86_64 MacOS is caused by hdiutil 17 | // FIXME: find a way to make these reliable 18 | // more context: https://github.com/VirtusLab/scala-cli/pull/2579 19 | override def munitTestTransforms: List[TestTransform] = 20 | List( 21 | new TestTransform( 22 | "Flaky MacOS x86_64", 23 | { test => 24 | if (test.tags.contains(FlakyMacOSx86_64) && Properties.isMac && !TestUtils.isAarch64) 25 | test.tag(munit.Flaky) 26 | else test 27 | } 28 | ) 29 | ) ++ super.munitTestTransforms 30 | 31 | override def outputPackagePath: os.Path = tmpDir / s"scalafmt.dmg" 32 | 33 | def shouldCreateAppDirectoryForDmg(): Unit = { 34 | val dmgPackage = DmgPackage(buildSettings) 35 | 36 | // create app directory 37 | dmgPackage.createAppDirectory() 38 | 39 | val expectedAppDirectoryPath = tmpDir / s"$packageName.app" 40 | val expectedLauncherPath = 41 | expectedAppDirectoryPath / "Contents" / "MacOS" / packageName 42 | expect(os.isDir(expectedAppDirectoryPath)) 43 | expect(os.isFile(expectedLauncherPath)) 44 | } 45 | 46 | def shouldGenerateDmgPackage(): Unit = { 47 | val dmgPackage = DmgPackage(buildSettings) 48 | 49 | // create dmg package 50 | dmgPackage.build() 51 | 52 | expect(os.exists(outputPackagePath)) 53 | } 54 | 55 | def shouldOverrideGeneratedDmgPackage(): Unit = { 56 | val dmgPackage = DmgPackage(buildSettings) 57 | 58 | // create twice dmg package 59 | dmgPackage.build() 60 | dmgPackage.build() 61 | 62 | expect(os.exists(outputPackagePath)) 63 | } 64 | 65 | def sizeDmgPackageShouldBeSimilarToTheApp(): Unit = { 66 | val dmgPackage = DmgPackage(buildSettings) 67 | val launcherSize = os.size(scalafmtLauncherPath) 68 | 69 | // create dmg package 70 | dmgPackage.build() 71 | 72 | expect(os.exists(outputPackagePath)) 73 | 74 | val dmgPackageSize = os.size(outputPackagePath) 75 | 76 | expect(dmgPackageSize < launcherSize + (1024 * 1024)) 77 | } 78 | 79 | if (Properties.isMac) { 80 | test("should create app directory for dmg".tag(FlakyMacOSx86_64)) { 81 | shouldCreateAppDirectoryForDmg() 82 | } 83 | 84 | test("should generate dmg package".tag(FlakyMacOSx86_64)) { 85 | shouldGenerateDmgPackage() 86 | } 87 | 88 | test("should override generated dmg package".tag(FlakyMacOSx86_64)) { 89 | shouldOverrideGeneratedDmgPackage() 90 | } 91 | 92 | test("size dmg package should be similar to the app".tag(FlakyMacOSx86_64)) { 93 | sizeDmgPackageShouldBeSimilarToTheApp() 94 | } 95 | } 96 | 97 | override def buildSettings: MacOSSettings = 98 | MacOSSettings( 99 | shared = sharedSettings, 100 | identifier = s"org.scala.$packageName" 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/docker/DockerPackageTests.scala: -------------------------------------------------------------------------------- 1 | package packager.docker 2 | 3 | import packager.PackagerHelper 4 | import packager.config.DockerSettings 5 | import com.eed3si9n.expecty.Expecty.expect 6 | 7 | import java.nio.file.Paths; 8 | import scala.util.Properties 9 | 10 | class DockerPackageTests extends munit.FunSuite with PackagerHelper { 11 | 12 | private val qualifier = "latest" 13 | private val repository = "scalafmt-scala-packager" 14 | 15 | if (Properties.isLinux) { 16 | test("should build docker image") { 17 | val dockerPackage = DockerPackage(scalafmtLauncherPath, buildSettings) 18 | // build docker image 19 | dockerPackage.build() 20 | 21 | val expectedImage = 22 | s"$repository:$qualifier" 23 | val expectedOutput = s"scalafmt $scalafmtVersion" 24 | 25 | val output = os 26 | .proc("docker", "run", expectedImage, "--version") 27 | .call(cwd = os.root) 28 | .out 29 | .text() 30 | .trim 31 | 32 | expect(output == expectedOutput) 33 | 34 | // clear 35 | os.proc("docker", "rmi", "-f", expectedImage).call(cwd = os.root) 36 | } 37 | test("should build docker image with native application") { 38 | val nativeAppSettings = buildSettings.copy(exec = None) 39 | val dockerPackage = DockerPackage(scalafmtNativePath, nativeAppSettings) 40 | // build docker image 41 | dockerPackage.build() 42 | 43 | val expectedImage = 44 | s"$repository:$qualifier" 45 | val expectedOutput = s"scalafmt $scalafmtVersion" 46 | 47 | val output = os 48 | .proc("docker", "run", expectedImage, "--version") 49 | .call(cwd = os.root) 50 | .out 51 | .text() 52 | .trim 53 | 54 | expect(output == expectedOutput) 55 | 56 | // clear 57 | os.proc("docker", "rmi", "-f", expectedImage).call(cwd = os.root) 58 | } 59 | } 60 | 61 | override def buildSettings: DockerSettings = 62 | DockerSettings( 63 | from = "eclipse-temurin:8-jdk", 64 | registry = None, 65 | repository = repository, 66 | tag = Some(qualifier), 67 | exec = Some("sh"), 68 | dockerExecutable = Some(Paths.get("docker")) 69 | ) 70 | 71 | } 72 | -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/pkg/PkgPackageTests.scala: -------------------------------------------------------------------------------- 1 | package packager.pkg 2 | 3 | import com.eed3si9n.expecty.Expecty.expect 4 | import packager.NativePackageHelper 5 | import packager.config.MacOSSettings 6 | import packager.mac.pkg.PkgPackage 7 | 8 | import scala.util.Properties 9 | 10 | class PkgPackageTests extends munit.FunSuite with NativePackageHelper { 11 | 12 | override def outputPackagePath: os.Path = tmpDir / s"scalafmt.pkg" 13 | 14 | if (Properties.isMac) { 15 | test("should create app directory") { 16 | 17 | val pkgPackage = PkgPackage(buildSettings) 18 | 19 | // create app directory 20 | pkgPackage.createAppDirectory() 21 | 22 | val expectedAppDirectoryPath = tmpDir / s"$packageName.app" 23 | val expectedLauncherPath = expectedAppDirectoryPath / "Contents" / "MacOS" / packageName 24 | expect(os.isDir(expectedAppDirectoryPath)) 25 | expect(os.isFile(expectedLauncherPath)) 26 | } 27 | 28 | test("should generate pkg package") { 29 | 30 | val pkgPackage = PkgPackage(buildSettings) 31 | 32 | // create pkg package 33 | pkgPackage.build() 34 | 35 | expect(os.isFile(outputPackagePath)) 36 | 37 | // list files which will be installed 38 | val payloadFiles = os 39 | .proc("pkgutil", "--payload-files", outputPackagePath) 40 | .call() 41 | .out 42 | .text() 43 | .trim 44 | val expectedAppPath = os.RelPath(s"$packageName.app") 45 | val expectedLauncherPath = expectedAppPath / "Contents" / "MacOS" / packageName 46 | 47 | expect(payloadFiles contains s"./$expectedAppPath") 48 | expect(payloadFiles contains s"./$expectedLauncherPath") 49 | 50 | } 51 | 52 | test("should override generated pkg package") { 53 | val pkgPackage = PkgPackage(buildSettings) 54 | 55 | // create twice pkg package 56 | pkgPackage.build() 57 | pkgPackage.build() 58 | 59 | expect(os.isFile(outputPackagePath)) 60 | } 61 | 62 | test("should set given launcher name explicitly for pkg package") { 63 | 64 | val launcherApp = "launcher-test" 65 | 66 | val buildSettingsWithLauncherName: MacOSSettings = buildSettings.copy( 67 | shared = sharedSettings.copy( 68 | launcherApp = Some(launcherApp) 69 | ) 70 | ) 71 | 72 | val pkgPackage = 73 | PkgPackage(buildSettingsWithLauncherName) 74 | 75 | // create pkg package 76 | pkgPackage.build() 77 | 78 | expect(os.isFile(outputPackagePath)) 79 | 80 | // list files which will be installed 81 | val payloadFiles = os 82 | .proc("pkgutil", "--payload-files", outputPackagePath) 83 | .call() 84 | .out 85 | .text() 86 | .trim 87 | val expectedAppPath = os.RelPath(s"$packageName.app") 88 | val expectedLauncherPath = expectedAppPath / "Contents" / "MacOS" / launcherApp 89 | 90 | expect(payloadFiles contains s"./$expectedAppPath") 91 | expect(payloadFiles contains s"./$expectedLauncherPath") 92 | 93 | } 94 | 95 | test("should copy post install script to pkg package") { 96 | 97 | val pkgPackage = PkgPackage(buildSettings) 98 | 99 | // create deb package 100 | pkgPackage.build() 101 | 102 | // expand the flat package pkg to directory 103 | val outPath = tmpDir / "out" 104 | os.proc("pkgutil", "--expand", outputPackagePath, outPath).call() 105 | 106 | val scriptsPath = outPath / "Scripts" 107 | val postInstallScriptPath = scriptsPath / "postinstall" 108 | 109 | expect(os.isDir(scriptsPath)) 110 | expect(os.isFile(postInstallScriptPath)) 111 | } 112 | } 113 | 114 | override def buildSettings: MacOSSettings = 115 | MacOSSettings( 116 | shared = sharedSettings, 117 | identifier = s"org.scala.$packageName" 118 | ) 119 | } 120 | -------------------------------------------------------------------------------- /modules/packager/src/test/scala/packager/rpm/RedHatPackageTests.scala: -------------------------------------------------------------------------------- 1 | package packager.rpm 2 | 3 | import com.eed3si9n.expecty.Expecty.expect 4 | import packager.NativePackageHelper 5 | import packager.config.RedHatSettings 6 | 7 | import scala.util.Properties 8 | 9 | class RedHatPackageTests extends munit.FunSuite with NativePackageHelper { 10 | 11 | override def outputPackagePath: os.Path = tmpDir / s"scalafmt.rpm" 12 | 13 | if (Properties.isLinux) { 14 | test("should create rpmbuild directory ") { 15 | 16 | val rpmPackage = RedHatPackage(buildSettings) 17 | 18 | // create app directory 19 | rpmPackage.createRedHatDir() 20 | 21 | val rpmDirectoryPath = tmpDir / "rpmbuild" 22 | val expectedAppDirectoryPath = rpmDirectoryPath / "SOURCES" 23 | val expectedLauncherPath = expectedAppDirectoryPath / packageName 24 | expect(os.isDir(expectedAppDirectoryPath)) 25 | expect(os.isFile(expectedLauncherPath)) 26 | } 27 | 28 | test("should generate rpm package") { 29 | 30 | val rpmPackage = RedHatPackage(buildSettings) 31 | 32 | // create dmg package 33 | rpmPackage.build() 34 | 35 | val expectedRpmPath = tmpDir / s"$packageName.rpm" 36 | expect(os.exists(expectedRpmPath)) 37 | 38 | // list files which will be installed 39 | val payloadFiles = 40 | os.proc("rpm", "-qpl", expectedRpmPath).call().out.text().trim 41 | val expectedLauncherPath = os.RelPath("usr") / "bin" / packageName 42 | 43 | expect(payloadFiles contains s"/$expectedLauncherPath") 44 | } 45 | 46 | test("should override generated rpm package") { 47 | 48 | val rpmPackage = RedHatPackage(buildSettings) 49 | 50 | // create twice dmg package 51 | rpmPackage.build() 52 | rpmPackage.build() 53 | 54 | val expectedRpmPath = tmpDir / s"$packageName.rpm" 55 | expect(os.exists(expectedRpmPath)) 56 | } 57 | 58 | test("should set given launcher name explicitly for redhat package") { 59 | 60 | val launcherApp = "launcher-test" 61 | 62 | val buildSettingsWithLauncherName: RedHatSettings = buildSettings.copy( 63 | shared = sharedSettings.copy( 64 | launcherApp = Some(launcherApp) 65 | ) 66 | ) 67 | 68 | val rpmPackage = RedHatPackage(buildSettingsWithLauncherName) 69 | 70 | // create rpm package 71 | rpmPackage.build() 72 | 73 | // list files which will be installed 74 | val payloadFiles = 75 | os.proc("rpm", "-qpl", outputPackagePath).call().out.text().trim 76 | val expectedLauncherPath = os.RelPath("usr") / "bin" / launcherApp 77 | 78 | expect(payloadFiles contains s"/$expectedLauncherPath") 79 | } 80 | } 81 | 82 | override def buildSettings: RedHatSettings = 83 | RedHatSettings( 84 | shared = sharedSettings, 85 | description = "Scala Packager Test", 86 | license = "ASL 2.0", 87 | release = "1", 88 | rpmArchitecture = "noarch" 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /project/Deps.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Deps { 4 | def expecty = "com.eed3si9n.expecty" %% "expecty" % "0.17.0" 5 | def munit = "org.scalameta" %% "munit" % "1.1.1" 6 | def osLib = "com.lihaoyi" %% "os-lib" % "0.11.4" 7 | def caseApp = "com.github.alexarchambault" %% "case-app" % "2.1.0-M30" 8 | def thumbnailator = "net.coobird" % "thumbnailator" % "0.4.20" 9 | def image4j = "org.jclarion" % "image4j" % "0.7" 10 | def jib = "com.google.cloud.tools" % "jib-core" % "0.27.3" 11 | def commonsIo = "commons-io" % "commons-io" % "2.19.0" 12 | } 13 | -------------------------------------------------------------------------------- /project/ScalaVersions.scala: -------------------------------------------------------------------------------- 1 | object ScalaVersions { 2 | def scala212 = "2.12.20" 3 | def scala213 = "2.13.16" 4 | def all = Seq(scala213, scala212) 5 | } 6 | -------------------------------------------------------------------------------- /project/Settings.scala: -------------------------------------------------------------------------------- 1 | import sbt.{Project, file} 2 | 3 | object Settings { 4 | def project(id: String) = 5 | Project(id, file(s"modules/$id")) 6 | } 7 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.1") 2 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 3 | --------------------------------------------------------------------------------