From 4ea2f11368cde2c5877a62e25e81f6ec7808401d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Belin?= Date: Sun, 3 Nov 2024 16:30:18 +0100 Subject: [PATCH] Port the `Setup` class --- lib/index.d.ts | 1 + lib/setup.d.ts | 32 ++++++++++++++++++ src/ant/Program.hx | 27 ---------------- src/ant/Release.hx | 41 ----------------------- src/ant/Setup.hx | 70 ---------------------------------------- src/cli.coffee | 19 +++++++++++ src/index.coffee | 1 + src/setup.coffee | 46 ++++++++++++++++++++++++++ test/ant/ReleaseTest.hx | 51 ----------------------------- test/ant/SetupTest.hx | 43 ------------------------ test/release_test.coffee | 6 ++-- test/setup_test.coffee | 25 ++++++++++++++ 12 files changed, 127 insertions(+), 235 deletions(-) create mode 100644 lib/setup.d.ts delete mode 100644 src/ant/Program.hx delete mode 100644 src/ant/Release.hx delete mode 100644 src/ant/Setup.hx create mode 100644 src/cli.coffee create mode 100644 src/setup.coffee delete mode 100644 test/ant/ReleaseTest.hx delete mode 100644 test/ant/SetupTest.hx create mode 100644 test/setup_test.coffee diff --git a/lib/index.d.ts b/lib/index.d.ts index 3d76f55..c4af6d9 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -1 +1,2 @@ export * from "./release.js"; +export * from "./setup.js"; diff --git a/lib/setup.d.ts b/lib/setup.d.ts new file mode 100644 index 0000000..475c41e --- /dev/null +++ b/lib/setup.d.ts @@ -0,0 +1,32 @@ +import {Release} from "./release.js"; + +/** + * Manages the download and installation of Apache Ant. + */ +export declare class Setup { + + /** + * The release to download and install. + */ + release: Release; + + /** + * Creates a new setup. + * @param release The release to download and install. + */ + constructor(release: Release); + + /** + * Downloads and extracts the ZIP archive of Apache Ant. + * @param options Value indicating whether to fetch the Ant optional tasks. + * @returns The path to the extracted directory. + */ + download(options?: {optionalTasks?: boolean}): Promise; + + /** + * Installs Apache Ant, after downloading it if required. + * @param options Value indicating whether to fetch the Ant optional tasks. + * @returns The path to the installation directory. + */ + install(options?: {optionalTasks?: boolean}): Promise; +} diff --git a/src/ant/Program.hx b/src/ant/Program.hx deleted file mode 100644 index c4ae793..0000000 --- a/src/ant/Program.hx +++ /dev/null @@ -1,27 +0,0 @@ -package ant; - -import js.actions.Core; -import js.Node; -import tink.semver.Constraint; - -/** Application entry point. **/ -function main() { - Node.process.title = "Setup Ant"; - - final optionalTasks = Core.getBooleanInput("optional-tasks"); - final version = Core.getInput("version"); - - switch Constraint.parse(version.length == 0 || version == "latest" ? "*" : version) { - case Failure(_): Core.setFailed("Invalid version constraint."); - case Success(constraint): switch Release.find(constraint) { - case None: Core.setFailed("No release matching the version constraint."); - case Some(release): new Setup(release).install(optionalTasks).handle(outcome -> switch outcome { - case Failure(error): - Core.setFailed(error.message); - case Success(path): - final installed = optionalTasks ? "installed with optional tasks" : "installed"; - Core.info('Apache Ant ${release.version} successfully $installed in "$path".'); - }); - } - } -} diff --git a/src/ant/Release.hx b/src/ant/Release.hx deleted file mode 100644 index d44b76f..0000000 --- a/src/ant/Release.hx +++ /dev/null @@ -1,41 +0,0 @@ -package ant; - -import coconut.data.List; -import coconut.data.Model; -import haxe.Resource; -import tink.Json; -import tink.Url; -import tink.semver.Constraint; -import tink.semver.Version; - -/** Represents a GitHub release. **/ -@:jsonParse(json -> new ant.Release(json)) -class Release implements Model { - - /** The latest release. **/ - public static var latest(get, never): Release; - static function get_latest() return data.first().sure(); - - /** The base URL of the releases. **/ - static final baseUrl: Url = "https://dlcdn.apache.org/ant/binaries/"; - - /** The list of all releases. **/ - static final data: List = (Json.parse(Resource.getString("releases.json")): Array); - - /** Value indicating whether this release exists. **/ - @:computed var exists: Bool = data.exists(release -> release.version == version); - - /** The download URL. **/ - @:computed var url: Url = baseUrl.resolve('apache-ant-$version-bin.zip'); - - /** The version number. **/ - @:constant var version: String = @byDefault "0.0.0"; - - /** Finds a release that matches the specified version constraint. **/ - public static function find(constraint: Constraint): Option - return data.first(release -> constraint.matches(release.version)); - - /** Gets the release corresponding to the specified version. **/ - public static function get(version: Version): Option - return data.first(release -> release.version == version); -} diff --git a/src/ant/Setup.hx b/src/ant/Setup.hx deleted file mode 100644 index ace8380..0000000 --- a/src/ant/Setup.hx +++ /dev/null @@ -1,70 +0,0 @@ -package ant; - -import js.actions.Core; -import js.actions.ToolCache; -import sys.FileSystem; -using Lambda; -using StringTools; -using haxe.io.Path; - -/** Manages the download and installation of Apache Ant. **/ -class Setup { - - /** The release to download and install. **/ - public final release: Release; - - /** Creates a new setup. **/ - public function new(release: Release) this.release = release; - - /** - Downloads and extracts the ZIP archive of Apache Ant. - Returns the path to the extracted directory. - **/ - public function download(optionalTasks = false): Promise - return ToolCache.downloadTool(release.url).toPromise() - .next(file -> ToolCache.extractZip(file)) - .next(path -> findSubfolder(path).next(name -> normalizeSeparator(Path.join([path, name])))) - .next(antHome -> optionalTasks ? fetchOptionalTasks(antHome).next(_ -> antHome) : Promise.resolve(antHome)); - - /** - Installs Apache Ant, after downloading it if required. - Returns the path to the install directory. - **/ - public function install(optionalTasks = false): Promise { - final directory = ToolCache.find("ant", release.version); - final promise = directory.length > 0 - ? Promise.resolve(directory) - : download(optionalTasks).next(path -> ToolCache.cacheDir(path, "ant", release.version)); - - return promise.next(path -> { - final normalizedPath = normalizeSeparator(path); - Core.addPath(normalizeSeparator(Path.join([path, "bin"]))); - Core.exportVariable("ANT_HOME", normalizedPath); - normalizedPath; - }); - } - - /** Fetches the external libraries required by Ant optional tasks. **/ - function fetchOptionalTasks(antHome: String): Promise { - final workingDirectory = Sys.getCwd(); - Sys.setCwd(antHome); - Sys.putEnv("ANT_HOME", antHome); - Sys.command("java -jar lib/ant-launcher.jar -buildfile fetch.xml -noinput -silent -Ddest=system"); - Sys.setCwd(workingDirectory); - return Noise; - } - - /** Determines the name of the single subfolder in the specified `directory`. **/ - function findSubfolder(directory: String): Outcome { - final folders = FileSystem.readDirectory(directory).filter(name -> FileSystem.isDirectory(Path.join([directory, name]))); - return switch folders.length { - case 0: Failure(new Error(NotFound, 'No subfolder found in: $directory.')); - case 1: Success(folders.pop()); - case _: Failure(new Error(Conflict, 'Multiple subfolders found in: $directory.')); - } - } - - /** Normalizes the segment separators of the given `path` using the platform-specific separator. **/ - function normalizeSeparator(path: String): String - return Sys.systemName() == "Windows" ? path.replace("/", "\\") : path; -} diff --git a/src/cli.coffee b/src/cli.coffee new file mode 100644 index 0000000..e96c04e --- /dev/null +++ b/src/cli.coffee @@ -0,0 +1,19 @@ +import {getBooleanInput, getInput, info, setFailed} from "@actions/core" +import process from "node:process" +import {Release} from "./release.js" +import {Setup} from "./setup.js" + +try + process.title = "Setup Ant" + + version = getInput "version" + release = Release.find if not version or version is "latest" then "*" else version + throw Error "No release matching the version constraint." if not release + + optionalTasks = getBooleanInput "optional-tasks" + installed = if optionalTasks then "installed with optional tasks" else "installed" + path = await new Setup(release).install {optionalTasks} + info "Apache Ant #{release.version} successfully #{installed} in \"#{path}\"." + +catch error + setFailed if error instanceof Error then error else String error diff --git a/src/index.coffee b/src/index.coffee index df97a06..969edc5 100644 --- a/src/index.coffee +++ b/src/index.coffee @@ -1 +1,2 @@ export * from "./release.js" +export * from "./setup.js" diff --git a/src/setup.coffee b/src/setup.coffee new file mode 100644 index 0000000..8a1f0f4 --- /dev/null +++ b/src/setup.coffee @@ -0,0 +1,46 @@ +import {addPath, exportVariable} from "@actions/core" +import {cacheDir, downloadTool, extractZip, find} from "@actions/tool-cache" +import {execFile} from "node:child_process" +import {readdir} from "node:fs/promises" +import {join} from "node:path" +import {promisify} from "node:util" + +# Manages the download and installation of Apache Ant. +export class Setup + + # Creates a new setup. + constructor: (release) -> + + # The release to download and install. + @release = release + + # Downloads and extracts the ZIP archive of Apache Ant. + # Returns the path to the extracted directory. + download: (options = {}) -> + directory = await extractZip await downloadTool @release.url.href + antHome = join directory, await @_findSubfolder directory + await @_fetchOptionalTasks antHome if options.optionalTasks + antHome + + # Installs Apache Ant, after downloading it if required. + # Returns the path to the install directory. + install: (options = {}) -> + antHome = find("ant", @release.version) ? await cacheDir (await @download options), "ant", @release.version + addPath join antHome, "bin" + exportVariable "ANT_HOME", antHome + antHome + + # Fetches the external libraries required by Ant optional tasks. + _fetchOptionalTasks: (antHome) -> + run = promisify execFile + run "java", ["-jar", "lib/ant-launcher.jar", "-buildfile", "fetch.xml", "-noinput", "-silent", "-Ddest=system"], + cwd: antHome + env: {ANT_HOME: antHome} + + # Determines the name of the single subfolder in the specified directory. + _findSubfolder: (directory) -> + folders = (await readdir directory, withFileTypes: true).filter (entity) -> entity.isDirectory() + switch folders.length + when 0 then throw Error "No subfolder found in: #{directory}." + when 1 then return folders[0].name + else throw Error "Multiple subfolders found in: #{directory}." diff --git a/test/ant/ReleaseTest.hx b/test/ant/ReleaseTest.hx deleted file mode 100644 index ff84191..0000000 --- a/test/ant/ReleaseTest.hx +++ /dev/null @@ -1,51 +0,0 @@ -package ant; - -/** Tests the features of the `Release` class. **/ -@:asserts final class ReleaseTest { - - /** A release that exists. **/ - public static final existingRelease = new Release({version: "1.10.14"}); - - /** A release that doesn't exist. **/ - public static final nonExistentRelease = new Release({version: "666.6.6"}); - - /** Creates a new test. **/ - public function new() {} - - /** Tests the `exists` property. **/ - @:variant(ant.ReleaseTest.existingRelease, true) - @:variant(ant.ReleaseTest.nonExistentRelease, false) - public function exists(input: Release, output: Bool) - return assert(input.exists == output); - - /** Tests the `latest` property. **/ - public function latest() { - asserts.doesNotThrow(() -> Release.latest); - return asserts.done(); - } - - /** Tests the `url` property. **/ - @:variant(ant.ReleaseTest.existingRelease, "apache-ant-1.10.14-bin.zip") - @:variant(ant.ReleaseTest.nonExistentRelease, "apache-ant-666.6.6-bin.zip") - public function url(input: Release, output: String) - return assert(input.url == 'https://dlcdn.apache.org/ant/binaries/$output'); - - /** Tests the `find()` method. **/ - @:variant("*", Some(ant.Release.latest.version)) - @:variant("1.x", Some(ant.Release.latest.version)) - @:variant("=1.9.16", Some("1.9.16")) - @:variant(">=1.0.0 <1.10.0", Some("1.9.16")) - @:variant("666.6.6", None) - public function find(input: String, output: Option) return switch Release.find(input) { - case None: assert(output == None); - case Some(release): assert(output.equals(release.version)); - } - - /** Tests the `get()` method. **/ - @:variant("1.10.14", Some("1.10.14")) - @:variant("666.6.6", None) - public function get(input: String, output: Option) return switch Release.get(input) { - case None: assert(output == None); - case Some(release): assert(output.equals(release.version)); - } -} diff --git a/test/ant/SetupTest.hx b/test/ant/SetupTest.hx deleted file mode 100644 index 63ad603..0000000 --- a/test/ant/SetupTest.hx +++ /dev/null @@ -1,43 +0,0 @@ -package ant; - -import sys.FileSystem; -using Lambda; -using StringTools; -using haxe.io.Path; - -/** Tests the features of the `Setup` class. **/ -@:asserts final class SetupTest { - - /** Creates a new test. **/ - public function new() {} - - /** Tests the `download()` method. **/ - @:timeout(180_000) - public function download() { - new Setup(Release.latest).download(true).next(path -> { - final jars = FileSystem.readDirectory(Path.join([path, "lib"])).filter(file -> file.extension() == "jar"); - asserts.assert(FileSystem.exists(Path.join([path, "bin", Sys.systemName() == "Windows" ? "ant.cmd" : "ant"]))); - asserts.assert(jars.filter(file -> file.startsWith("ivy-")).length == 1); - }).handle(asserts.handle); - - return asserts; - } - - /** Tests the `install()` method. **/ - @:timeout(180_000) - public function install() { - new Setup(Release.latest).install(false).next(path -> { - asserts.assert(Sys.getEnv("ANT_HOME") == path); - asserts.assert(Sys.getEnv("PATH").contains(path)); - }).handle(asserts.handle); - - return asserts; - } - - /** Method invoked once before running the first test. **/ - @:setup public function setup(): Promise { - if (Sys.getEnv("RUNNER_TEMP") == null) Sys.putEnv("RUNNER_TEMP", FileSystem.absolutePath("var/tmp")); - if (Sys.getEnv("RUNNER_TOOL_CACHE") == null) Sys.putEnv("RUNNER_TOOL_CACHE", FileSystem.absolutePath("var/cache")); - return Noise; - } -} diff --git a/test/release_test.coffee b/test/release_test.coffee index 070e206..a6150e5 100644 --- a/test/release_test.coffee +++ b/test/release_test.coffee @@ -5,10 +5,10 @@ import {Release} from "@cedx/setup-ant" # Tests the features of the `Release` class. describe "Release", -> existingRelease = new Release "1.10.15" - nonExistentRelease = new Release "666.6.6" + nonExistingRelease = new Release "666.6.6" describe "exists", -> - it "should return `false` if the release does not exist", -> ok not nonExistentRelease.exists + it "should return `false` if the release does not exist", -> ok not nonExistingRelease.exists it "should return `true` if the release exists", -> ok existingRelease.exists describe "latest", -> @@ -17,7 +17,7 @@ describe "Release", -> describe "url", -> it "should return the URL of the Ant archive", -> equal existingRelease.url.href, "https://dlcdn.apache.org/ant/binaries/apache-ant-1.10.15-bin.zip" - equal nonExistentRelease.url.href, "https://dlcdn.apache.org/ant/binaries/apache-ant-666.6.6-bin.zip" + equal nonExistingRelease.url.href, "https://dlcdn.apache.org/ant/binaries/apache-ant-666.6.6-bin.zip" describe "find()", -> it "should return `null` if no release matches the version constraint", -> diff --git a/test/setup_test.coffee b/test/setup_test.coffee new file mode 100644 index 0000000..8d61de7 --- /dev/null +++ b/test/setup_test.coffee @@ -0,0 +1,25 @@ +import {doesNotReject, equal, ok} from "node:assert/strict" +import {access, readdir} from "node:fs/promises" +import {extname, join, resolve} from "node:path" +import {env, platform} from "node:process" +import {describe, it} from "node:test" +import {Release, Setup} from "@cedx/setup-ant" + +# Tests the features of the `Setup` class. +describe "Setup", -> + env.RUNNER_TEMP ?= resolve "var/tmp" + env.RUNNER_TOOL_CACHE ?= resolve "var/cache" + + describe "download()", -> + it "should properly download and extract Apache Ant", -> + path = await new Setup(Release.latest).download optionalTasks: yes + await doesNotReject access join path, "bin", if platform is "win32" then "ant.cmd" else "ant" + + jars = (await readdir join path, "lib").filter (file) -> extname(file) is ".jar" + equal jars.filter((file) -> file.startsWith "ivy-").length, 1 + + describe "install()", -> + it "should add the Ant directory to the PATH environment variable", -> + path = await new Setup(Release.latest).install optionalTasks: no + equal env.ANT_HOME, path + ok env.PATH.includes path