Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Core/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ Which {0} provider would you like to install?</value></data>
If the game is still running, close it and try again. Otherwise check the permissions.</value></data>
<data name="KrakenMissingDependency" xml:space="preserve"><value>Unsatisfied dependency {1} needed for: {0}</value></data>
<data name="KrakenMissingDependencyNeededFor" xml:space="preserve"><value>needed for {0}</value></data>
<data name="KrakenRejectedProvidesConflict" xml:space="preserve"><value>{0} is needed for {3}, but cannot be installed because it conflicts with {2} which also provides {1}</value></data>
<data name="KrakenRejectedConflict" xml:space="preserve"><value>{0} is needed for {2}, but cannot be installed because it conflicts with {1}</value></data>
<data name="KrakenRejectedVersionMismatch" xml:space="preserve"><value>Version conflict: {0} is needed for {2}, but {1} is already being installed</value></data>
<data name="KrakenRejectedVersionMismatchFor" xml:space="preserve"><value>Version conflict: {0} is needed for {2}, and {1} is needed for {3}, but both cannot be installed at the same time</value></data>
<data name="KrakenConflictsWith" xml:space="preserve"><value>{0} conflicts with {1}</value></data>
<data name="KrakenFileExistsWithOwner" xml:space="preserve"><value>Oh no! We tried to overwrite a file owned by another mod!

Expand Down
262 changes: 240 additions & 22 deletions Core/Relationships/ResolvedRelationship.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,109 @@ namespace CKAN
{
using RelationshipCache = ConcurrentDictionary<RelationshipDescriptor, ResolvedRelationship>;

public abstract class ProviderRejection
{
public readonly CkanModule provider;
protected ProviderRejection(CkanModule provider)
{
this.provider = provider;
}
}

public sealed class RejectedByRelationship : ProviderRejection
{
public readonly Relationship violation;
public RejectedByRelationship(CkanModule provider, Relationship violation)
: base(provider)
{
this.violation = violation;
}

public static IEnumerable<RejectedByRelationship> WrapMany(
CkanModule candidate,
IEnumerable<Relationship> violations)
=> violations.Select(r => new RejectedByRelationship(candidate, r));

public override bool Equals(object? other)
=> other is RejectedByRelationship r
&& provider.Equals(r.provider)
&& violation.Equals(r.violation);

public override int GetHashCode()
=> (provider, violation).GetHashCode();
}

public sealed class RejectedByConflict : ProviderRejection
{
public readonly string? sharedProvidesId;
public readonly CkanModule blockingMod;
public readonly bool blockerIsInstalled;
public RejectedByConflict(CkanModule provider,
string? sharedProvidesId,
CkanModule blockingMod,
bool blockerIsInstalled)
: base(provider)
{
this.sharedProvidesId = sharedProvidesId;
this.blockingMod = blockingMod;
this.blockerIsInstalled = blockerIsInstalled;
}

public override bool Equals(object? other)
=> other is RejectedByConflict r
&& provider.Equals(r.provider)
&& sharedProvidesId == r.sharedProvidesId
&& blockingMod.Equals(r.blockingMod)
&& blockerIsInstalled == r.blockerIsInstalled;

public override int GetHashCode()
=> (provider, sharedProvidesId, blockingMod, blockerIsInstalled).GetHashCode();
}

public sealed class ResolutionContext
{
public readonly IRegistryQuerier Registry;
public readonly IReadOnlyCollection<CkanModule> Installed;
public readonly IReadOnlyCollection<CkanModule> Installing;
public readonly StabilityToleranceConfig StabilityTolerance;
public readonly GameVersionCriteria Crit;

public ResolutionContext(IRegistryQuerier registry,
IReadOnlyCollection<CkanModule> installed,
IReadOnlyCollection<CkanModule> installing,
StabilityToleranceConfig stabilityTolerance,
GameVersionCriteria crit)
{
Registry = registry;
Installed = installed;
Installing = installing;
StabilityTolerance = stabilityTolerance;
Crit = crit;
}
}

public sealed class RejectedByVersionMismatch : ProviderRejection
{
public readonly CkanModule blockingMod;
public readonly ResolvedRelationship[] blockerChain;
public RejectedByVersionMismatch(CkanModule provider,
CkanModule blockingMod,
ResolvedRelationship[]? blockerChain = null)
: base(provider)
{
this.blockingMod = blockingMod;
this.blockerChain = blockerChain ?? Array.Empty<ResolvedRelationship>();
}

public override bool Equals(object? other)
=> other is RejectedByVersionMismatch r
&& provider.Equals(r.provider)
&& blockingMod.Equals(r.blockingMod);

public override int GetHashCode()
=> (provider, blockingMod).GetHashCode();
}

public abstract class ResolvedRelationship : IEquatable<ResolvedRelationship>
{
public ResolvedRelationship(CkanModule source,
Expand Down Expand Up @@ -56,13 +159,13 @@ public override int GetHashCode()
=> (source, relationship, reason).GetHashCode();


public virtual IEnumerable<ResolvedRelationship[]> UnsatisfiedFrom()
public virtual IEnumerable<UnsatisfiedRelation> UnsatisfiedFrom()
=> reason is SelectionReason.Depends && Unsatisfied()
? Enumerable.Repeat(new ResolvedRelationship[] { this }, 1)
: Enumerable.Empty<ResolvedRelationship[]>();
? Enumerable.Repeat(new UnsatisfiedRelation(new ResolvedRelationship[] { this }, null), 1)
: Enumerable.Empty<UnsatisfiedRelation>();

public virtual IReadOnlyCollection<(ResolvedRelationship[], Relationship)> BadRelationships(IReadOnlyCollection<CkanModule> installing)
=> Array.Empty<(ResolvedRelationship[], Relationship)>();
public virtual IReadOnlyCollection<UnsatisfiedRelation> BadRelationships(IReadOnlyCollection<CkanModule> installing)
=> Array.Empty<UnsatisfiedRelation>();
}

public class ResolvedByInstalled : ResolvedRelationship
Expand Down Expand Up @@ -138,10 +241,12 @@ public class ResolvedByNew : ResolvedRelationship
public ResolvedByNew(CkanModule source,
RelationshipDescriptor relationship,
SelectionReason reason,
IReadOnlyDictionary<CkanModule, ResolvedRelationship[]> resolved)
IReadOnlyDictionary<CkanModule, ResolvedRelationship[]> resolved,
ResolutionContext? context = null)
: base(source, relationship, reason)
{
this.resolved = resolved;
this.context = context;
}

public ResolvedByNew(CkanModule source,
Expand Down Expand Up @@ -180,7 +285,11 @@ public ResolvedByNew(CkanModule source,
providers.Count == 1
? relationshipCache
: new RelationshipCache(relationshipCache))
.ToArray()))
.ToArray()),
new ResolutionContext(registry, installed,
allInstalling.Append(source).ToArray(),
new StabilityToleranceConfig(stabilityTolerance),
crit))
{
}

Expand All @@ -190,6 +299,11 @@ public ResolvedByNew(CkanModule source,
/// </summary>
public readonly IReadOnlyDictionary<CkanModule, ResolvedRelationship[]> resolved;

/// <summary>
/// The world this relationship was resolved against, if known.
/// </summary>
public readonly ResolutionContext? context;

public override bool Contains(CkanModule mod)
=> resolved.Any(rr => rr.Key == mod || rr.Value.Any(rrr => rrr.Contains(mod)));

Expand All @@ -215,10 +329,10 @@ public override IEnumerable<string> ToLines()

public override ResolvedRelationship WithSource(CkanModule newSrc, SelectionReason newRsn)
=> source == newSrc ? this
: new ResolvedByNew(newSrc, relationship, newRsn, resolved);
: new ResolvedByNew(newSrc, relationship, newRsn, resolved, context);


public override IEnumerable<ResolvedRelationship[]> UnsatisfiedFrom()
public override IEnumerable<UnsatisfiedRelation> UnsatisfiedFrom()
{
// Our goal here is to return an array of ResolvedRelationships for each full
// trace from rr to a relationship we can't satisfy.
Expand All @@ -228,7 +342,7 @@ public override IEnumerable<ResolvedRelationship[]> UnsatisfiedFrom()
// Now if this relationship itself can't be resolved directly, return it.
if (Unsatisfied())
{
return Enumerable.Repeat(new ResolvedRelationship[] { this }, 1);
return Enumerable.Repeat(new UnsatisfiedRelation(new ResolvedRelationship[] { this }, null), 1);
}
// Now we know it's a dependency that has at least one option for satisfying it,
// but those options may or may not be fully satisfied when considering _their_ dependencies.
Expand All @@ -242,29 +356,31 @@ public override IEnumerable<ResolvedRelationship[]> UnsatisfiedFrom()

return unsats.Any(u => u.Length == 0)
// One of the dependencies is fully satisfied
? Enumerable.Empty<ResolvedRelationship[]>()
: unsats.SelectMany(uns => uns.Select(u => u.Prepend(this).ToArray()));
? Enumerable.Empty<UnsatisfiedRelation>()
: unsats.SelectMany(uns => uns.Select(u => new UnsatisfiedRelation(
u.depends.Prepend(this).ToArray(),
u.rejection)));
}
return Enumerable.Empty<ResolvedRelationship[]>();
return Enumerable.Empty<UnsatisfiedRelation>();
}

public override IReadOnlyCollection<(ResolvedRelationship[], Relationship)> BadRelationships(IReadOnlyCollection<CkanModule> installing)
public override IReadOnlyCollection<UnsatisfiedRelation> BadRelationships(IReadOnlyCollection<CkanModule> installing)
{
if (reason is SelectionReason.Depends)
{
var unsatisfied = new List<(ResolvedRelationship[], Relationship)>();
var unsatisfied = new List<UnsatisfiedRelation>();
foreach ((CkanModule module, ResolvedRelationship[] resRels) in resolved)
{
if (module.BadRelationships(installing)
.Select(r => (new ResolvedRelationship[] { this }, r))
.ToArray()
if (RejectedByRelationship.WrapMany(module, module.BadRelationships(installing))
.Select(rej => new UnsatisfiedRelation(new ResolvedRelationship[] { this }, rej))
.ToArray()
is { Length: > 0 } badRels)
{
unsatisfied.AddRange(badRels);
}
else if (resRels.SelectMany(rr => rr.BadRelationships(installing))
.Select(tuple => (tuple.Item1.Prepend(this).ToArray(),
tuple.Item2))
.Select(u => new UnsatisfiedRelation(u.depends.Prepend(this).ToArray(),
u.rejection))
.ToArray()
is { Length: > 0 } badRRs)
{
Expand All @@ -273,12 +389,114 @@ public override IEnumerable<ResolvedRelationship[]> UnsatisfiedFrom()
else
{
// This relationship is satisfied
return Array.Empty<(ResolvedRelationship[], Relationship)>();
return Array.Empty<UnsatisfiedRelation>();
}
}
return unsatisfied;
}
return Array.Empty<(ResolvedRelationship[], Relationship)>();
return Array.Empty<UnsatisfiedRelation>();
}

/// <summary>
/// Re-resolves this relationship without filters and returns a list of
/// <see cref="UnsatisfiedRelation"/> that describe why each candidate
/// would be rejected.
/// </summary>
public IEnumerable<UnsatisfiedRelation> UnsatisfiedCandidates()
{
if (context == null)
{
yield break;
}

foreach (var module in relationship.LatestAvailableWithProvides(
context.Registry, context.StabilityTolerance, context.Crit, null, null))
{
var rejection = FindConflict(module, context.Installing, context.Installed)
?? (ProviderRejection?)RejectedByRelationship.WrapMany(
module,
module.BadRelationships(context.Installed)
.Concat(module.BadRelationships(context.Installing))
.Where(r => r.Type == RelationshipType.Depends))
.FirstOrDefault();
if (rejection != null)
{
yield return new UnsatisfiedRelation(
new ResolvedRelationship[] { this }, rejection);
}
}
}

// Find a mod that explicitly conflicts with the candidate (in either
// direction). Returns a rejection naming both sides; if they happen to
// share a virtual provides id, that's recorded for nicer messaging but
// is not required for the rejection to fire.
public static ProviderRejection? FindConflict(
CkanModule candidate,
IReadOnlyCollection<CkanModule> installing,
IReadOnlyCollection<CkanModule> installed)
{
foreach (var blocker in installed)
{
if (blocker.identifier != candidate.identifier
&& HasExplicitConflict(candidate, blocker))
{
return new RejectedByConflict(candidate, SharedProvidesId(candidate, blocker),
blocker, blockerIsInstalled: true);
}
}

foreach (var blocker in installing)
{
if (blocker.identifier != candidate.identifier
&& HasExplicitConflict(candidate, blocker))
{
return new RejectedByConflict(candidate, SharedProvidesId(candidate, blocker),
blocker, blockerIsInstalled: false);
}
}

return null;
}

private static bool HasExplicitConflict(CkanModule candidate, CkanModule blocker)
=> candidate.BadRelationships(new[] { blocker })
.Any(r => r.Type == RelationshipType.Conflicts);

// Best-effort: find a shared virtual id between the two mods so the
// message can name it. Each side's identifier is treated as an implicit
// provide so identifier-shadowing cases still produce a meaningful name.
// Returns null when there is no shared id; the conflict still stands.
private static string? SharedProvidesId(CkanModule candidate, CkanModule blocker)
{
var blockerIds = new HashSet<string>(blocker.provides ?? Enumerable.Empty<string>())
{
blocker.identifier,
};

return (candidate.provides ?? Enumerable.Empty<string>())
.Append(candidate.identifier)
.FirstOrDefault(blockerIds.Contains);
}
}

public sealed class UnsatisfiedRelation
{
/// <summary>
/// The dependency chain to reach this relationship.
/// </summary>
public readonly ResolvedRelationship[] depends;

/// <summary>
/// The reason that this relationship could not be satisfied, if any.
/// </summary>
public readonly ProviderRejection? rejection;

public UnsatisfiedRelation(ResolvedRelationship[] depends,
ProviderRejection? rejection)
{
this.depends = depends;
this.rejection = rejection;
}
}
}
Loading