Skip to content

Commit

Permalink
! lib_compiled: Reorganize Pog.Package class hierarchy in preparation…
Browse files Browse the repository at this point in the history
… for remote repository
MatejKafka committed Apr 1, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 69116c8 commit c715cf3
Showing 12 changed files with 412 additions and 333 deletions.
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@

namespace Pog.Commands;

// TODO: only works on local repository, either document it or extend it to remote repositories (probably don't)
/// <summary>
/// <para type="synopsis">Validates a repository package.</para>
/// <para type="description">
@@ -27,7 +28,7 @@ public sealed class ConfirmPogRepositoryPackageCommand : PogCmdlet {
protected const string DefaultPS = PackageNamePS;

[Parameter(Mandatory = true, Position = 0, ParameterSetName = PackagePS, ValueFromPipeline = true)]
public RepositoryPackage[] Package = null!;
public LocalRepositoryPackage[] Package = null!;

/// <summary><para type="description">Name of the repository package.</para></summary>
[Parameter(Position = 0, ParameterSetName = PackageNamePS, ValueFromPipeline = true)]
@@ -39,12 +40,21 @@ public sealed class ConfirmPogRepositoryPackageCommand : PogCmdlet {
[ArgumentCompleter(typeof(PSAttributes.RepositoryPackageVersionCompleter))]
public PackageVersion? Version;

private readonly Repository _repo = InternalState.Repository;
private readonly LocalRepository _repo;
private bool _noIssues = true;

private static readonly Regex QuoteHighlightRegex = new(@"'([^']+)'",
RegexOptions.Compiled | RegexOptions.CultureInvariant);

public ConfirmPogRepositoryPackageCommand() {
if (InternalState.Repository is LocalRepository r) {
_repo = r;
} else {
// TODO: implement
throw new RuntimeException("Validation of remote repositories is not yet supported");
}
}

private void AddIssue(string message) {
_noIssues = false;
var aligned = message.Replace("\n", "\n ");
@@ -68,9 +78,10 @@ protected override void BeginProcessing() {
"-Version must not be passed when -PackageName contains multiple package names.");
}

RepositoryPackage? package;
LocalRepositoryPackage? package;
try {
package = _repo.GetPackage(PackageName![0], true, true).GetVersionPackage(Version, true);
package = (LocalRepositoryPackage) _repo.GetPackage(PackageName![0], true, true)
.GetVersionPackage(Version, true);
} catch (RepositoryPackageNotFoundException e) {
WriteError(e, "PackageNotFound", ErrorCategory.ObjectNotFound, PackageName![0]);
return;
@@ -111,9 +122,9 @@ protected override void EndProcessing() {
WriteObject(_noIssues);
}

private RepositoryVersionedPackage? ResolvePackage(string packageName) {
private LocalRepositoryVersionedPackage? ResolvePackage(string packageName) {
try {
return _repo.GetPackage(packageName, true, true);
return (LocalRepositoryVersionedPackage) _repo.GetPackage(packageName, true, true);
} catch (RepositoryPackageNotFoundException e) {
WriteError(e, "PackageNotFound", ErrorCategory.ObjectNotFound, packageName);
return null;
@@ -134,16 +145,16 @@ private void ValidateAll() {

// validate all packages
foreach (var vp in _repo.Enumerate()) {
ValidatePackage(vp);
ValidatePackage((LocalRepositoryVersionedPackage) vp);
}
}

private void ValidatePackage(RepositoryVersionedPackage vp) {
private void ValidatePackage(LocalRepositoryVersionedPackage vp) {
if (vp.IsTemplated) ValidateTemplatedPackage(vp);
else ValidateDirectPackage(vp);
}

private void ValidateTemplatedPackage(RepositoryVersionedPackage vp) {
private void ValidateTemplatedPackage(LocalRepositoryVersionedPackage vp) {
if (File.Exists($"{vp.Path}\\.template.psd1")) {
AddIssue($"Package '{vp.PackageName}' contains an invalid version '.template'. " +
$"This version is not allowed, as it leads to ambiguity for direct packages.");
@@ -165,7 +176,7 @@ private void ValidateTemplatedPackage(RepositoryVersionedPackage vp) {
}

// validate .template dir
ValidateManifestDirectory($"'{vp.PackageName}'", templateDirPath);
ValidateManifestDirectory($"package '{vp.PackageName}'", templateDirPath);

// validate that manifest template exists
if (!File.Exists(templatePath)) {
@@ -176,7 +187,7 @@ private void ValidateTemplatedPackage(RepositoryVersionedPackage vp) {
ValidatePackageVersions(vp, false); // manifest dir already validated
}

private void ValidateDirectPackage(RepositoryVersionedPackage vp) {
private void ValidateDirectPackage(LocalRepositoryVersionedPackage vp) {
var extraFiles = GetFileList(Directory.EnumerateFiles(vp.Path));
if (extraFiles != "") {
AddIssue($"Package '{vp.PackageName}' has an incorrect file structure, contains extra files, " +
@@ -186,11 +197,11 @@ private void ValidateDirectPackage(RepositoryVersionedPackage vp) {
ValidatePackageVersions(vp, true);
}

private void ValidatePackageVersions(RepositoryVersionedPackage vp, bool validateManifestDir) {
private void ValidatePackageVersions(LocalRepositoryVersionedPackage vp, bool validateManifestDir) {
var hasVersion = false;
foreach (var p in vp.Enumerate()) {
hasVersion = true;
ValidatePackageVersion(p, validateManifestDir);
ValidatePackageVersion((LocalRepositoryPackage) p, validateManifestDir);
}

if (!hasVersion) {
@@ -199,9 +210,9 @@ private void ValidatePackageVersions(RepositoryVersionedPackage vp, bool validat
}
}

private void ValidatePackageVersion(RepositoryPackage p, bool validateManifestDir = true) {
private void ValidatePackageVersion(LocalRepositoryPackage p, bool validateManifestDir = true) {
if (validateManifestDir) {
ValidateManifestDirectory($"'{p.PackageName}', version '{p.Version}'", p.Path);
ValidateManifestDirectory(p.GetDescriptionString(), p.Path);
}

// validate the manifest
@@ -239,13 +250,13 @@ private void ValidateManifestDirectory(string packageInfoStr, string templateDir
var extraEntries = GetFileList(Directory.EnumerateFileSystemEntries(templateDirPath)
.Where(p => !p.EndsWith(@"\pog.psd1") && !p.EndsWith(@"\.pog")));
if (extraEntries != "") {
AddIssue($"Manifest directory for package {packageInfoStr} at '{templateDirPath}' contains extra " +
AddIssue($"Manifest directory for {packageInfoStr} at '{templateDirPath}' contains extra " +
$"entries: {extraEntries}. Only a 'pog.psd1' manifest file and an optional '.pog' directory for extra " +
$"files is allowed.");
}

if (File.Exists($"{templateDirPath}\\.pog")) {
AddIssue($"Extra resource directory for package {packageInfoStr} must be a directory, " +
AddIssue($"Extra resource directory for {packageInfoStr} must be a directory, " +
$"found a file at '{templateDirPath}\\.pog'.");
}

Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ public sealed class GetPogRepositoryPackage : PogCmdlet {
[Parameter(ParameterSetName = AllVersionsPS)]
public SwitchParameter AllVersions;

private readonly Repository _packages = InternalState.Repository;
private readonly IRepository _packages = InternalState.Repository;

protected override void BeginProcessing() {
base.BeginProcessing();
4 changes: 3 additions & 1 deletion app/Pog/lib_compiled/Pog/src/Commands/ImportPogCommand.cs
Original file line number Diff line number Diff line change
@@ -277,7 +277,7 @@ private void ImportPackage(RepositoryPackage package, ImportedPackage target) {

// FIXME: for templated manifests, `MatchesRepositoryManifest` will build the manifest once,
// and then .ImportTo will build it a second time; figure out how to avoid the duplication
if (!Force && target.MatchesRepositoryManifest(package)) {
if (!Force && package.MatchesImportedManifest(target)) {
WriteInformation($"Skipping import of package '{package.PackageName}', " +
"target already contains this package.");
return;
@@ -319,6 +319,8 @@ private bool ConfirmManifestOverwrite(RepositoryPackage package, ImportedPackage
// either a random folder was erroneously created, or this is a package, but corrupted
WriteWarning($"A package directory already exists at '{target.Path}', but it doesn't seem to contain " +
$"a package manifest. All directories in a package root should be packages with a valid manifest.");
// overwrite without confirmation
return true;
} catch (Exception e) when (e is IPackageManifestException) {
WriteWarning($"Found an existing package manifest at '{target.Path}', but it is not valid.");
}
47 changes: 18 additions & 29 deletions app/Pog/lib_compiled/Pog/src/ImportedPackageTypes.cs
Original file line number Diff line number Diff line change
@@ -3,9 +3,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Pog.Utils;
using IOPath = System.IO.Path;

namespace Pog;
@@ -118,18 +116,33 @@ public IEnumerable<ImportedPackage> EnumeratePackages(string packageRoot, bool l
/// otherwise an exception is thrown.
/// </summary>
[PublicAPI]
public sealed class ImportedPackage : Package {
public sealed class ImportedPackage : Package, ILocalPackage {
public PackageVersion? Version => Manifest.Version;
public string? ManifestName => Manifest.Name;

internal ImportedPackage(string packageName, string path, bool loadManifest = true) : base(packageName, path) {
public string Path {get;}
public string ManifestPath => IOPath.Combine(Path, PathConfig.PackagePaths.ManifestRelPath);
public string ManifestResourceDirPath => IOPath.Combine(Path, PathConfig.PackagePaths.ManifestResourceRelPath);

public override bool Exists => Directory.Exists(Path);

internal ImportedPackage(string packageName, string path, bool loadManifest = true) : base(packageName, null) {
Verify.Assert.FilePath(path);
Verify.Assert.PackageName(packageName);
Path = path;
if (loadManifest) {
// load the manifest to validate it and ensure the getters won't throw
ReloadManifest();
}
}

protected override PackageManifest LoadManifest() {
if (!Exists) {
throw new PackageNotFoundException($"Tried to read the package manifest of a non-existent package at '{Path}'.");
}
return new PackageManifest(ManifestPath);
}

// called while importing a new manifest
internal void RemoveManifest() {
FsUtils.EnsureDeleteFile(ManifestPath);
@@ -138,26 +151,6 @@ internal void RemoveManifest() {
InvalidateManifest();
}

public bool MatchesRepositoryManifest(RepositoryPackage p) {
// compare resource dirs
if (!FsUtils.DirectoryTreeEqual(ManifestResourceDirPath, p.ManifestResourceDirPath)) {
return false;
}

// compare manifest
var importedManifest = new FileInfo(ManifestPath);
if (!importedManifest.Exists) {
return false;
} else if (p is DirectRepositoryPackage dp) {
return importedManifest.Exists && FsUtils.FileContentEqual(new FileInfo(dp.ManifestPath), importedManifest);
} else if (p is TemplatedRepositoryPackage tp) {
var repoManifest = Encoding.UTF8.GetBytes(tp.GetManifestString());
return FsUtils.FileContentEqual(repoManifest, importedManifest);
} else {
throw new UnreachableException();
}
}

public bool RemoveExportedShortcuts() {
// shortcut dir is the root of the package, delete the shortcuts one-by-one instead of deleting the whole directory
var deleted = false;
@@ -206,11 +199,7 @@ private IEnumerable<FileInfo> EnumerateFilesRel(string relDirPath, string search
}
}

protected override PackageManifest LoadManifest() {
return new PackageManifest(ManifestPath);
}

public string GetDescriptionString() {
public override string GetDescriptionString() {
var versionStr = Manifest.Version != null ? $", version '{Manifest.Version}'" : "";
if (Manifest.Private) {
return $"private package '{PackageName}'{versionStr}";
176 changes: 176 additions & 0 deletions app/Pog/lib_compiled/Pog/src/LocalRepositoryTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using Pog.Utils;
using IOPath = System.IO.Path;
using PPaths = Pog.PathConfig.PackagePaths;

namespace Pog;

[PublicAPI]
public sealed class LocalRepository(string manifestRepositoryDirPath) : IRepository {
public readonly string Path = manifestRepositoryDirPath;
public bool Exists => Directory.Exists(Path);

public IEnumerable<string> EnumeratePackageNames(string searchPattern = "*") {
try {
return FsUtils.EnumerateNonHiddenDirectoryNames(Path, searchPattern);
} catch (DirectoryNotFoundException) {
throw new RepositoryNotFoundException($"Package repository does not seem to exist: {Path}");
}
}

public IEnumerable<RepositoryVersionedPackage> Enumerate(string searchPattern = "*") {
var repo = this;
return EnumeratePackageNames(searchPattern).Select(p => new LocalRepositoryVersionedPackage(repo, p));
}

public RepositoryVersionedPackage GetPackage(string packageName, bool resolveName, bool mustExist) {
Verify.PackageName(packageName);
if (resolveName) {
packageName = FsUtils.GetResolvedChildName(Path, packageName);
}
var package = new LocalRepositoryVersionedPackage(this, packageName);
if (mustExist && !package.Exists) {
throw new RepositoryPackageNotFoundException(
$"Package '{package.PackageName}' does not exist in the repository, expected path: {package.Path}");
}
return package;
}
}

/// <summary>
/// Class representing a repository directory containing different versions of a RepositoryPackage.
/// </summary>
/// The backing directory may not exist, in which case the enumeration methods behave as if it was empty.
[PublicAPI]
public sealed class LocalRepositoryVersionedPackage : RepositoryVersionedPackage {
public override IRepository Repository {get;}
public readonly string Path;
public override bool Exists => Directory.Exists(Path);
public bool IsTemplated => Directory.Exists(TemplateDirPath);

public string TemplateDirPath => $"{Path}\\{PPaths.RepositoryTemplateDirName}";
internal string TemplatePath => $"{TemplateDirPath}\\{PPaths.ManifestRelPath}";
protected override string ExpectedPathStr => $"expected path: {Path}";

internal LocalRepositoryVersionedPackage(LocalRepository repository, string packageName) : base(packageName) {
Repository = repository;
Path = $"{repository.Path}\\{packageName}";
}

public override IEnumerable<string> EnumerateVersionStrings(string searchPattern = "*") {
try {
if (IsTemplated) {
return FsUtils.EnumerateNonHiddenFileNames(Path, searchPattern + ".psd1")
.Select(IOPath.GetFileNameWithoutExtension);
} else {
return FsUtils.EnumerateNonHiddenDirectoryNames(Path, searchPattern);
}
} catch (DirectoryNotFoundException) {
return Enumerable.Empty<string>();
}
}

protected override RepositoryPackage GetPackageUnchecked(PackageVersion version) {
return this.IsTemplated
? new TemplatedLocalRepositoryPackage(this, version)
: new DirectLocalRepositoryPackage(this, version);
}
}

[PublicAPI]
public abstract class LocalRepositoryPackage(LocalRepositoryVersionedPackage parent, PackageVersion version, string path)
: RepositoryPackage(parent, version), ILocalPackage {
public string Path {get; init;} = path;
public abstract string ManifestPath {get;}
protected abstract string ManifestResourceDirPath {get;}
protected abstract void ImportManifestTo(string targetManifestPath);

public override void ImportTo(ImportedPackage target) {
// remove any previous manifest
target.RemoveManifest();
// ensure target directory exists
Directory.CreateDirectory(target.Path);

// copy the resource directory
var resDir = new DirectoryInfo(ManifestResourceDirPath);
if (resDir.Exists) {
FsUtils.CopyDirectory(resDir, target.ManifestResourceDirPath);
}

// write the manifest
ImportManifestTo(target.ManifestPath);

Debug.Assert(MatchesImportedManifest(target));
}

public override bool MatchesImportedManifest(ImportedPackage p) {
// compare resource dirs
if (!FsUtils.DirectoryTreeEqual(ManifestResourceDirPath, p.ManifestResourceDirPath)) {
return false;
}

// compare manifest
var importedManifest = new FileInfo(p.ManifestPath);
if (!importedManifest.Exists) {
return false;
} else if (this is DirectLocalRepositoryPackage dp) {
return importedManifest.Exists && FsUtils.FileContentEqual(new FileInfo(dp.ManifestPath), importedManifest);
} else if (this is TemplatedLocalRepositoryPackage tp) {
var repoManifest = Encoding.UTF8.GetBytes(tp.GetManifestString());
return FsUtils.FileContentEqual(repoManifest, importedManifest);
} else {
throw new UnreachableException();
}
}
}

[PublicAPI]
public sealed class DirectLocalRepositoryPackage(LocalRepositoryVersionedPackage parent, PackageVersion version)
: LocalRepositoryPackage(parent, version, $"{parent.Path}\\{version}") {
public override string ManifestPath => $"{Path}\\{PPaths.ManifestRelPath}";
protected override string ManifestResourceDirPath => $"{Path}\\{PPaths.ManifestResourceRelPath}";
public override bool Exists => Directory.Exists(Path);

protected override void ImportManifestTo(string targetManifestPath) {
File.Copy(ManifestPath, targetManifestPath);
}

protected override PackageManifest LoadManifest() {
if (!Exists) {
throw new PackageNotFoundException($"Tried to read the package manifest of a non-existent package at '{Path}'.");
}
return new PackageManifest(ManifestPath, owningPackage: this);
}
}

[PublicAPI]
public sealed class TemplatedLocalRepositoryPackage(LocalRepositoryVersionedPackage parent, PackageVersion version)
: LocalRepositoryPackage(parent, version, $"{parent.Path}\\{version}.psd1") {
public override string ManifestPath => Path;
protected override string ManifestResourceDirPath => $"{TemplateDirPath}\\{PPaths.ManifestResourceRelPath}";
public override bool Exists => File.Exists(Path) && Directory.Exists(TemplateDirPath);

private string TemplateDirPath => ((LocalRepositoryVersionedPackage) Container).TemplateDirPath;
private string TemplatePath => $"{TemplateDirPath}\\{PPaths.ManifestRelPath}";

protected override void ImportManifestTo(string targetManifestPath) {
// TODO: figure out how to avoid calling .Substitute twice when first validating, and then importing the package
ManifestTemplateFile.Substitute(TemplatePath, ManifestPath, targetManifestPath);
}

protected override PackageManifest LoadManifest() {
if (!Exists) {
throw new PackageNotFoundException($"Tried to read the package manifest of a non-existent package at '{Path}'.");
}
return new PackageManifest(GetManifestString(), ManifestPath, owningPackage: this);
}

internal string GetManifestString() {
return ManifestTemplateFile.Substitute(TemplatePath, ManifestPath);
}
}
27 changes: 17 additions & 10 deletions app/Pog/lib_compiled/Pog/src/Pog.Container.cs
Original file line number Diff line number Diff line change
@@ -92,18 +92,25 @@ private Runspace GetInitializedRunspace(PSHost? host, OutputStreamConfig streamC
var iss = CreateInitialSessionState();
var runspace = host == null ? RunspaceFactory.CreateRunspace(iss) : RunspaceFactory.CreateRunspace(host, iss);

// set the working directory
// this is a hack, but unfortunately a necessary one: https://github.com/PowerShell/PowerShell/issues/17603
// TODO: add a mutex here in case multiple containers are started in parallel
var originalWorkingDirectory = Environment.CurrentDirectory;
try {
// temporarily override the working directory
Environment.CurrentDirectory = _package.Path;
if (_package is ILocalPackage lp) {
// set the working directory
// this is a hack, but unfortunately a necessary one: https://github.com/PowerShell/PowerShell/issues/17603
// TODO: add a mutex here in case multiple containers are started in parallel
var originalWorkingDirectory = Environment.CurrentDirectory;
try {
// temporarily override the working directory
Environment.CurrentDirectory = lp.Path;
// run runspace init (module import, variable setup,...)
// the runspace keeps the changed working directory even after it's reverted back on the process level
runspace.Open();
} finally {
Environment.CurrentDirectory = originalWorkingDirectory;
}
} else {
// not a local package, do not change working directory

// run runspace init (module import, variable setup,...)
// the runspace keeps the changed working directory even after it's reverted back on the process level
runspace.Open();
} finally {
Environment.CurrentDirectory = originalWorkingDirectory;
}

// preference variables must be copied AFTER imports, otherwise we would get a slew
4 changes: 2 additions & 2 deletions app/Pog/lib_compiled/Pog/src/Pog.InternalState.cs
Original file line number Diff line number Diff line change
@@ -46,8 +46,8 @@ public static void OverrideDataRoot(string dataRootDirPath) {
private static PathConfig? _pathConfig;
public static PathConfig PathConfig => _pathConfig ??= new PathConfig(GetRootDirPath());

private static Repository? _repository;
public static Repository Repository => _repository ??= new Repository(PathConfig.ManifestRepositoryDir);
private static IRepository? _repository;
public static IRepository Repository => _repository ??= new LocalRepository(PathConfig.ManifestRepositoryDir);

private static GeneratorRepository? _generatorRepository;
public static GeneratorRepository GeneratorRepository =>
47 changes: 20 additions & 27 deletions app/Pog/lib_compiled/Pog/src/Pog.Package.cs
Original file line number Diff line number Diff line change
@@ -1,59 +1,52 @@
using System.IO;
using JetBrains.Annotations;
using IOPath = System.IO.Path;

namespace Pog;

public class PackageNotFoundException(string message) : DirectoryNotFoundException(message);

[PublicAPI]
public abstract class Package {
public readonly string PackageName;
public readonly string Path;
public string ManifestPath => _manifest != null ? Manifest.Path : GetManifestPath();

public virtual bool Exists => Directory.Exists(Path);

// TODO: make this public?
internal string ManifestResourceDirPath => IOPath.Combine(Path, PathConfig.PackagePaths.ManifestResourceRelPath);
public abstract bool Exists {get;}

private PackageManifest? _manifest;
public PackageManifest Manifest => EnsureManifestIsLoaded();

/// Only used for asserting that the caller called `EnsureManifestIsLoaded()`
internal bool ManifestLoaded => _manifest != null;

protected Package(string packageName, string packagePath, PackageManifest? manifest = null) {
protected Package(string packageName, PackageManifest? manifest) {
Verify.Assert.PackageName(packageName);
Verify.Assert.FilePath(packagePath);
PackageName = packageName;
Path = packagePath;
_manifest = manifest;
}

/// <inheritdoc cref="ReloadManifest"/>
protected void InvalidateManifest() {
_manifest = null;
}

/// <inheritdoc cref="LoadManifest"/>
public PackageManifest EnsureManifestIsLoaded() {
return _manifest ?? ReloadManifest();
}

protected void InvalidateManifest() {
_manifest = null;
/// <inheritdoc cref="LoadManifest"/>
public PackageManifest ReloadManifest() {
return _manifest = LoadManifest();
}

/// <exception cref="PackageNotFoundException">The package directory does not exist.</exception>
/// <exception cref="PackageManifestNotFoundException">The package manifest file does not exist.</exception>
/// <exception cref="PackageManifestParseException">The package manifest file is not a valid PowerShell data file (.psd1).</exception>
/// <exception cref="InvalidPackageManifestStructureException">The package manifest is a valid data file, but the structure is not valid.</exception>
public PackageManifest ReloadManifest() {
if (!Exists) {
throw new PackageNotFoundException($"Tried to read the package manifest of a non-existent package at '{Path}'.");
}
return _manifest = LoadManifest();
}
protected abstract PackageManifest LoadManifest();

protected virtual string GetManifestPath() {
return IOPath.Combine(Path, PathConfig.PackagePaths.ManifestRelPath);
}
public abstract string GetDescriptionString();
}

protected abstract PackageManifest LoadManifest();
public interface ILocalPackage {
string Path {get;}
string ManifestPath {get;}
}

public interface IRemotePackage {
string Url {get;}
}
51 changes: 26 additions & 25 deletions app/Pog/lib_compiled/Pog/src/Pog.PackageManifest.cs
Original file line number Diff line number Diff line change
@@ -18,32 +18,32 @@ public class PackageManifestNotFoundException(string message, string fileName)

[PublicAPI]
public class PackageManifestParseException : ParseException, IPackageManifestException {
public readonly string ManifestPath;
public readonly string ManifestSource;

internal PackageManifestParseException(string manifestPath, string message) : base(message) {
ManifestPath = manifestPath;
internal PackageManifestParseException(string manifestSource, string message) : base(message) {
ManifestSource = manifestSource;
}

internal PackageManifestParseException(string manifestPath, ParseError[] errors) : base(errors) {
ManifestPath = manifestPath;
internal PackageManifestParseException(string manifestSource, ParseError[] errors) : base(errors) {
ManifestSource = manifestSource;
}

public override string Message =>
$"Could not {(Errors == null ? "load" : "parse")} the package manifest at '{ManifestPath}':\n" + base.Message;
$"Could not {(Errors == null ? "load" : "parse")} the package manifest at '{ManifestSource}':\n" + base.Message;
}

[PublicAPI]
public class InvalidPackageManifestStructureException : Exception, IPackageManifestException {
public readonly string ManifestPath;
public readonly string ManifestSource;
public readonly List<string> Issues;

public InvalidPackageManifestStructureException(string manifestPath, List<string> issues) {
ManifestPath = manifestPath;
public InvalidPackageManifestStructureException(string manifestSource, List<string> issues) {
ManifestSource = manifestSource;
Issues = issues;
}

public override string Message =>
$"Package manifest at '{ManifestPath}' has invalid structure:\n\t" + string.Join("\n\t", Issues);
$"Package manifest at '{ManifestSource}' has invalid structure:\n\t" + string.Join("\n\t", Issues);
}

/// <summary>Package manifest had a ScriptBlock as the 'Install.Url' property, but it did not return a valid URL.</summary>
@@ -53,8 +53,6 @@ public InvalidPackageManifestUrlScriptBlockException(string message) : base(mess

[PublicAPI]
public record PackageManifest {
public readonly string Path;

public readonly bool Private;
public readonly string? Name;
public readonly PackageVersion? Version;
@@ -70,26 +68,29 @@ public record PackageManifest {

public readonly Hashtable Raw;

/// <inheritdoc cref="PackageManifest(string, string?, RepositoryPackage?)"/>
/// <inheritdoc cref="PackageManifest(string, RepositoryPackage?)"/>
public PackageManifest(string manifestPath) : this(manifestPath, null) {}

/// <param name="manifestPath">Path to the manifest file.</param>
/// <param name="manifestStr">
/// If passed, the manifest is parsed from the string and <paramref name="manifestPath"/> is only used to improve error reporting.
/// </param>
/// <param name="owningPackage">If parsing a repository manifest, this should be the package that owns the manifest.</param>
///
/// <inheritdoc cref="PackageManifest(Hashtable, string, RepositoryPackage?)"/>
internal PackageManifest(string manifestPath, RepositoryPackage? owningPackage = null)
: this(PackageManifestParser.LoadManifest(manifestPath), manifestPath, owningPackage) {}

/// <param name="manifestStr">String from which the manifest is parsed.</param>
/// <param name="manifestSource">Source describing the origin of the manifest string, used for better error reporting.</param>
/// <param name="owningPackage">If parsing a repository manifest, this should be the package that owns the manifest.</param>
/// <inheritdoc cref="PackageManifest(Hashtable, string, RepositoryPackage?)"/>
internal PackageManifest(string manifestStr, string manifestSource, RepositoryPackage? owningPackage = null)
: this(PackageManifestParser.LoadManifest(manifestStr, manifestSource), manifestSource, owningPackage) {}

/// <exception cref="PackageManifestNotFoundException">The package manifest file does not exist.</exception>
/// <exception cref="PackageManifestParseException">The package manifest file is not a valid PowerShell data file (.psd1).</exception>
/// <exception cref="InvalidPackageManifestStructureException">The package manifest was correctly parsed, but has invalid structure.</exception>
internal PackageManifest(string manifestPath, string? manifestStr = null, RepositoryPackage? owningPackage = null) {
InstrumentationCounter.ManifestLoads++;

Path = manifestPath;
Raw = PackageManifestParser.LoadManifest(manifestPath, manifestStr);

private PackageManifest(Hashtable manifest, string manifestSource, RepositoryPackage? owningPackage = null) {
Raw = manifest;
// parse the raw manifest into properties on this object
HashtableParser parser = new(Raw);
var parser = new HashtableParser(manifest);

Private = parser.ParseScalar<bool>("Private", false) ?? false;
if (Private) {
@@ -131,7 +132,7 @@ internal PackageManifest(string manifestPath, string? manifestStr = null, Reposi
}

if (parser.HasIssues) {
throw new InvalidPackageManifestStructureException(manifestPath, parser.Issues);
throw new InvalidPackageManifestStructureException(manifestSource, parser.Issues);
}
}

31 changes: 22 additions & 9 deletions app/Pog/lib_compiled/Pog/src/Pog.PackageManifestParser.cs
Original file line number Diff line number Diff line change
@@ -7,28 +7,41 @@

namespace Pog;

// NOTE: how the manifest is loaded is important; the resulting scriptblocks must NOT be bound to a single runspace;
// this should not be an issue when loading the manifest in C#, but in PowerShell, it happens semi-often
// (see e.g. https://github.com/PowerShell/PowerShell/issues/11658#issuecomment-577304407)
internal static class PackageManifestParser {
/// Loads the manifest the same way as `Import-PowerShellDataFile` would, while providing better error messages and
/// unwrapping any script-blocks (see the other methods).
public static Hashtable LoadManifest(string manifestPath, string? manifestStr = null) {
if (manifestStr == null && !File.Exists(manifestPath)) {
public static Hashtable LoadManifest(string manifestPath) {
if (!File.Exists(manifestPath)) {
throw new PackageManifestNotFoundException($"Package manifest file is missing, expected path: {manifestPath}",
manifestPath);
}

// NOTE: how this is loaded is important; the resulting scriptblocks must NOT be bound to a single runspace;
// this should not be an issue when loading the manifest in C#, but in PowerShell, it happens semi-often
// (see e.g. https://github.com/PowerShell/PowerShell/issues/11658#issuecomment-577304407)
var ast = manifestStr == null
? Parser.ParseFile(manifestPath, out _, out var errors)
: Parser.ParseInput(manifestStr, out _, out errors);
var ast = Parser.ParseFile(manifestPath, out _, out var errors);
if (errors.Length > 0) {
throw new PackageManifestParseException(manifestPath, errors);
}

return ParseLoadedManifest(ast, manifestPath);
}

/// Loads the manifest the same way as `Import-PowerShellDataFile` would, while providing better error messages and
/// unwrapping any script-blocks (see the other methods).
public static Hashtable LoadManifest(string manifestStr, string manifestSource) {
var ast = Parser.ParseInput(manifestStr, out _, out var errors);
if (errors.Length > 0) {
throw new PackageManifestParseException(manifestSource, errors);
}

return ParseLoadedManifest(ast, manifestSource);
}

private static Hashtable ParseLoadedManifest(Ast ast, string manifestSource) {
var hashtableNode = (HashtableAst) ast.Find(static a => a is HashtableAst, false);
if (hashtableNode == null) {
throw new PackageManifestParseException(manifestPath,
throw new PackageManifestParseException(manifestSource,
"The manifest is not a valid PowerShell data file, must be a single Hashtable literal.");
}

212 changes: 0 additions & 212 deletions app/Pog/lib_compiled/Pog/src/RepositoryPackageTypes.cs

This file was deleted.

99 changes: 99 additions & 0 deletions app/Pog/lib_compiled/Pog/src/RepositoryTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using PPaths = Pog.PathConfig.PackagePaths;

namespace Pog;

public class RepositoryNotFoundException(string message) : Exception(message);

public class RepositoryPackageNotFoundException(string message) : PackageNotFoundException(message);

public class RepositoryPackageVersionNotFoundException(string message) : PackageNotFoundException(message);

// it would be nice if we could express in the type system that we have 2 separate hierarchies
// (e.g. RemoteRepository always returns RemoteRepositoryVersionedPackage), but since .netstandard2.0 does not support
// covariant return types, we would have to express it with generics, which would prevent us from having a list of repositories
[PublicAPI]
public interface IRepository {
public bool Exists {get;}
public IEnumerable<string> EnumeratePackageNames(string searchPattern = "*");
public IEnumerable<RepositoryVersionedPackage> Enumerate(string searchPattern = "*");
public RepositoryVersionedPackage GetPackage(string packageName, bool resolveName, bool mustExist);
}

/// <summary>
/// Class representing a package with multiple available versions. Each version is represented by an `IRepositoryPackage`.
/// </summary>
/// The backing directory may not exist, in which case the enumeration methods behave as if there were no existing versions.
[PublicAPI]
public abstract class RepositoryVersionedPackage {
public readonly string PackageName;
public abstract IRepository Repository {get;}
public abstract bool Exists {get;}

protected RepositoryVersionedPackage(string packageName) {
Verify.Assert.PackageName(packageName);
PackageName = packageName;
}

protected abstract string ExpectedPathStr {get;}
protected abstract RepositoryPackage GetPackageUnchecked(PackageVersion version);

/// Enumerate UNORDERED versions of the package.
public abstract IEnumerable<string> EnumerateVersionStrings(string searchPattern = "*");

/// Enumerate parsed versions of the package, in a DESCENDING order.
public virtual IEnumerable<PackageVersion> EnumerateVersions(string searchPattern = "*") {
return EnumerateVersionStrings(searchPattern)
.Select(v => new PackageVersion(v))
.OrderByDescending(v => v);
}

/// Enumerate packages for versions matching the pattern, in a DESCENDING order according to the version.
public IEnumerable<RepositoryPackage> Enumerate(string searchPattern = "*") {
return EnumerateVersions(searchPattern).Select(GetPackageUnchecked);
}

public RepositoryPackage? TryGetLatestPackage() {
var latestVersion = EnumerateVersionStrings().Select(v => new PackageVersion(v)).Max();
return latestVersion == null ? null : GetPackageUnchecked(latestVersion);
}

public RepositoryPackage GetLatestPackage() {
return TryGetLatestPackage() ?? throw new RepositoryPackageVersionNotFoundException(
$"Package '{PackageName}' in the repository does not have any versions, {ExpectedPathStr}");
}

public RepositoryPackage GetVersionPackage(string version, bool mustExist) {
return GetVersionPackage(new PackageVersion(version), mustExist);
}

public RepositoryPackage GetVersionPackage(PackageVersion version, bool mustExist) {
if (version.ToString() == PPaths.RepositoryTemplateDirName) {
// disallow creating this version, otherwise we couldn't distinguish between a templated and direct package types
throw new InvalidPackageVersionException(
$"Version of a package in the repository must not be '{PPaths.RepositoryTemplateDirName}'.");
}

var package = GetPackageUnchecked(version);
if (mustExist && !package.Exists) {
throw new RepositoryPackageVersionNotFoundException(
$"Package '{PackageName}' in the repository does not have version '{version}', {ExpectedPathStr}");
}
return package;
}
}

[PublicAPI]
public abstract class RepositoryPackage(RepositoryVersionedPackage parent, PackageVersion version)
: Package(parent.PackageName, null) {
public readonly RepositoryVersionedPackage Container = parent;
public readonly PackageVersion Version = version;

public override string GetDescriptionString() => $"package '{PackageName}', version '{Version}'";

public abstract bool MatchesImportedManifest(ImportedPackage p);
public abstract void ImportTo(ImportedPackage target);
}

0 comments on commit c715cf3

Please sign in to comment.