├── .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 |
--------------------------------------------------------------------------------