From a479e6757217338547302bdcb566e99703b4f28c Mon Sep 17 00:00:00 2001 From: 0000matteo0000 Date: Thu, 22 Jun 2023 12:48:09 +0200 Subject: [PATCH] conan2 support --- README.md | 91 ++-- tasks/JFrogConanV2/conanBuild.js | 246 +++++++++++ tasks/JFrogConanV2/conanUtils.d.ts | 6 + tasks/JFrogConanV2/conanUtils.js | 409 ++++++++++++++++++ tasks/JFrogConanV2/icon.png | Bin 0 -> 1536 bytes tasks/JFrogConanV2/package.json | 17 + tasks/JFrogConanV2/task.json | 212 +++++++++ tests/resources/conanTaskV2/conanAddRemote.js | 26 ++ .../conanTaskV2/conanAddRemoteWithPurge.js | 27 ++ .../conanTaskV2/conanConfigInstall.js | 25 ++ tests/resources/conanTaskV2/conanCreate.js | 25 ++ .../conanTaskV2/conanCustomCommand.js | 24 + .../conanCustomCommandWithBuildInfo.js | 24 + .../conanCustomCommandWithWorkingDir.js | 24 + .../conanTaskV2/conanCustomInvalidCommand.js | 22 + tests/resources/conanTaskV2/conanInstall.js | 26 ++ tests/resources/conanTaskV2/conanUpload.js | 25 ++ .../conanTaskV2/conanUploadInRelease.js | 24 + .../files/conan-config/conan-config.zip | Bin 0 -> 1169 bytes .../files/conan-install/.gitignore | 1 + .../files/conan-install/CMakeLists.txt | 10 + .../files/conan-install/conanfile.txt | 5 + .../files/conan-install/src/main.cpp | 10 + .../conanTaskV2/files/conan-min/conanfile.py | 38 ++ .../files/conan-min/src/CMakeLists.txt | 7 + .../conanTaskV2/files/conan-min/src/hello.cpp | 10 + .../conanTaskV2/files/conan-min/src/hello.h | 9 + .../resources/conanTaskV2/publishBuildInfo.js | 13 + tests/testUtils.ts | 1 + tests/tests.ts | 114 +++++ vss-extension.json | 13 + 31 files changed, 1453 insertions(+), 31 deletions(-) create mode 100644 tasks/JFrogConanV2/conanBuild.js create mode 100644 tasks/JFrogConanV2/conanUtils.d.ts create mode 100644 tasks/JFrogConanV2/conanUtils.js create mode 100644 tasks/JFrogConanV2/icon.png create mode 100644 tasks/JFrogConanV2/package.json create mode 100644 tasks/JFrogConanV2/task.json create mode 100644 tests/resources/conanTaskV2/conanAddRemote.js create mode 100644 tests/resources/conanTaskV2/conanAddRemoteWithPurge.js create mode 100644 tests/resources/conanTaskV2/conanConfigInstall.js create mode 100644 tests/resources/conanTaskV2/conanCreate.js create mode 100644 tests/resources/conanTaskV2/conanCustomCommand.js create mode 100644 tests/resources/conanTaskV2/conanCustomCommandWithBuildInfo.js create mode 100644 tests/resources/conanTaskV2/conanCustomCommandWithWorkingDir.js create mode 100644 tests/resources/conanTaskV2/conanCustomInvalidCommand.js create mode 100644 tests/resources/conanTaskV2/conanInstall.js create mode 100644 tests/resources/conanTaskV2/conanUpload.js create mode 100644 tests/resources/conanTaskV2/conanUploadInRelease.js create mode 100644 tests/resources/conanTaskV2/files/conan-config/conan-config.zip create mode 100644 tests/resources/conanTaskV2/files/conan-install/.gitignore create mode 100644 tests/resources/conanTaskV2/files/conan-install/CMakeLists.txt create mode 100644 tests/resources/conanTaskV2/files/conan-install/conanfile.txt create mode 100644 tests/resources/conanTaskV2/files/conan-install/src/main.cpp create mode 100644 tests/resources/conanTaskV2/files/conan-min/conanfile.py create mode 100644 tests/resources/conanTaskV2/files/conan-min/src/CMakeLists.txt create mode 100644 tests/resources/conanTaskV2/files/conan-min/src/hello.cpp create mode 100644 tests/resources/conanTaskV2/files/conan-min/src/hello.h create mode 100644 tests/resources/conanTaskV2/publishBuildInfo.js diff --git a/README.md b/README.md index 66d3c48d..1c42d600 100644 --- a/README.md +++ b/README.md @@ -30,37 +30,60 @@ The *[JFrog Extension](https://marketplace.visualstudio.com/items?itemName=JFrog ## Table of contents -- [Table of contents](#Table-of-contents) - - [Overview](#Overview) - - [Download and Installation](#Download-and-Installation) - - [Installing the Extension](#Installing-the-Extension) - - [Installing the Build Agent](#Installing-the-Build-Agent) - - [Configuring the Service Connections](#Configuring-the-Service-Connections) - - [Executing JFrog CLI Commands](#Executing-JFrog-CLI-Commands) - - [Build tools Tasks](#build-tools-tasks) - - [JFrog Maven](#JFrog-Maven-Task) - - [JFrog Gradle](#JFrog-Gradle-Task) - - [JFrog Npm](#JFrog-Npm-Task) - - [JFrog Nuget](#JFrog-Nuget-and-NET-Core-Task) - - [JFrog .NET Core](#JFrog-Nuget-and-NET-Core-Task) - - [JFrog Pip](#JFrog-Pip-Task) - - [JFrog Conan](#JFrog-Conan-Task) - - [JFrog Go](#JFrog-Go-Task) - - [Build Tasks](#Build-tasks) - - [JFrog Collect Build Issues](#JFrog-Collect-Build-Issues) - - [JFrog Publish Build Info](#JFrog-Publish-Build-Info) - - [JFrog Build Promotion](#JFrog-Build-Promotion) - - [Discarding Published Builds](#Discarding-Published-Builds-from-Artifactory) - - [Managing Generic Artifacts](#Managing-Generic-Artifacts) - - [JFrog Xray](#JFrog-Xray-tasks) - - [Audit project's dependencies for Security Vulnerabilities](#Audit-projects-dependencies-for-Security-Vulnerabilities) - - [Scanning Published Builds for Security Vulnerabilities with JFrog Xray](#Scanning-Published-Builds-for-Security-Vulnerabilities) - - [JFrog Docker Tasks](#JFrog-Docker-tasks) - - [Pushing and Pulling Docker Images to and from Artifactory](#Pushing-and-Pulling-Docker-Images-to-and-from-Artifactory) - - [Scanning Local Docker Images with JFrog Xray](#Scanning-Local-Docker-Images-with-JFrog-Xray) - - [JFrog Distribution](#Managing-and-Distributing-Release-Bundles) - - [JFrog Distribution Task](#JFrog-Distribution-V2-Task) - - [Contributions](#Contribution) +- [JFrog Azure DevOps Extension](#jfrog-azure-devops-extension) +- [Overview](#overview) + - [Table of contents](#table-of-contents) + - [Download and Installation](#download-and-installation) + - [Installing the Extension](#installing-the-extension) + - [Installing the Build Agent](#installing-the-build-agent) + - [Automatic Installation](#automatic-installation) + - [Custom tools Installation](#custom-tools-installation) + - [Manual Installation](#manual-installation) + - [Installing JFrog CLI](#installing-jfrog-cli) + - [Installing the Maven Extractor](#installing-the-maven-extractor) + - [Installing the Gradle Extractor](#installing-the-gradle-extractor) + - [Installing Conan](#installing-conan) + - [Using TFS 2015](#using-tfs-2015) + - [Configuring the Service Connections](#configuring-the-service-connections) + - [Executing JFrog CLI Commands](#executing-jfrog-cli-commands) + - [JFrog CLI V2 Task](#jfrog-cli-v2-task) + - [Managing Generic Artifacts](#managing-generic-artifacts) + - [Generic artifacts handling](#generic-artifacts-handling) + - [Downloading generic build dependencies from Artifactory](#downloading-generic-build-dependencies-from-artifactory) + - [Uploading generic build artifacts to Artifactory](#uploading-generic-build-artifacts-to-artifactory) + - [Setting / Deleting properties on files in Artifactory](#setting--deleting-properties-on-files-in-artifactory) + - [Moving / Copying / Deleting artifacts in Artifactory](#moving--copying--deleting-artifacts-in-artifactory) + - [Build tools Tasks](#build-tools-tasks) + - [JFrog Maven Task](#jfrog-maven-task) + - [JFrog Gradle Task](#jfrog-gradle-task) + - [JFrog Npm Task](#jfrog-npm-task) + - [JFrog Nuget and .NET Core Task](#jfrog-nuget-and-net-core-task) + - [JFrog Pip Task](#jfrog-pip-task) + - [JFrog Conan Task](#jfrog-conan-task) + - [JFrog Go Task](#jfrog-go-task) + - [Build Tasks](#build-tasks) + - [JFrog Collect Build Issues](#jfrog-collect-build-issues) + - [Configuration properties](#configuration-properties) + - [JFrog Publish Build Info](#jfrog-publish-build-info) + - [JFrog Build Promotion](#jfrog-build-promotion) + - [Using Build Promotion in a Release](#using-build-promotion-in-a-release) + - [Discarding Published Builds from Artifactory](#discarding-published-builds-from-artifactory) + - [JFrog Xray tasks](#jfrog-xray-tasks) + - [Audit project's dependencies for Security Vulnerabilities](#audit-projects-dependencies-for-security-vulnerabilities) + - [Scanning Published Builds for Security Vulnerabilities](#scanning-published-builds-for-security-vulnerabilities) + - [JFrog Docker tasks](#jfrog-docker-tasks) + - [Pushing and Pulling Docker Images to and from Artifactory](#pushing-and-pulling-docker-images-to-and-from-artifactory) + - [Scanning Local Docker Images with JFrog Xray](#scanning-local-docker-images-with-jfrog-xray) + - [Using Published Artifacts in a Release](#using-published-artifacts-in-a-release) + - [Using JFrog Generic Artifacts task](#using-jfrog-generic-artifacts-task) + - [Using Azure Artifact source](#using-azure-artifact-source) + - [Managing and Distributing Release Bundles](#managing-and-distributing-release-bundles) + - [JFrog Distribution V2 Task](#jfrog-distribution-v2-task) + - [Contribution](#contribution) + - [Building](#building) + - [Testing](#testing) + - [Skipping Tests](#skipping-tests) + - [Reporting issues](#reporting-issues) ## Download and Installation @@ -661,6 +684,12 @@ The full documentation of Conan is available at the [conan website](https://docs pathOrReference: './' buildName: '$(Build.DefinitionName)' buildNumber: '$(Build.BuildNumber)' +- task: JFrogConanV2@2 + inputs: + conanCommand: 'Install' + pathOrReference: './' + buildName: '$(Build.DefinitionName)' + buildNumber: '$(Build.BuildNumber)' ``` For more information about Conan repositories, diff --git a/tasks/JFrogConanV2/conanBuild.js b/tasks/JFrogConanV2/conanBuild.js new file mode 100644 index 00000000..87f1a3b6 --- /dev/null +++ b/tasks/JFrogConanV2/conanBuild.js @@ -0,0 +1,246 @@ +const conanutils = require('./conanUtils.js'); +const tl = require('azure-pipelines-task-lib/task'); +const utils = require('@jfrog/tasks-utils/utils.js'); + +function run() { + let conanCommand = tl.getInput('conanCommand', true); + + // Handle different conan commands + switch (conanCommand) { + case 'Config Install': + handleConfigInstallCommand(); + break; + case 'Add Remote': + handleAddRemoteCommand(); + break; + case 'Create': + handleCreateCommand(); + break; + case 'Install': + handleInstallCommand(); + break; + case 'Upload': + handleUploadCommand(); + break; + case 'Custom': + handleCustomCommand(); + break; + default: + tl.setResult(tl.TaskResult.Failed, 'Conan Command not supported: ' + conanCommand); + } +} + +/** + * Handle Conan Config Install Command + */ +function handleConfigInstallCommand() { + let configSourceType = tl.getInput('configSourceType', true); + let configInstallItem = tl.getInput('configInstallItem', true); + let extraArguments = tl.getInput('extraArguments', false); + + let conanArguments = ['config', 'install']; + conanArguments = addExtraArguments(conanArguments, extraArguments); + conanArguments.push('--type'); + conanArguments.push(configSourceType); + conanArguments.push(configInstallItem); + + conanutils + .executeConanTask(conanArguments) + .then(() => { + setTaskResult(true); + }) + .catch((err) => { + console.error('Failed to execute Conan Task: ' + err.message); + setTaskResult(false); + }); +} + +/** + * Handle Conan Add Remote Command + */ +function handleAddRemoteCommand() { + let remoteName = tl.getInput('remoteName', true); + let artifactoryService = tl.getInput('artifactoryConnection', true); + let artifactoryUrl = tl.getEndpointUrl(artifactoryService, false); + let artifactoryUser = tl.getEndpointAuthorizationParameter(artifactoryService, 'username', true); + let artifactoryPassword = tl.getEndpointAuthorizationParameter(artifactoryService, 'password', true); + let artifactoryAccessToken = tl.getEndpointAuthorizationParameter(artifactoryService, 'apitoken', true); + let conanRepo = tl.getInput('conanRepo', true); + let purgeExistingRemotes = tl.getBoolInput('purgeExistingRemotes', true); + + if (artifactoryAccessToken) { + // Access token is not supported. + console.error( + 'Access Token is not supported for authentication with Artifactory, please configure Artifactory service connection' + + ' to work with basic authentication.' + ); + setTaskResult(false); + return; + } + + let conanArguments = [ + 'remote', + 'add', + remoteName, + utils.stripTrailingSlash(artifactoryUrl) + '/api/conan/' + conanRepo, + '--index', + '0', + '--force', + ]; + + // Purge existing + if (purgeExistingRemotes) { + try { + conanutils.purgeConanRemotes(); + } catch (err) { + console.error('Failed to purge Conan Remotes: ' + err.message); + setTaskResult(false); + return; + } + } + + // Add remote repo configuration + conanutils + .executeConanTask(conanArguments) + .then(() => { + // Add user credentials + conanArguments = ['remote', 'login', '--password', artifactoryPassword, remoteName, artifactoryUser]; + conanutils.executeConanTask(conanArguments).then(() => { + setTaskResult(true); + }); + }) + .catch((err) => { + console.error('Failed to execute Conan Task: ' + err.message); + setTaskResult(false); + }); +} + +/** + * Handle Conan Create Command + */ +function handleCreateCommand() { + let createPath = tl.getPathInput('createPath', true, true); + let extraArguments = tl.getInput('extraArguments', false); + + let conanArguments = ['create']; + conanArguments = addExtraArguments(conanArguments, extraArguments); + conanArguments.push(createPath); + + conanutils + .executeConanTask(conanArguments) + .then(() => { + setTaskResult(true); + }) + .catch((err) => { + console.error('Failed to execute Conan Task: ' + err.message); + setTaskResult(false); + }); +} + +/** + * Handle Conan Install Command + */ +function handleInstallCommand() { + let pathOrReference = tl.getInput('pathOrReference', true); + let extraArguments = tl.getInput('extraArguments', false); + + let conanArguments = ['install']; + conanArguments = addExtraArguments(conanArguments, extraArguments); + conanArguments.push(pathOrReference); + + conanutils + .executeConanTask(conanArguments) + .then(() => { + setTaskResult(true); + }) + .catch((err) => { + console.error('Failed to execute Conan Task: ' + err.message); + setTaskResult(false); + }); +} + +/** + * Handle Conan Upload Command + */ +function handleUploadCommand() { + let patternOrReference = tl.getInput('patternOrReference', true); + let extraArguments = tl.getInput('extraArguments', false); + + let conanArguments = ['upload']; + conanArguments = addExtraArguments(conanArguments, extraArguments); + // Enforce --confirm option since the command runs in a non-interactive mode + enforceArgumentOrOption(conanArguments, '-c', '--confirm'); + conanArguments.push(patternOrReference); + + conanutils + .executeConanTask(conanArguments) + .then(() => { + setTaskResult(true); + }) + .catch((err) => { + console.error('Failed to execute Conan Task: ' + err.message); + setTaskResult(false); + }); +} + +/** + * Handle Conan Custom Command + */ +function handleCustomCommand() { + let customArguments = tl.getInput('customArguments', true); + let conanArguments = customArguments.split(' '); + + conanutils + .executeConanTask(conanArguments) + .then(() => { + setTaskResult(true); + }) + .catch((err) => { + console.error('Failed to execute Conan Task: ' + err.message); + setTaskResult(false); + }); +} + +/** + * Add extra arguments to list of options and arguments + * + * @param conanArguments (Array) - Collection of options and arguments + * @param extraArguments (String) - String containing the input of extra options and arguments + */ +function addExtraArguments(conanArguments, extraArguments) { + if (extraArguments && extraArguments.trim().length > 0) { + let extraArgumentsArray = extraArguments.split(' '); + return conanArguments.concat(extraArgumentsArray); + } + return conanArguments; +} + +/** + * Enforce the presence of an option or argument in the list of arguments + * + * @param conanArguments (Array) - Collection of options and arguments + * @param shortVersion (string) - Short version of option or argument + * @param longVersion (string) - Long version of option or argument + */ +function enforceArgumentOrOption(conanArguments, shortVersion, longVersion) { + if (conanArguments.indexOf(shortVersion) < 0 && conanArguments.indexOf(longVersion) < 0) { + conanArguments.push(longVersion); + } +} + +/** + * Set Task Result + * + * @param taskSuccessful (Boolean) - Flag with task result. + * True: Task was Successful. + * False: Task failed. + */ +function setTaskResult(taskSuccessful) { + if (taskSuccessful) { + tl.setResult(tl.TaskResult.Succeeded, 'Conan Task finished.'); + } else { + tl.setResult(tl.TaskResult.Failed, 'Conan Task failed.'); + } +} + +run(); diff --git a/tasks/JFrogConanV2/conanUtils.d.ts b/tasks/JFrogConanV2/conanUtils.d.ts new file mode 100644 index 00000000..5f828208 --- /dev/null +++ b/tasks/JFrogConanV2/conanUtils.d.ts @@ -0,0 +1,6 @@ +// conanUtils.d.ts +// eslint-disable-next-line @typescript-eslint/no-namespace +declare namespace conan_utils { + function getCliPartialsBuildDir(buildName: string, buildNumber: string): string; +} +export = conan_utils; diff --git a/tasks/JFrogConanV2/conanUtils.js b/tasks/JFrogConanV2/conanUtils.js new file mode 100644 index 00000000..c2f8e043 --- /dev/null +++ b/tasks/JFrogConanV2/conanUtils.js @@ -0,0 +1,409 @@ +const tl = require('azure-pipelines-task-lib/task'); +const uuid = require('uuid/v1'); +const os = require('os'); +const fs = require('fs-extra'); +const path = require('path'); +const crypto = require('crypto'); + +// Helper Constants +const CONAN_ARTIFACTS_PROPERTIES_BUILD_NAME = 'artifact_property_build.name'; +const CONAN_ARTIFACTS_PROPERTIES_BUILD_NUMBER = 'artifact_property_build.number'; +const CONAN_ARTIFACTS_PROPERTIES_BUILD_TIMESTAMP = 'artifact_property_build.timestamp'; +const BUILD_INFO_BUILD_NAME = 'name'; +const BUILD_INFO_BUILD_NUMBER = 'number'; +const BUILD_INFO_BUILD_STARTED = 'started'; +const BUILD_INFO_FILE_NAME = 'generatedBuildInfo'; +const BUILD_TEMP_PATH = 'jfrog/builds'; + +/** + * Execute Artifactory Conan Task + * @param commandArgs (Array) - Conan command arguments + */ +function executeConanTask(commandArgs) { + return new Promise((resolve, reject) => { + let workingDir = tl.getPathInput('workingDirectory', false, false); + let conanUserHome = tl.getInput('conanUserHome', false); + let collectBuildInfo = tl.getBoolInput('collectBuildInfo', false); + + let conanTaskId = generateConanTaskUUId(); + tl.debug('Conan Task Id: ' + conanTaskId); + let buildTimestamp = Date.now(); + + let conanPath = null; + try { + /* + * Get Conan tool Path. This will force the conan task to fail fast if + * conan tool is not available in the PATH + */ + conanPath = tl.which('conan', true); + console.log('Running Conan build tool from: ' + conanPath); + } catch (err) { + reject(new Error('Failed to locate Conan executable path: ' + err)); + } + + /* + * Set Conan Environment Variable + * Conan User Home is set as a variable in the phase scope, so it will be + * available to every task running after this one + */ + if (!conanUserHome) { + conanUserHome = getDefaultConanUserHome(); + } + console.log('Conan User Home: ' + conanUserHome); + tl.setVariable('CONAN_USER_HOME', conanUserHome); + + // Prepare Conan to generate build info + if (collectBuildInfo) { + let buildName = tl.getInput('buildName', true); + let buildNumber = tl.getInput('buildNumber', true); + try { + initCliPartialsBuildDir(buildName, buildNumber); + setConanTraceFileLocation(conanUserHome, conanTaskId); + setArtifactsBuildInfoProperties(conanUserHome, buildName, buildNumber, buildTimestamp); + } catch (err) { + reject(new Error('Failed to setup Build Info collection: ' + err.message)); + } + } + + // Run conan command and set task result + executeConanCommand(conanPath, commandArgs, workingDir) + .then((exitCode) => { + if (exitCode !== 0) { + reject(new Error('Conan command returned bad exit code: ' + exitCode)); + } + }) + .then(() => { + // Generate build info if requested + if (collectBuildInfo) { + generateBuildInfo(conanUserHome, conanTaskId).then((exitCode) => { + if (exitCode !== 0) { + reject(new Error('Failed to generate build info with bad exit code: ' + exitCode)); + } + try { + completeBuildInfo(conanUserHome, conanTaskId); + } catch (err) { + reject(new Error('Failed to make Build Info available: ' + err)); + } + resolve(); + }); + } else { + resolve(); + } + }); + }); +} + +/** + * Generate a unique ID to be used by this Conan task + */ +function generateConanTaskUUId() { + return uuid(); +} + +/** + * Get Default Conan User Home Folder. + * + * If process is a build it will be: + * $(Agent.WorkFolder)/$(System.DefinitionId)/$(Build.BuildId) + * + * If the process is a release it will be: + * $(Agent.WorkFolder)/$(Build.DefinitionId)/$(Build.BuildId) + */ +function getDefaultConanUserHome() { + let hostType = tl.getVariable('System.HostType'); + let workFolder = tl.getVariable('Agent.WorkFolder'); + let buildNumber = tl.getVariable('Build.BuildNumber'); + + // Get Build Id during build process + let buildId = tl.getVariable('System.DefinitionId'); + if (hostType === 'release') { + // Get Build Id during release process + buildId = tl.getVariable('Build.DefinitionId'); + } + + return path.join(workFolder, buildId, buildNumber); +} + +/** + * Create Conan artifacts.properties file with build info information. + * The properties in this file will be attached to all artifacts pushed to + * Artifactory by Conan. + * + * If the file is already present with content related to the specific buildName and + * buildNumber arguments, this file generation is skipped and the current file + * will be used. + * + * If the file is present with content related to a different buildName and buildNumber + * it will be overwritten with the new content. + * + * @param conanUserHome (string) - Conan User Home location + * @param buildName (string) - Build name used to generate buildInfo + * @param buildNumber (string) - Build number used to generate buildInfo + * @param buildTimestamp (string) - Timestamp attached to buildInfo started date + */ +function setArtifactsBuildInfoProperties(conanUserHome, buildName, buildNumber, buildTimestamp) { + let conanArtifactsPropertiesPath = getConanArtifactsPropertiesLocation(conanUserHome); + + // Check if existing content is related to current buildInfo information + if (fs.existsSync(conanArtifactsPropertiesPath)) { + let existingContent = fs.readFileSync(conanArtifactsPropertiesPath); + let propertiesMap = convertConanPropertiesToMap(existingContent.toString()); + if (CONAN_ARTIFACTS_PROPERTIES_BUILD_NAME in propertiesMap && CONAN_ARTIFACTS_PROPERTIES_BUILD_NUMBER in propertiesMap) { + if ( + buildName === propertiesMap[CONAN_ARTIFACTS_PROPERTIES_BUILD_NAME] && + buildNumber === propertiesMap[CONAN_ARTIFACTS_PROPERTIES_BUILD_NUMBER] + ) { + tl.debug('Conan artifacts.properties already set for this build at ' + conanArtifactsPropertiesPath); + return; + } else { + throw new Error('Conan artifacts.properties file exists at ' + conanArtifactsPropertiesPath + ' with different build info values.'); + } + } + } + + // Generate file + let content = + CONAN_ARTIFACTS_PROPERTIES_BUILD_NAME + + '=' + + buildName + + os.EOL + + CONAN_ARTIFACTS_PROPERTIES_BUILD_NUMBER + + '=' + + buildNumber + + os.EOL + + CONAN_ARTIFACTS_PROPERTIES_BUILD_TIMESTAMP + + '=' + + buildTimestamp + + os.EOL; + fs.outputFileSync(conanArtifactsPropertiesPath, content); + tl.debug('Conan artifacts.properties file created at ' + conanArtifactsPropertiesPath); +} + +/** + * Execute Conan Command + * @param conanPath (string) - Path to Conan Tool + * @param commandArgs (Array) - Conan command arguments list + * @param workingDir (string) - Working directory location. If set, command will + * be executed at this location + */ +function executeConanCommand(conanPath, commandArgs, workingDir) { + cleanupConanArguments(commandArgs); + + // Create command + let conan = tl.tool(conanPath).arg(commandArgs); + let options = { + failOnStdErr: false, + errStream: process.stdout, + outStream: process.stdout, + ignoreReturnCode: true, + }; + + // Set working dir if present + if (workingDir) { + // Make sure custom working directory exists + fs.mkdirsSync(workingDir); + } else { + // Use default working dir + workingDir = tl.getVariable('System.DefaultWorkingDirectory'); + } + + console.log('Running Conan command at: ' + workingDir); + options['cwd'] = workingDir; + + // Run command and wait for exitCode + return conan.exec(options); +} + +/** + * Cleanup empty arguments and remove conan keyword from the beginning of arguments + * list if present + * @param commandArgs (Array) - The collection of arguments + */ +function cleanupConanArguments(commandArgs) { + if (commandArgs[0] === 'conan') { + commandArgs.splice(0, 1); + } + for (let i = 0; i < commandArgs.length; i++) { + commandArgs[i] = commandArgs[i].trim(); + if (commandArgs[i] === '') { + commandArgs.splice(i, 1); + // Process the current index again, since one element has been removed + i--; + } + } +} + +/** + * Generate buildInfo Json file. Run conan_build_info tool to convert the conan + * trace file to a buildinfo json file + * @param conanUserHome (string) - Conan User Home location + * @param conanTaskId (string) - Conan Task Id + */ +function generateBuildInfo(conanUserHome, conanTaskId) { + let conanBuildInfoPath = tl.which('conan_build_info', true); + let buildInfoFilePath = getBuildInfoFileLocation(conanUserHome, conanTaskId); + let conanTraceFilePath = process.env.CONAN_TRACE_FILE; + + console.log('Generating Build Info at ' + buildInfoFilePath); + + let conanBuildInfoArgs = [conanTraceFilePath, '--output', buildInfoFilePath]; + + let options = { + failOnStdErr: false, + errStream: process.stdout, + outStream: process.stdout, + ignoreReturnCode: true, + }; + + let conanBuildInfo = tl.tool(conanBuildInfoPath).arg(conanBuildInfoArgs); + + // Run command and wait for exitCode + return conanBuildInfo.exec(options); +} + +/** + * Complete build info json file with information from artifacts.properties + * @param conanUserHome (string) - Conan User Home location + * @param conanTaskId (string) - Conan Task Id + */ +function completeBuildInfo(conanUserHome, conanTaskId) { + let conanArtifactsPropertiesPath = getConanArtifactsPropertiesLocation(conanUserHome); + let buildInfoFilePath = getBuildInfoFileLocation(conanUserHome, conanTaskId); + + // Read artifacts.properties file + let content = fs.readFileSync(conanArtifactsPropertiesPath); + let propertiesMap = convertConanPropertiesToMap(content.toString()); + + let buildName = propertiesMap[CONAN_ARTIFACTS_PROPERTIES_BUILD_NAME]; + let buildNumber = propertiesMap[CONAN_ARTIFACTS_PROPERTIES_BUILD_NUMBER]; + + // Read build info json file + let buildInfoJson = fs.readJsonSync(buildInfoFilePath); + buildInfoJson[BUILD_INFO_BUILD_NAME] = buildName; + buildInfoJson[BUILD_INFO_BUILD_NUMBER] = buildNumber; + let buildTimestamp = new Date(parseInt(propertiesMap[CONAN_ARTIFACTS_PROPERTIES_BUILD_TIMESTAMP], 10)); + buildInfoJson[BUILD_INFO_BUILD_STARTED] = buildTimestamp.toISOString(); + + // Delete the previously created BuildInfo + fs.unlinkSync(buildInfoFilePath); + buildInfoFilePath = path.join(getCliPartialsBuildDir(buildName, buildNumber), BUILD_INFO_FILE_NAME + conanTaskId); + + // Write build info json file + fs.writeJsonSync(buildInfoFilePath, buildInfoJson); + tl.debug('Build Info created at ' + buildInfoFilePath); +} + +/** + * Set enviroment variable with the location for the Conan Trace File + * This file will later be used to generate the buildInfo json file if requested + * by the user + * @param conanUserHome (string) - Conan User Home location + * @param conanTaskId (string) - Conan Task Id + */ +function setConanTraceFileLocation(conanUserHome, conanTaskId) { + process.env.CONAN_TRACE_FILE = path.join(conanUserHome, '.conan', 'conan_trace_' + conanTaskId + '.log'); +} + +/** + * Get build info file location + * @param conanUserHome (string) - Conan User Home location + * @param conanTaskId (string) - Conan Task Id + */ +function getBuildInfoFileLocation(conanUserHome, conanTaskId) { + return path.join(conanUserHome, '.conan', 'build_info_' + conanTaskId + '.json'); +} + +/** + * Get Conan artifacts.properties file location + * @param conanUserHome (string) - Conan User Home location + */ +function getConanArtifactsPropertiesLocation(conanUserHome) { + return path.join(conanUserHome, '.conan', 'artifacts.properties'); +} + +/** + * Convert content in format key=value to a Map + * @param propertiesContent (String) - Properties content + */ +function convertConanPropertiesToMap(propertiesContent) { + if (!propertiesContent) { + return {}; + } + let map = {}; + let lines = propertiesContent.split(os.EOL); + for (let i = 0; i < lines.length; i++) { + let parts = lines[i].split('='); + let key = parts[0]; + parts.splice(0, 1); + map[key] = parts.join('='); + } + return map; +} + +/** + * Purge existing Conan remote repositories + */ +function purgeConanRemotes() { + let conanUserHome = tl.getInput('conanUserHome', false); + + try { + /* + * Get Conan tool Path. This will force the conan task to fail fast if + * conan tool is not available in the PATH + */ + let conanPath = tl.which('conan', true); + console.log('Running Conan build tool from: ' + conanPath); + } catch (err) { + throw new Error('Failed to locate Conan executable path: ' + err.message); + } + + /* + * Set Conan Environment Variable + * Conan User Home is set as a variable in the phase scope, so it will be + * available to every task running after this one + */ + if (!conanUserHome) { + conanUserHome = getDefaultConanUserHome(); + } + console.log('Conan User Home: ' + conanUserHome); + tl.setVariable('CONAN_USER_HOME', conanUserHome); + + // Make sure Conan User Home exists + let conanFolder = path.join(conanUserHome, '.conan'); + fs.mkdirsSync(conanFolder); + + // Empty registry.txt file to remove all existing remotes + let registryFile = path.join(conanFolder, 'registry.txt'); + console.log('Purging Conan remotes by removing content of ' + registryFile); + try { + fs.writeFileSync(registryFile, ''); + } catch (err) { + throw new Error('Failed to remove registry.txt file content: ' + err.message); + } +} + +/** + * Creates the path of for partials build info and initializing the details file with Timestamp. + * @param buildName (string) - The build name + * @param buildNumber (string) - The build number + */ +function initCliPartialsBuildDir(buildName, buildNumber) { + let partialsBuildDir = path.join(getCliPartialsBuildDir(buildName, buildNumber), 'partials'); + if (!fs.pathExistsSync(partialsBuildDir)) { + fs.ensureDirSync(partialsBuildDir); + } + fs.writeJsonSync(path.join(partialsBuildDir, 'details'), { Timestamp: new Date().toISOString() }); + tl.debug('Created partial details at: ' + path.join(partialsBuildDir, 'details')); +} + +function getCliPartialsBuildDir(buildName, buildNumber) { + const buildId = buildName + '_' + buildNumber + '_' + ''; + const hexId = crypto.createHash('sha256').update(buildId).digest('hex'); + return path.join(os.tmpdir(), BUILD_TEMP_PATH, hexId); +} + +module.exports = { + executeConanTask: executeConanTask, + getCliPartialsBuildDir: getCliPartialsBuildDir, // Exported for tests + purgeConanRemotes: purgeConanRemotes, +}; diff --git a/tasks/JFrogConanV2/icon.png b/tasks/JFrogConanV2/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..189228c50fef09f9ae376467715944ca1682a0f1 GIT binary patch literal 1536 zcmV+b2LJhqP)N<==t)*MwWf}GGOI(97_ zdwu_IX>AF_(YM~eH|EBsM3@5kjEG3bqFAd~Z9hAXU_?+QkLU?&qoc>poc_!4#~zDr zA3{}*|Kr2^9m5nNdqm{ZonJSCh5&BWN&8z-BIA#hdI{$53n zzVo@!Hx7!}TS7diYVP^x;sdBoiLl3rd}8I|i&yJhzTTizX`z5IU~Y&LMT!FPm|>{j zV`MN%E*nQxFRSVaL63dop0QIRqN^ZgXJ;K}@_{dlWA=jX5s{IVwErzMIKNoqn%}Ou zTsFq`z68CQ7%|tN3e|eZHNVN_>kU+)C*?9eoMLQ8`i8(ZwJ-&C>_+OVGp8w-~ zt|!$xI5OaU6OqBS02 zMl(0F4GU`RQ5&>geRTK4X>)F|(06ve^!2j~mBCsgT(d;-rC^=ta;AN}df!!AHQu>5DCLcSO zobHwrNF`l9nR3}VkR+Rqb=sJ6rA6KkC=`S7R?GIgH*%#S?H&7-;08>JTs96sPdY{- zZfFLUVx`5UYjt)GB`VvJx6OP@uAbn<5CW)PA0o8$`t&7i}j3Sv~< z^>$KgM7%fOUTkDA$;3#SMBH%kYMnwUVBt!gg)4Ou@wOd@`aLqq(Z(Hcsuwq}z;DOiHpHQ7g@6i}?RuvW?S#u@1IaGd2e;g1EbEzpoEdGq}HgKTG;YIPbz9Z?sOocMp5&dnJ*RU<-#+nJgchjZ*@LoU2ke= zqv%8kXO_+R&P#v$iW)g!M839Bgml~?)mI{-pS|$J zeJ4M%TRFh=%WqDB%oxK%skmc%Jl9yO|5B@mqFU?7_xJBEex!d5bQJB~G2`w}C!DV) mW6tZL4G#a{Tf65r6YGD5>aawlxb_VI0000rU5qusxkMqgE2vL^tp z^(vZHF?6j3Mfqu&IjO~PGlG!J5Mkf|!kcJjXy7y>Co`|KBEG`HEZ)o{J|#7&G#%u! z44{dTP*Yv|&T<_x5NLT{IkhR^!lTrDx!UG{rki>%oqUq&ylSeGrN5XM-`{Zfl#AtN z<4iHTgL7k7p4EuGp=WESv@T%%yvGfH4FjX>>QS}4~NtN19CkOgTe{o z$1`OdC_$`6oF9u)b5awFQ$fD;0s62%EgKyEu6+kVzI1v26XMJN2PR(+aM~UF3g}BS z&6oG3voBmevnR3rs*~o%=+uz72gmfLXPudIQ%~1VP(EN?v*fw<{8VMIFCzr&A8rX0 z0sFG@SkQv5C9J#Gy}xRha!HQkT}q}<(5?0m{@2#OZo5^b_I+V|vMq2gBgmVQw(}h8 zfnMB*;!W!+)RZKF?#-gq-29T%V!e`z5|F24fr&~Gh(U?U``mf&Gnak+&T4z=2A$B* z)YRB^>AF7~=PT9JXH!K~!(ORAf3DRcGId!IBRHVSJI40J*=8.0.1] + +[generators] +cmake_paths \ No newline at end of file diff --git a/tests/resources/conanTaskV2/files/conan-install/src/main.cpp b/tests/resources/conanTaskV2/files/conan-install/src/main.cpp new file mode 100644 index 00000000..777a54a8 --- /dev/null +++ b/tests/resources/conanTaskV2/files/conan-install/src/main.cpp @@ -0,0 +1,10 @@ +#include + +#include + +int main() { + std::string msg = fmt::format("{} + {} = {}", 1, 2, 3); + + std::cout << "Hello, World: " << msg << std::endl; + return 0; +} \ No newline at end of file diff --git a/tests/resources/conanTaskV2/files/conan-min/conanfile.py b/tests/resources/conanTaskV2/files/conan-min/conanfile.py new file mode 100644 index 00000000..eefb8f4e --- /dev/null +++ b/tests/resources/conanTaskV2/files/conan-min/conanfile.py @@ -0,0 +1,38 @@ +from conans import ConanFile, CMake, tools + + +class ConanminConan(ConanFile): + name = "Conan-min" + version = "0.0.1" + license = "" + url = "" + description = "" + settings = "os", "compiler", "build_type", "arch" + options = {"shared": [True, False]} + default_options = "shared=False" + generators = "cmake" + exports_sources = "src/*" + + def requirements(self): + self.requires("boost/[>=1.77]") + + def build(self): + cmake = CMake(self) + cmake.configure(source_folder="src") + cmake.build() + + # Explicit way: + # self.run('cmake %s/hello %s' + # % (self.source_folder, cmake.command_line)) + # self.run("cmake --build . %s" % cmake.build_config) + + def package(self): + self.copy("*.h", dst="include", src="hello") + self.copy("*hello.lib", dst="lib", keep_path=False) + self.copy("*.dll", dst="bin", keep_path=False) + self.copy("*.so", dst="lib", keep_path=False) + self.copy("*.dylib", dst="lib", keep_path=False) + self.copy("*.a", dst="lib", keep_path=False) + + def package_info(self): + self.cpp_info.libs = ["hello"] diff --git a/tests/resources/conanTaskV2/files/conan-min/src/CMakeLists.txt b/tests/resources/conanTaskV2/files/conan-min/src/CMakeLists.txt new file mode 100644 index 00000000..acf5c2be --- /dev/null +++ b/tests/resources/conanTaskV2/files/conan-min/src/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.5) + +project(MyHello CXX) + +include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) + +add_library(hello hello.cpp) diff --git a/tests/resources/conanTaskV2/files/conan-min/src/hello.cpp b/tests/resources/conanTaskV2/files/conan-min/src/hello.cpp new file mode 100644 index 00000000..235428a0 --- /dev/null +++ b/tests/resources/conanTaskV2/files/conan-min/src/hello.cpp @@ -0,0 +1,10 @@ +#include +#include "hello.h" + +void hello(){ + #ifdef NDEBUG + std::cout << "Hello World Release!" < { ); }); + describe('Conan V2 Task Tests', (): void => { + runSyncTest( + 'Conan Custom Command', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanCustomCommand'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Custom Command With Working Dir', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanCustomCommandWithWorkingDir'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Custom Invalid Command', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanCustomInvalidCommand', true); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Custom Command With Build Info', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanCustomCommandWithBuildInfo'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Add Remote', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanAddRemote'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Add Remote With Purge', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanAddRemoteWithPurge'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Create And Upload', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanAddRemote'); + mockTask(testDir, 'conanCreate'); + mockTask(testDir, 'conanUpload'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Create And Upload in Release', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanAddRemote'); + mockTask(testDir, 'conanCreate'); + mockTask(testDir, 'conanUploadInRelease'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Install', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanInstall'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Add Config', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanConfigInstall'); + }, + TestUtils.isSkipTest('conan') + ); + + runSyncTest( + 'Conan Publish Build Info', + (): void => { + const testDir: string = 'conanTaskV2'; + mockTask(testDir, 'conanAddRemote'); + mockTask(testDir, 'conanCreate'); + mockTask(testDir, 'conanUpload'); + mockTask(testDir, 'publishBuildInfo'); + getAndAssertBuild('conanTaskV2', '1'); + deleteBuild('conanTaskV2'); + }, + TestUtils.isSkipTest('conan') + ); + }); + describe('Pip Tests', (): void => { runSyncTest( 'Pip Install', @@ -1211,6 +1322,9 @@ function testGetCliPartialsBuildDir(): void { testDTO.testsBuildNames.forEach((element: string): void => runBuildCommand('bce', element, testDTO.testBuildNumber)); // Assert the partials created at the generated by getCliPartialsBuildDir() path testDTO.testsBuildNames.forEach((element: string): void => assertPathExists(conanUtils.getCliPartialsBuildDir(element, testDTO.testBuildNumber))); + testDTO.testsBuildNames.forEach((element: string): void => + assertPathExists(conanUtilsV2.getCliPartialsBuildDir(element, testDTO.testBuildNumber)) + ); // Cleanup the created partials testDTO.testsBuildNames.forEach((element: string): void => runBuildCommand('bc', element, testDTO.testBuildNumber)); } diff --git a/vss-extension.json b/vss-extension.json index 6d96ef09..ea8b1906 100644 --- a/vss-extension.json +++ b/vss-extension.json @@ -722,6 +722,16 @@ "name": "tasks/JFrogConan" } }, + { + "id": "jfrog-conan-build-task", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "tasks/JFrogConanV2" + } + }, { "id": "jfrog-build-scan-task", "type": "ms.vss-distributed-task.task", @@ -871,6 +881,9 @@ { "path": "tasks/JFrogConan" }, + { + "path": "tasks/JFrogConanV2" + }, { "path": "tasks/JFrogBuildScan" },