Skip to content

Commit

Permalink
Added .dmg and .pkg support for macos
Browse files Browse the repository at this point in the history
  • Loading branch information
ekaterina-tatanova committed Jul 29, 2020
1 parent c3dce98 commit 699ae5d
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 50 deletions.
28 changes: 14 additions & 14 deletions Tasks/JavaToolInstallerV0/FileExtractor/JavaFilesExtractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<void> {
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
169 changes: 140 additions & 29 deletions Tasks/JavaToolInstallerV0/javatoolinstaller.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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');
Expand All @@ -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'));
Expand All @@ -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<any> {
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<string> {
let jdkDirectory;
if (fileExtension === '.dmg' && os.platform() === 'darwin') {
// Using set because 'includes' array method requires tsconfig option "lib": ["ES2017"]
const volumes: Set<string> = 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>): 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<string> {
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<string> = 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<number> {
const installer = sudo('installer');
installer.line(`-package "${pkgPath}" -target /`);
return await installer.exec();
}

run();
15 changes: 12 additions & 3 deletions Tasks/JavaToolInstallerV0/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"author": "Microsoft Corporation",
"version": {
"Major": 0,
"Minor": 173,
"Patch": 1
"Minor": 174,
"Patch": 0
},
"satisfies": [
"Java"
Expand Down Expand Up @@ -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"
}
}
16 changes: 13 additions & 3 deletions Tasks/JavaToolInstallerV0/task.loc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"author": "Microsoft Corporation",
"version": {
"Major": 0,
"Minor": 173,
"Patch": 1
"Minor": 174,
"Patch": 0
},
"satisfies": [
"Java"
Expand Down Expand Up @@ -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"
}
}
57 changes: 57 additions & 0 deletions Tasks/JavaToolInstallerV0/taskutils.ts
Original file line number Diff line number Diff line change
@@ -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<any>
*/
export function sleepFor(sleepDurationInMilliSeconds: number): Promise<any> {
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<number> {
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<number> {
console.log(tl.loc('DetachDiskImage'));
const hdiutil = sudo('hdiutil');
hdiutil.line(`detach "${volumePath}"`);
return await hdiutil.exec();
}

0 comments on commit 699ae5d

Please sign in to comment.