diff --git a/Tasks/JavaToolInstallerV0/FileExtractor/JavaFilesExtractor.ts b/Tasks/JavaToolInstallerV0/FileExtractor/JavaFilesExtractor.ts index 351e0bde9b16..64b9ec48aef5 100644 --- a/Tasks/JavaToolInstallerV0/FileExtractor/JavaFilesExtractor.ts +++ b/Tasks/JavaToolInstallerV0/FileExtractor/JavaFilesExtractor.ts @@ -4,6 +4,8 @@ import * as path from 'path'; import * as taskLib from 'azure-pipelines-task-lib/task'; import * as toolLib from 'azure-pipelines-tool-lib/tool'; +const supportedFileEndings = ['.tar', '.tar.gz', '.zip', '.7z', '.dmg', '.pkg']; + export const BIN_FOLDER = 'bin'; interface IDirectoriesDictionary { @@ -70,22 +72,20 @@ export class JavaFilesExtractor { } } - public static getFileEnding(file: string): string { - let fileEnding = ''; - - if (file.endsWith('.tar')) { - fileEnding = '.tar'; - } else if (file.endsWith('.tar.gz')) { - fileEnding = '.tar.gz'; - } else if (file.endsWith('.zip')) { - fileEnding = '.zip'; - } else if (file.endsWith('.7z')) { - fileEnding = '.7z'; + /** + * Get file ending if it is supported. Otherwise throw an error. + * Find file ending, not extension. For example, there is supported .tar.gz file ending but the extension is .gz. + * @param file Path to a file. + * @returns string + */ + public static getSupportedFileEnding(file: string): string { + const fileEnding: string = supportedFileEndings.find(ending => file.endsWith(ending)); + + if (fileEnding) { + return fileEnding; } else { throw new Error(taskLib.loc('UnsupportedFileExtension')); } - - return fileEnding; } private async extractFiles(file: string, fileEnding: string): Promise { @@ -182,7 +182,7 @@ export class JavaFilesExtractor { */ public static getStrippedName(name: string): string { const fileBaseName: string = path.basename(name); - const fileEnding: string = JavaFilesExtractor.getFileEnding(fileBaseName); + const fileEnding: string = JavaFilesExtractor.getSupportedFileEnding(fileBaseName); return fileBaseName.substring(0, fileBaseName.length - fileEnding.length); } diff --git a/Tasks/JavaToolInstallerV0/Strings/resources.resjson/en-US/resources.resjson b/Tasks/JavaToolInstallerV0/Strings/resources.resjson/en-US/resources.resjson index 6fda51432fd6..e20adb521735 100644 --- a/Tasks/JavaToolInstallerV0/Strings/resources.resjson/en-US/resources.resjson +++ b/Tasks/JavaToolInstallerV0/Strings/resources.resjson/en-US/resources.resjson @@ -53,5 +53,15 @@ "loc.messages.CorrelationIdForARM": "Correlation ID from ARM api call response : %s", "loc.messages.JavaNotPreinstalled": "Java %s is not preinstalled on this agent", "loc.messages.UsePreinstalledJava": "Use preinstalled JDK from %s", - "loc.messages.WrongArchiveStructure": "JDK file is not valid. Verify if JDK file contains only one root folder with 'bin' inside." + "loc.messages.WrongArchiveStructure": "JDK file is not valid. Verify if JDK file contains only one root folder with 'bin' inside.", + "loc.messages.ShareAccessError": "Network shared resource not available: (%s)", + "loc.messages.UnsupportedDMGStructure": "JDK file is not supported. Verify if JDK file contains exactly one folder inside.", + "loc.messages.NoPKGFile": "Could not find PKG file.", + "loc.messages.SeveralPKGFiles": "Found more than one PKG files.", + "loc.messages.InstallJDK": "Installing JDK.", + "loc.messages.AttachDiskImage": "Attaching a disk image.", + "loc.messages.DetachDiskImage": "Detaching a disk image.", + "loc.messages.PkgPathDoesNotExist": "Package path does not exist.", + "loc.messages.PreInstalledJavaUpgraded": "Preinstalled JDK updated.", + "loc.messages.JavaSuccessfullyInstalled": "Java has been successfully installed" } diff --git a/Tasks/JavaToolInstallerV0/javatoolinstaller.ts b/Tasks/JavaToolInstallerV0/javatoolinstaller.ts index 22b5981bd680..c2e4353adaa6 100644 --- a/Tasks/JavaToolInstallerV0/javatoolinstaller.ts +++ b/Tasks/JavaToolInstallerV0/javatoolinstaller.ts @@ -1,12 +1,16 @@ import fs = require('fs'); +import os = require('os'); import path = require('path'); import taskLib = require('azure-pipelines-task-lib/task'); import toolLib = require('azure-pipelines-tool-lib/tool'); -import { AzureStorageArtifactDownloader } from "./AzureStorageArtifacts/AzureStorageArtifactDownloader"; -import { JavaFilesExtractor } from './FileExtractor/JavaFilesExtractor'; -import {BIN_FOLDER} from "./FileExtractor/JavaFilesExtractor"; +import { AzureStorageArtifactDownloader } from './AzureStorageArtifacts/AzureStorageArtifactDownloader'; +import { JavaFilesExtractor, BIN_FOLDER } from './FileExtractor/JavaFilesExtractor'; +import { sleepFor, sudo, attach, detach } from './taskutils'; +const VOLUMES_FOLDER = '/Volumes'; +const JDK_FOLDER = '/Library/Java/JavaVirtualMachines'; +const JDK_HOME_FOLDER = 'Contents/Home'; taskLib.setResourcePath(path.join(__dirname, 'task.json')); async function run() { @@ -21,24 +25,12 @@ async function run() { } async function getJava(versionSpec: string) { - const preInstalled: boolean = ("PreInstalled" === taskLib.getInput('jdkSourceOption', true)); + const preInstalled: boolean = ('PreInstalled' === taskLib.getInput('jdkSourceOption', true)); const fromAzure: boolean = ('AzureStorage' == taskLib.getInput('jdkSourceOption', true)); const extractLocation: string = taskLib.getPathInput('jdkDestinationDirectory', true); const cleanDestinationDirectory: boolean = taskLib.getBoolInput('cleanDestinationDirectory', false); - const unpackArchive = async (unpackDir, jdkFileName, fileExt) => { - const javaFilesExtractor = new JavaFilesExtractor(); - if (!cleanDestinationDirectory && taskLib.exist(unpackDir)){ - // do nothing since the files were extracted and ready for using - console.log(taskLib.loc('ArchiveWasExtractedEarlier')); - } else { - // unpack files to specified directory - console.log(taskLib.loc('ExtractingArchiveToPath', unpackDir)); - await javaFilesExtractor.unzipJavaDownload(jdkFileName, fileExt, unpackDir); - } - }; let compressedFileExtension: string; let jdkDirectory: string; - let extractionDirectory: string; const extendedJavaHome: string = `JAVA_HOME_${versionSpec}_${taskLib.getInput('jdkArchitectureOption', true)}`; toolLib.debug('Trying to get tool from local cache first'); @@ -60,14 +52,13 @@ async function getJava(versionSpec: string) { console.log(taskLib.loc('Info_ResolvedToolFromCache', version)); } else if (preInstalled) { const preInstalledJavaDirectory: string | undefined = taskLib.getVariable(extendedJavaHome); - if (preInstalledJavaDirectory === undefined) { + if (!preInstalledJavaDirectory) { throw new Error(taskLib.loc('JavaNotPreinstalled', versionSpec)); } console.log(taskLib.loc('UsePreinstalledJava', preInstalledJavaDirectory)); jdkDirectory = JavaFilesExtractor.setJavaHome(preInstalledJavaDirectory, false); } else { - let extractDirectoryName; - let jdkFileName; + let jdkFileName: string; if (fromAzure) { // download from azure and save to temporary directory console.log(taskLib.loc('RetrievingJdkFromAzure')); @@ -82,22 +73,142 @@ async function getJava(versionSpec: string) { console.log(taskLib.loc('RetrievingJdkFromLocalPath')); jdkFileName = taskLib.getInput('jdkFile', true); } - // unpack the archive, set `JAVA_HOME` and save it for further processing - compressedFileExtension = JavaFilesExtractor.getFileEnding(jdkFileName); - extractDirectoryName = `${extendedJavaHome}_${JavaFilesExtractor.getStrippedName(jdkFileName)}_${compressedFileExtension.substr(1)}`; - extractionDirectory = path.join(extractLocation, extractDirectoryName); - await unpackArchive(extractionDirectory, jdkFileName, compressedFileExtension); - jdkDirectory = JavaFilesExtractor.setJavaHome(extractionDirectory); + compressedFileExtension = JavaFilesExtractor.getSupportedFileEnding(jdkFileName); + jdkDirectory = await installJDK(jdkFileName, compressedFileExtension, extractLocation, extendedJavaHome, versionSpec, cleanDestinationDirectory); } console.log(taskLib.loc('SetExtendedJavaHome', extendedJavaHome, jdkDirectory)); taskLib.setVariable(extendedJavaHome, jdkDirectory); toolLib.prependPath(path.join(jdkDirectory, BIN_FOLDER)); } -function sleepFor(sleepDurationInMillisecondsSeconds): Promise { - return new Promise((resolve, reeject) => { - setTimeout(resolve, sleepDurationInMillisecondsSeconds); - }); +/** + * Install JDK. + * @param sourceFile Path to JDK file. + * @param fileExtension JDK file extension. + * @param archiveExtractLocation Path to folder to extract a JDK. + * @returns string + */ +async function installJDK(sourceFile: string, fileExtension: string, archiveExtractLocation: string, extendedJavaHome: string, versionSpec: string, cleanDestinationDirectory: boolean): Promise { + let jdkDirectory; + if (fileExtension === '.dmg' && os.platform() === 'darwin') { + // Using set because 'includes' array method requires tsconfig option "lib": ["ES2017"] + const volumes: Set = new Set(fs.readdirSync(VOLUMES_FOLDER)); + + await attach(sourceFile); + + const volumePath: string = getVolumePath(volumes); + + let pkgPath: string = getPackagePath(volumePath); + try { + jdkDirectory = await installPkg(pkgPath, extendedJavaHome, versionSpec); + } catch (error) { + throw error; + } finally { + // In case of an error, there is still a need to detach the disk image + await detach(volumePath); + } + } + else if (fileExtension === '.pkg' && os.platform() === 'darwin') { + jdkDirectory = await installPkg(sourceFile, extendedJavaHome, versionSpec); + } + else { + // unpack the archive, set `JAVA_HOME` and save it for further processing + const extractDirectoryName: string = `${extendedJavaHome}_${JavaFilesExtractor.getStrippedName(sourceFile)}_${fileExtension.substr(1)}`; + const extractionDirectory: string = path.join(archiveExtractLocation, extractDirectoryName); + await unpackArchive(extractionDirectory, sourceFile, fileExtension, cleanDestinationDirectory); + jdkDirectory = JavaFilesExtractor.setJavaHome(extractionDirectory); + } + return jdkDirectory; +} + +async function unpackArchive(unpackDir: string, jdkFileName: string, fileExt: string, cleanDestinationDirectory: boolean) { + const javaFilesExtractor = new JavaFilesExtractor(); + if (!cleanDestinationDirectory && taskLib.exist(unpackDir)){ + // do nothing since the files were extracted and ready for using + console.log(taskLib.loc('ArchiveWasExtractedEarlier')); + } else { + // unpack files to specified directory + console.log(taskLib.loc('ExtractingArchiveToPath', unpackDir)); + await javaFilesExtractor.unzipJavaDownload(jdkFileName, fileExt, unpackDir); + } +}; + +/** + * Get the path to a folder inside the VOLUMES_FOLDER. + * Only for macOS. + * @param volumes VOLUMES_FOLDER contents before attaching a disk image. + * @returns string + */ +function getVolumePath(volumes: Set): string { + const newVolumes: string[] = fs.readdirSync(VOLUMES_FOLDER).filter(volume => !volumes.has(volume)); + + if (newVolumes.length !== 1) { + throw new Error(taskLib.loc('UnsupportedDMGStructure')); + } + return path.join(VOLUMES_FOLDER, newVolumes[0]); +} + +/** + * Get path to a .pkg file. + * Only for macOS. + * @param volumePath Path to the folder containing a .pkg file. + * @returns string + */ +function getPackagePath(volumePath: string): string { + const packages: string[] = fs.readdirSync(volumePath).filter(file => file.endsWith('.pkg')); + + if (packages.length === 1) { + return path.join(volumePath, packages[0]); + } else if (packages.length === 0) { + throw new Error(taskLib.loc('NoPKGFile')); + } else { + throw new Error(taskLib.loc('SeveralPKGFiles')); + } +} + +async function installPkg(pkgPath: string, extendedJavaHome: string, versionSpec: string): Promise { + if (!fs.existsSync(pkgPath)) { + throw new Error('PkgPathDoesNotExist'); + } + + console.log(taskLib.loc('InstallJDK')); + + // Using set because 'includes' array method requires tsconfig option "lib": ["ES2017"] + const JDKs: Set = new Set(fs.readdirSync(JDK_FOLDER)); + + await runPkgInstaller(pkgPath); + + const newJDKs = fs.readdirSync(JDK_FOLDER).filter(jdkName => !JDKs.has(jdkName)); + + let jdkDirectory: string; + + if (newJDKs.length === 0) { + const preInstalledJavaDirectory: string | undefined = taskLib.getVariable(extendedJavaHome); + if (!preInstalledJavaDirectory) { + throw new Error(taskLib.loc('JavaNotPreinstalled', versionSpec)); + } + console.log(taskLib.loc('PreInstalledJavaUpgraded')); + console.log(taskLib.loc('UsePreinstalledJava', preInstalledJavaDirectory)); + jdkDirectory = preInstalledJavaDirectory; + } else { + console.log(taskLib.loc('JavaSuccessfullyInstalled')); + jdkDirectory = path.join(JDK_FOLDER, newJDKs[0], JDK_HOME_FOLDER); + } + + return jdkDirectory; +} + +/** + * Install a .pkg file. + * Only for macOS. + * Returns promise with return code. + * @param pkgPath Path to a .pkg file. + * @returns number + */ +async function runPkgInstaller(pkgPath: string): Promise { + const installer = sudo('installer'); + installer.line(`-package "${pkgPath}" -target /`); + return await installer.exec(); } run(); diff --git a/Tasks/JavaToolInstallerV0/task.json b/Tasks/JavaToolInstallerV0/task.json index 5b9e4f09eb59..5cb1ab3d476c 100644 --- a/Tasks/JavaToolInstallerV0/task.json +++ b/Tasks/JavaToolInstallerV0/task.json @@ -13,8 +13,8 @@ "author": "Microsoft Corporation", "version": { "Major": 0, - "Minor": 173, - "Patch": 1 + "Minor": 174, + "Patch": 0 }, "satisfies": [ "Java" @@ -178,6 +178,15 @@ "JavaNotPreinstalled": "Java %s is not preinstalled on this agent", "UsePreinstalledJava": "Use preinstalled JDK from %s", "WrongArchiveStructure": "JDK file is not valid. Verify if JDK file contains only one root folder with 'bin' inside.", - "ShareAccessError": "Network shared resource not available: (%s)" + "ShareAccessError": "Network shared resource not available: (%s)", + "UnsupportedDMGStructure": "JDK file is not supported. Verify if JDK file contains exactly one folder inside.", + "NoPKGFile": "Could not find PKG file.", + "SeveralPKGFiles": "Found more than one PKG files.", + "InstallJDK": "Installing JDK.", + "AttachDiskImage": "Attaching a disk image.", + "DetachDiskImage": "Detaching a disk image.", + "PkgPathDoesNotExist": "Package path does not exist.", + "PreInstalledJavaUpgraded": "Preinstalled JDK updated.", + "JavaSuccessfullyInstalled": "Java has been successfully installed" } } diff --git a/Tasks/JavaToolInstallerV0/task.loc.json b/Tasks/JavaToolInstallerV0/task.loc.json index 41bb6434bfee..eff8374e1d20 100644 --- a/Tasks/JavaToolInstallerV0/task.loc.json +++ b/Tasks/JavaToolInstallerV0/task.loc.json @@ -13,8 +13,8 @@ "author": "Microsoft Corporation", "version": { "Major": 0, - "Minor": 173, - "Patch": 1 + "Minor": 174, + "Patch": 0 }, "satisfies": [ "Java" @@ -177,6 +177,16 @@ "CorrelationIdForARM": "ms-resource:loc.messages.CorrelationIdForARM", "JavaNotPreinstalled": "ms-resource:loc.messages.JavaNotPreinstalled", "UsePreinstalledJava": "ms-resource:loc.messages.UsePreinstalledJava", - "WrongArchiveStructure": "ms-resource:loc.messages.WrongArchiveStructure" + "WrongArchiveStructure": "ms-resource:loc.messages.WrongArchiveStructure", + "ShareAccessError": "ms-resource:loc.messages.ShareAccessError", + "UnsupportedDMGStructure": "ms-resource:loc.messages.UnsupportedDMGStructure", + "NoPKGFile": "ms-resource:loc.messages.NoPKGFile", + "SeveralPKGFiles": "ms-resource:loc.messages.SeveralPKGFiles", + "InstallJDK": "ms-resource:loc.messages.InstallJDK", + "AttachDiskImage": "ms-resource:loc.messages.AttachDiskImage", + "DetachDiskImage": "ms-resource:loc.messages.DetachDiskImage", + "PkgPathDoesNotExist": "ms-resource:loc.messages.PkgPathDoesNotExist", + "PreInstalledJavaUpgraded": "ms-resource:loc.messages.PreInstalledJavaUpgraded", + "JavaSuccessfullyInstalled": "ms-resource:loc.messages.JavaSuccessfullyInstalled" } } diff --git a/Tasks/JavaToolInstallerV0/taskutils.ts b/Tasks/JavaToolInstallerV0/taskutils.ts new file mode 100644 index 000000000000..386b19084187 --- /dev/null +++ b/Tasks/JavaToolInstallerV0/taskutils.ts @@ -0,0 +1,57 @@ +import * as tl from 'azure-pipelines-task-lib/task'; +import * as os from 'os'; + +import { ToolRunner } from 'azure-pipelines-task-lib/toolrunner'; + +/** + * Returns promise which will be resolved in given number of milliseconds. + * @param sleepDurationInMilliSeconds Number of milliseconds. + * @returns Promise + */ +export function sleepFor(sleepDurationInMilliSeconds: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(resolve, sleepDurationInMilliSeconds); +}); +} + +/** + * Run a tool with `sudo` on Linux and macOS. + * Precondition: `toolName` executable is in PATH. + * @returns ToolRunner + */ +export function sudo(toolName: string): ToolRunner { + if (os.platform() === 'win32') { + return tl.tool(toolName); + } else { + const toolPath = tl.which(toolName); + return tl.tool('sudo').line(toolPath); + } +} + +/** + * Attach a disk image. + * Only for macOS. + * Returns promise with return code. + * @param sourceFile Path to a disk image file. + * @returns number + */ +export async function attach(sourceFile: string): Promise { + console.log(tl.loc('AttachDiskImage')); + const hdiutil = sudo('hdiutil'); + hdiutil.line(`attach "${sourceFile}"`); + return await hdiutil.exec(); +} + +/** + * Detach a disk image. + * Only for macOS. + * Returns promise with return code. + * @param volumePath Path to the attached disk image. + * @returns number + */ +export async function detach(volumePath: string): Promise { + console.log(tl.loc('DetachDiskImage')); + const hdiutil = sudo('hdiutil'); + hdiutil.line(`detach "${volumePath}"`); + return await hdiutil.exec(); +}