Add VersionAssemblyName to package manifest (#14046)

* Add VersionAssemblyName to package manifest

* Fix/improve nullability

* Ensure package version from manifest is set when package migration exists

* Set versionAssemblyName in umbracopackage template

* Use Assembly.Load instead of ITypeFinder

* Use AssemblyLoadContext to get asesmbly by name

* Get version from package migration assembly

* Hide unknown package version

* Set versionAssemblyName in umbracopackage-rcl template
This commit is contained in:
Ronald Barendse
2023-05-30 15:47:36 +02:00
committed by GitHub
parent fcf477191f
commit 84a0cd8c09
9 changed files with 169 additions and 44 deletions

View File

@@ -1,7 +1,9 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Umbraco.Cms.Core.Semver;
namespace Umbraco.Extensions;
@@ -104,4 +106,35 @@ public static class AssemblyExtensions
return null;
}
/// <summary>
/// Gets the assembly informational version for the specified <paramref name="assembly" />.
/// </summary>
/// <param name="assembly">The assembly.</param>
/// <param name="version">The assembly version.</param>
/// <returns>
/// <c>true</c> if the assembly information version is retrieved; otherwise, <c>false</c>.
/// </returns>
public static bool TryGetInformationalVersion(this Assembly assembly, [NotNullWhen(true)] out string? version)
{
AssemblyInformationalVersionAttribute? assemblyInformationalVersionAttribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
if (assemblyInformationalVersionAttribute is not null &&
SemVersion.TryParse(assemblyInformationalVersionAttribute.InformationalVersion, out SemVersion? semVersion))
{
version = semVersion.ToSemanticStringWithoutBuild();
return true;
}
else
{
AssemblyName assemblyName = assembly.GetName();
if (assemblyName.Version is not null)
{
version = assemblyName.Version.ToString(3);
return true;
}
}
version = null;
return false;
}
}

View File

@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Extensions;
@@ -11,6 +12,7 @@ public static class IOHelperExtensions
/// <param name="ioHelper"></param>
/// <param name="path"></param>
/// <returns></returns>
[return: NotNullIfNotNull("path")]
public static string? ResolveRelativeOrVirtualUrl(this IIOHelper ioHelper, string? path)
{
if (string.IsNullOrWhiteSpace(path))

View File

@@ -5,7 +5,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Manifest;
/// <summary>
/// Represents the content of a package manifest.
/// Represents the content of a package manifest.
/// </summary>
[DataContract]
public class PackageManifest
@@ -13,8 +13,11 @@ public class PackageManifest
private string? _packageName;
/// <summary>
/// An optional package name. If not specified then the directory name is used.
/// Gets or sets the name of the package. If not specified, uses the directory name instead.
/// </summary>
/// <value>
/// The name of the package.
/// </value>
[DataMember(Name = "name")]
public string? PackageName
{
@@ -35,81 +38,132 @@ public class PackageManifest
set => _packageName = value;
}
/// <summary>
/// Gets or sets the package view.
/// </summary>
/// <value>
/// The package view.
/// </value>
[DataMember(Name = "packageView")]
public string? PackageView { get; set; }
/// <summary>
/// Gets the source path of the manifest.
/// Gets or sets the source path of the manifest.
/// </summary>
/// <value>
/// The source path.
/// </value>
/// <remarks>
/// <para>
/// Gets the full absolute file path of the manifest,
/// using system directory separators.
/// </para>
/// Gets the full/absolute file path of the manifest, using system directory separators.
/// </remarks>
[IgnoreDataMember]
public string Source { get; set; } = null!;
/// <summary>
/// Gets or sets the version of the package
/// Gets or sets the version of the package.
/// </summary>
/// <value>
/// The version of the package.
/// </value>
[DataMember(Name = "version")]
public string Version { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether telemetry is allowed
/// Gets or sets the assembly name to get the package version from.
/// </summary>
/// <value>
/// The assembly name to get the package version from.
/// </value>
[DataMember(Name = "versionAssemblyName")]
public string? VersionAssemblyName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether telemetry is allowed.
/// </summary>
/// <value>
/// <c>true</c> if package telemetry is allowed; otherwise, <c>false</c>.
/// </value>
[DataMember(Name = "allowPackageTelemetry")]
public bool AllowPackageTelemetry { get; set; } = true;
/// <summary>
/// Gets or sets the bundle options.
/// </summary>
/// <value>
/// The bundle options.
/// </value>
[DataMember(Name = "bundleOptions")]
public BundleOptions BundleOptions { get; set; }
/// <summary>
/// Gets or sets the scripts listed in the manifest.
/// Gets or sets the scripts listed in the manifest.
/// </summary>
/// <value>
/// The scripts.
/// </value>
[DataMember(Name = "javascript")]
public string[] Scripts { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the stylesheets listed in the manifest.
/// Gets or sets the stylesheets listed in the manifest.
/// </summary>
/// <value>
/// The stylesheets.
/// </value>
[DataMember(Name = "css")]
public string[] Stylesheets { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the property editors listed in the manifest.
/// Gets or sets the property editors listed in the manifest.
/// </summary>
/// <value>
/// The property editors.
/// </value>
[DataMember(Name = "propertyEditors")]
public IDataEditor[] PropertyEditors { get; set; } = Array.Empty<IDataEditor>();
/// <summary>
/// Gets or sets the parameter editors listed in the manifest.
/// Gets or sets the parameter editors listed in the manifest.
/// </summary>
/// <value>
/// The parameter editors.
/// </value>
[DataMember(Name = "parameterEditors")]
public IDataEditor[] ParameterEditors { get; set; } = Array.Empty<IDataEditor>();
/// <summary>
/// Gets or sets the grid editors listed in the manifest.
/// Gets or sets the grid editors listed in the manifest.
/// </summary>
/// <value>
/// The grid editors.
/// </value>
[DataMember(Name = "gridEditors")]
public GridEditor[] GridEditors { get; set; } = Array.Empty<GridEditor>();
/// <summary>
/// Gets or sets the content apps listed in the manifest.
/// Gets or sets the content apps listed in the manifest.
/// </summary>
/// <value>
/// The content apps.
/// </value>
[DataMember(Name = "contentApps")]
public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty<ManifestContentAppDefinition>();
/// <summary>
/// Gets or sets the dashboards listed in the manifest.
/// Gets or sets the dashboards listed in the manifest.
/// </summary>
/// <value>
/// The dashboards.
/// </value>
[DataMember(Name = "dashboards")]
public ManifestDashboard[] Dashboards { get; set; } = Array.Empty<ManifestDashboard>();
/// <summary>
/// Gets or sets the sections listed in the manifest.
/// Gets or sets the sections listed in the manifest.
/// </summary>
/// <value>
/// The sections.
/// </value>
[DataMember(Name = "sections")]
public ManifestSection[] Sections { get; set; } = Array.Empty<ManifestSection>();
}

View File

@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using System.Diagnostics.CodeAnalysis;
#if !NETSTANDARD
using System.Globalization;
using System.Runtime.Serialization;
@@ -195,7 +196,7 @@ namespace Umbraco.Cms.Core.Semver
/// </param>
/// <param name="strict">If set to <c>true</c> minor and patch version are required, else they default to 0.</param>
/// <returns><c>False</c> when a invalid version string is passed, otherwise <c>true</c>.</returns>
public static bool TryParse(string version, out SemVersion? semver, bool strict = false)
public static bool TryParse(string version, [NotNullWhen(true)] out SemVersion? semver, bool strict = false)
{
try
{

View File

@@ -1,3 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@@ -17,7 +20,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Manifest;
/// <summary>
/// Parses the Main.js file and replaces all tokens accordingly.
/// Parses the Main.js file and replaces all tokens accordingly.
/// </summary>
public class ManifestParser : IManifestParser
{
@@ -39,7 +42,7 @@ public class ManifestParser : IManifestParser
private string _path = null!;
/// <summary>
/// Initializes a new instance of the <see cref="ManifestParser" /> class.
/// Initializes a new instance of the <see cref="ManifestParser" /> class.
/// </summary>
public ManifestParser(
AppCaches appCaches,
@@ -163,31 +166,35 @@ public class ManifestParser : IManifestParser
/// </summary>
public PackageManifest ParseManifest(string text)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
ArgumentNullException.ThrowIfNull(text);
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(text));
}
PackageManifest? manifest = JsonConvert.DeserializeObject<PackageManifest>(
PackageManifest manifest = JsonConvert.DeserializeObject<PackageManifest>(
text,
new DataEditorConverter(_dataValueEditorFactory, _ioHelper, _localizedTextService, _shortStringHelper, _jsonSerializer),
new ValueValidatorConverter(_validators),
new DashboardAccessRuleConverter());
new DashboardAccessRuleConverter())!;
if (string.IsNullOrEmpty(manifest.Version) &&
!string.IsNullOrEmpty(manifest.VersionAssemblyName) &&
TryGetAssemblyInformationalVersion(manifest.VersionAssemblyName, out string? version))
{
manifest.Version = version;
}
// scripts and stylesheets are raw string, must process here
for (var i = 0; i < manifest!.Scripts.Length; i++)
for (var i = 0; i < manifest.Scripts.Length; i++)
{
manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i])!;
manifest.Scripts[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Scripts[i]);
}
for (var i = 0; i < manifest.Stylesheets.Length; i++)
{
manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i])!;
manifest.Stylesheets[i] = _ioHelper.ResolveRelativeOrVirtualUrl(manifest.Stylesheets[i]);
}
foreach (ManifestContentAppDefinition contentApp in manifest.ContentApps)
@@ -197,7 +204,7 @@ public class ManifestParser : IManifestParser
foreach (ManifestDashboard dashboard in manifest.Dashboards)
{
dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View)!;
dashboard.View = _ioHelper.ResolveRelativeOrVirtualUrl(dashboard.View);
}
foreach (GridEditor gridEditor in manifest.GridEditors)
@@ -217,6 +224,22 @@ public class ManifestParser : IManifestParser
return manifest;
}
private bool TryGetAssemblyInformationalVersion(string name, [NotNullWhen(true)] out string? version)
{
foreach (Assembly assembly in AssemblyLoadContext.Default.Assemblies)
{
AssemblyName assemblyName = assembly.GetName();
if (string.Equals(assemblyName.Name, name, StringComparison.OrdinalIgnoreCase) &&
assembly.TryGetInformationalVersion(out version))
{
return true;
}
}
version = null;
return false;
}
/// <summary>
/// Merges all manifests into one.
/// </summary>

View File

@@ -116,8 +116,7 @@ public class PackagingService : IPackagingService
public IEnumerable<InstalledPackage> GetAllInstalledPackages()
{
IReadOnlyDictionary<string, string?>? keyValues =
_keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix);
IReadOnlyDictionary<string, string?>? keyValues = _keyValueService.FindByKeyPrefix(Constants.Conventions.Migrations.KeyValuePrefix);
var installedPackages = new Dictionary<string, InstalledPackage>();
@@ -126,14 +125,21 @@ public class PackagingService : IPackagingService
{
if (!installedPackages.TryGetValue(plan.PackageName, out InstalledPackage? installedPackage))
{
installedPackage = new InstalledPackage { PackageName = plan.PackageName };
installedPackage = new InstalledPackage
{
PackageName = plan.PackageName,
};
if (plan.GetType().Assembly.TryGetInformationalVersion(out string? version))
{
installedPackage.Version = version;
}
installedPackages.Add(plan.PackageName, installedPackage);
}
var currentPlans = installedPackage.PackageMigrationPlans.ToList();
if (keyValues is null || keyValues.TryGetValue(
Constants.Conventions.Migrations.KeyValuePrefix + plan.Name,
out var currentState) is false)
if (keyValues is null || keyValues.TryGetValue(Constants.Conventions.Migrations.KeyValuePrefix + plan.Name, out var currentState) is false)
{
currentState = null;
}
@@ -157,14 +163,20 @@ public class PackagingService : IPackagingService
if (!installedPackages.TryGetValue(package.PackageName, out InstalledPackage? installedPackage))
{
installedPackage = new InstalledPackage {
installedPackage = new InstalledPackage
{
PackageName = package.PackageName,
Version = string.IsNullOrEmpty(package.Version) ? "Unknown" : package.Version,
};
installedPackages.Add(package.PackageName, installedPackage);
}
// Set additional values
if (!string.IsNullOrEmpty(package.Version))
{
installedPackage.Version = package.Version;
}
installedPackage.PackageView = package.PackageView;
}

View File

@@ -17,7 +17,7 @@
</div>
<div class="umb-package-list__item-content">
<div class="umb-package-list__item-name">{{ installedPackage.name }}</div>
<div class="umb-package-list__item-description">Version: {{ installedPackage.version }}</div>
<div class="umb-package-list__item-description" ng-if="installedPackage.version">Version: {{ installedPackage.version }}</div>
<div class="umb-package-list__item-description">
<localize ng-if="installedPackage.hasMigrations && !installedPackage.hasPendingMigrations"
key="packager_packageMigrationsNonePending">No pending migrations</localize>

View File

@@ -1,5 +1,5 @@
{
{
"name": "UmbracoPackage",
"version": "",
"versionAssemblyName": "UmbracoPackage",
"allowPackageTelemetry": true
}
}

View File

@@ -1,5 +1,5 @@
{
"name": "UmbracoPackage",
"version": "",
"versionAssemblyName": "UmbracoPackage",
"allowPackageTelemetry": true
}