// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using OpenTap.Cli;
using System.Threading;
using Tap.Shared;
namespace OpenTap.Package
{
///
/// CLI sub command `tap sdk create` that can create a *.TapPackage from a definition in a package.xml file.
///
[Display("create", Group: "package", Description: "Create a package based on an XML description file.")]
public class PackageCreateAction : PackageAction
{
private static readonly char[] IllegalPackageNameChars = {'"', '<', '>', '|', '\0', '\u0001', '\u0002', '\u0003', '\u0004', '\u0005', '\u0006', '\a', '\b',
'\t', '\n', '\v', '\f', '\r', '\u000e', '\u000f', '\u0010', '\u0011', '\u0012', '\u0013', '\u0014', '\u0015', '\u0016', '\u0017', '\u0018',
'\u0019', '\u001a', '\u001b', '\u001c', '\u001d', '\u001e', '\u001f', ':', '*', '?', '\\'};
///
/// The default file extension for OpenTAP packages.
///
public static string DefaultEnding = "TapPackage";
///
/// The default file name for the created OpenTAP package.
/// Not used anymore, a default file name is now generated from the package name and version.
///
public static string DefaultFileName = "Package";
///
/// Represents an unnamed command line argument which specifies the package.xml file that defines the package that should be generated.
///
[UnnamedCommandLineArgument("Package Xml File", Required = true, Description = "The path to the package xml file to create a package from.")]
public string PackageXmlFile { get; set; }
///
/// Represents the --project-directory command line argument, which specifies the directory containing the git repository used to get values for version/branch macros.
///
[CommandLineArgument("project-directory", Description = "The directory containing the git repository.\nUsed to get values for version/branch macros.")]
public string ProjectDir { get; set; }
///
/// Represents the --install command line argument. When true, this action will also install the created package.
///
[CommandLineArgument("install", Description = "Install the created package. It will not overwrite the files \nalready in the target installation (e.g., debug binaries).")]
public bool Install { get; set; } = false;
///
/// Obsolete, use Install property instead.
///
[CommandLineArgument("fake-install", Description = "Install the created package. It will not overwrite files \nalready in the target installation (e.g. debug binaries).")]
[Browsable(false)]
public bool FakeInstall { get; set; } = false;
///
/// Represents the --out command line argument which specifies the path to the output file.
///
[CommandLineArgument("out", Description = "Path to the output file.", ShortName = "o")]
public string[] OutputPaths { get; set; }
///
/// Constructs new action with default values for arguments.
///
public PackageCreateAction()
{
ProjectDir = Directory.GetCurrentDirectory();
}
///
/// Executes this action.
///
public override int Execute(CancellationToken cancellationToken)
{
if (PackageXmlFile == null)
throw new Exception("No packages definition file specified.");
return Process(OutputPaths, cancellationToken);
}
private int Process(string[] OutputPaths, CancellationToken cancellationToken)
{
try
{
PackageDef pkg = null;
if (!File.Exists(PackageXmlFile))
{
log.Error("Cannot locate XML file '{0}'", PackageXmlFile);
return (int)ExitCodes.ArgumentError;
}
if (!Directory.Exists(ProjectDir))
{
log.Error("Project directory '{0}' does not exist.", ProjectDir);
return (int)ExitCodes.ArgumentError;
}
try
{
var fullpath = Path.GetFullPath(PackageXmlFile);
pkg = PackageDefExt.FromInputXml(fullpath, ProjectDir);
if (pkg.Version == null)
{
log.Error(
$"The specified version '{pkg.RawVersion}' is not a valid semantic version. See https://semver.org");
return (int)PackageExitCodes.InvalidPackageDefinition;
}
// Check if package name has invalid characters or is not a valid path
var illegalCharacter = pkg.Name.IndexOfAny(IllegalPackageNameChars);
if (illegalCharacter >= 0)
{
log.Error("Package name cannot contain invalid file path characters: '{0}'.", pkg.Name[illegalCharacter]);
return (int)PackageExitCodes.InvalidPackageName;
}
foreach (var ch in pkg.Name.Distinct())
{
switch (ch)
{
case '/':
log.Warning($"Use of the character '/' in a package name is discouraged. " +
$"This will never be supported by the package repository, " +
$"and might lead to problems in the future.");
break;
default:
break;
}
}
// Check for invalid package metadata
const string validMetadataPattern = "^[_a-zA-Z][_a-zA-Z0-9]*";
var validMetadataRegex = new Regex(validMetadataPattern);
foreach (var metaDataKey in pkg.MetaData.Keys)
{
var match = validMetadataRegex.Match(metaDataKey);
if (match.Success == false || match.Length != metaDataKey.Length)
{
if (metaDataKey.Length > 0)
log.Error($"Found invalid character '{metaDataKey[match.Length]}' in package metadata key '{metaDataKey}' at position {match.Length + 1}.");
else
log.Error($"Metadata key cannot be empty.");
return (int)PackageExitCodes.InvalidPackageDefinition;
}
}
}
catch (AggregateException aex)
{
foreach (var inner in aex.InnerExceptions)
{
if (inner is FileNotFoundException ex)
{
log.Error("File not found: '{0}'", ex.FileName);
}
else
{
log.Error(inner.ToString());
}
}
log.Error("Caught errors while loading package definition.");
return (int)PackageExitCodes.InvalidPackageDefinition;
}
var tmpFile = PathUtils.GetTempFileName(".opentap_package_tmp.zip"); ;
// If user omitted the Version XML attribute or put Version="", lets inform.
if(string.IsNullOrEmpty(pkg.RawVersion))
log.Warning($"Package version is {pkg.Version} due to blank or missing 'Version' XML attribute in 'Package' element");
using (var str = new FileStream(tmpFile, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.DeleteOnClose))
{
pkg.CreatePackage(str);
if (OutputPaths == null || OutputPaths.Length == 0)
OutputPaths = new string[1] {""};
foreach (var outputPath in OutputPaths)
{
var path = outputPath;
if (String.IsNullOrEmpty(path) || path.EndsWith(Path.DirectorySeparatorChar.ToString()) || path.EndsWith(Path.AltDirectorySeparatorChar.ToString()))
{
// Package names support path separators now -- avoid writing the newly created package into a nested folder and
// replace the path separators with dots instead
var name = pkg.Name.Replace(Path.DirectorySeparatorChar, '.').Replace(Path.AltDirectorySeparatorChar, '.');
path = Path.Combine(path, GetRealFilePathFromName(name, pkg.Version.ToString(), DefaultEnding));
}
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
ProgramHelper.FileCopy(tmpFile, path);
log.Info("OpenTAP plugin package '{0}' containing '{1}' successfully created.", path, pkg.Name);
}
}
if (FakeInstall)
{
log.Warning("--fake-install argument is obsolete, use --install instead");
Install = FakeInstall;
}
if (Install)
{
var path = PackageDef.GetDefaultPackageMetadataPath(pkg, Directory.GetCurrentDirectory());
Directory.CreateDirectory(Path.GetDirectoryName(path));
using (FileStream fs = new FileStream(path, FileMode.Create))
{
pkg.SaveTo(fs);
}
log.Info($"Installed '{pkg.Name}' ({Path.GetFullPath(path)})");
}
}
catch (ArgumentException ex)
{
log.Error("Caught exception: {0}", ex.Message);
return (int)PackageExitCodes.PackageCreateError;
}
catch (InvalidDataException ex)
{
log.Error("Caught invalid data exception: {0}", ex.Message);
return (int)PackageExitCodes.InvalidPackageDefinition;
}
return (int)ExitCodes.Success;
}
///
/// Obsolete. Do not use.
///
[Obsolete("Will be removed in OpenTAP 10.")]
public static string GetRealFilePath(string path, string version, string extension)
{
return GetRealFilePathFromName(Path.GetFileNameWithoutExtension(path), version, extension);
}
internal static string GetRealFilePathFromName(string name, string version, string extension)
{
string toInsert;
if (String.IsNullOrEmpty(version))
{
toInsert = "."; //just separate with '.'
}
else
{
toInsert = "." + version + "."; // insert version between ext and path
}
return name + toInsert + extension;
}
}
}