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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public HomebrewSourceHelper(Homebrew manager)

protected override IReadOnlyList<IManagerSource> GetSources_UnSafe()
{
var sources = new List<ManagerSource>();
var tapLines = new List<string>();

using var p = new Process
{
Expand All @@ -32,8 +32,31 @@ protected override IReadOnlyList<IManagerSource> GetSources_UnSafe()
while ((line = p.StandardOutput.ReadLine()) is not null)
{
logger.AddToStdOut(line);
var name = line.Trim();
tapLines.Add(line);
}

logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
return BuildSourceList(tapLines);
}

/// <summary>
/// Homebrew 4 and later serve homebrew/core and homebrew/cask from the API, so `brew tap` does not
/// print them and `brew tap homebrew/core` is refused. The built-in sources are therefore always
/// listed, followed by every other tap.
/// </summary>
internal IReadOnlyList<IManagerSource> BuildSourceList(IEnumerable<string> tapLines)
{
var sources = new List<IManagerSource>(Manager.Properties.KnownSources);

foreach (string rawLine in tapLines)
{
var name = rawLine.Trim();
if (name.Length == 0) continue;
if (name.Equals(CoreTap, StringComparison.OrdinalIgnoreCase)
|| name.Equals(CaskTap, StringComparison.OrdinalIgnoreCase))
continue;

// Build a best-effort URL: "org/repo" → "https://github.com/org/homebrew-repo"
Uri url;
Expand All @@ -60,19 +83,33 @@ protected override IReadOnlyList<IManagerSource> GetSources_UnSafe()
}
}

logger.AddToStdErr(p.StandardError.ReadToEnd());
p.WaitForExit();
logger.Close(p.ExitCode);
return sources;
}

// ── Add / remove ───────────────────────────────────────────────────────

internal const string CoreTap = "homebrew/core";
internal const string CaskTap = "homebrew/cask";

/// <summary>
/// The tap name brew expects for a source: the built-in "Homebrew" and "Homebrew Cask" sources map to
/// homebrew/core and homebrew/cask; any other source is named after its tap already.
/// </summary>
internal static string GetTapName(IManagerSource source) => source.Name switch
{
"Homebrew" => CoreTap,
"Homebrew Cask" => CaskTap,
_ => source.Name,
};

public override string[] GetAddSourceParameters(IManagerSource source)
=> ["tap", source.Name, source.Url.ToString()];
{
string tap = GetTapName(source);
return tap == source.Name ? ["tap", tap, source.Url.ToString()] : ["tap", tap];
}

public override string[] GetRemoveSourceParameters(IManagerSource source)
=> ["untap", source.Name];
=> ["untap", GetTapName(source)];

protected override OperationVeredict _getAddSourceOperationVeredict(
IManagerSource source, int ReturnCode, string[] Output)
Expand Down
19 changes: 14 additions & 5 deletions src/UniGetUI.PackageEngine.Managers.Homebrew/Homebrew.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ public Homebrew()
InstallVerb = "install",
UpdateVerb = "upgrade",
UninstallVerb = "uninstall",
KnownSources =
[
new HomebrewSource(this, "Homebrew", new Uri("https://github.com/Homebrew/homebrew-core")),
new HomebrewSource(this, "Homebrew Cask", new Uri("https://github.com/Homebrew/homebrew-cask")),
],
KnownSources = CreateBuiltInSources(this, OperatingSystem.IsMacOS()),
DefaultSource = new HomebrewSource(this, "Homebrew", new Uri("https://github.com/Homebrew/homebrew-core")),
};

Expand All @@ -82,6 +78,19 @@ public Homebrew()
OperationHelper = new HomebrewPkgOperationHelper(this);
}

/// <summary>
/// The sources Homebrew serves from its API without a tap: formulae everywhere, casks on macOS
/// only (Homebrew on Linux has no casks).
/// </summary>
internal static IManagerSource[] CreateBuiltInSources(Homebrew manager, bool isMacOS)
{
var formulae = new HomebrewSource(manager, "Homebrew", new Uri("https://github.com/Homebrew/homebrew-core"));
if (!isMacOS)
return [formulae];

return [formulae, new HomebrewSource(manager, "Homebrew Cask", new Uri("https://github.com/Homebrew/homebrew-cask"))];
}

// ── Executable discovery ───────────────────────────────────────────────

public override IReadOnlyList<string> FindCandidateExecutableFiles()
Expand Down
68 changes: 68 additions & 0 deletions src/UniGetUI.PackageEngine.Tests/HomebrewManagerTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using UniGetUI.Core.Data;
using UniGetUI.Core.SettingsEngine;
using UniGetUI.PackageEngine.Classes.Manager;
using UniGetUI.PackageEngine.Interfaces;
using UniGetUI.PackageEngine.Managers.HomebrewManager;
using UniGetUI.PackageEngine.PackageClasses;
Expand Down Expand Up @@ -103,6 +104,73 @@ public void ParseAvailableUpdatesDetectsBothFormulaAndCaskUpdates()
);
}

// Issue #5219: on Homebrew 4 and later `brew tap` prints nothing for homebrew/core, so the
// sources page was empty and the default "Homebrew" source was reported as not configured.
[Fact]
public void SourcesListTheBuiltInSourcesWhenBrewTapPrintsNothing()
{
var manager = new Homebrew();
var helper = (HomebrewSourceHelper)manager.SourcesHelper;

IReadOnlyList<IManagerSource> sources = helper.BuildSourceList([]);

Assert.Equal(manager.Properties.KnownSources, sources);
Assert.Contains(sources, source => source.Name == "Homebrew");
}

[Fact]
public void SourcesListOtherTapsOnceAndSkipTheBuiltInTaps()
{
var manager = new Homebrew();
var helper = (HomebrewSourceHelper)manager.SourcesHelper;

IReadOnlyList<IManagerSource> sources = helper.BuildSourceList(
["homebrew/core", "hashicorp/tap", "", " ", "Homebrew/cask"]
);

Assert.Equal(manager.Properties.KnownSources.Length + 1, sources.Count);
Assert.Equal(manager.Properties.KnownSources, sources.Take(manager.Properties.KnownSources.Length));
IManagerSource tap = sources[^1];
Assert.Equal("hashicorp/tap", tap.Name);
Assert.Equal(new Uri("https://github.com/hashicorp/homebrew-tap"), tap.Url);
}

[Fact]
public void CasksAreABuiltInSourceOnMacOsOnly()
{
var manager = new Homebrew();

Assert.Equal(["Homebrew"], Homebrew.CreateBuiltInSources(manager, isMacOS: false).Select(s => s.Name));
Assert.Equal(
["Homebrew", "Homebrew Cask"],
Homebrew.CreateBuiltInSources(manager, isMacOS: true).Select(s => s.Name)
);
Assert.Equal(
OperatingSystem.IsMacOS() ? 2 : 1,
manager.Properties.KnownSources.Length
);
}

// brew rejects "Homebrew" and "Homebrew Cask" ("Error: Invalid tap name: 'Homebrew'"); the
// parameters must name the tap.
[Theory]
[InlineData("Homebrew", "https://github.com/Homebrew/homebrew-core", "tap homebrew/core", "untap homebrew/core")]
[InlineData("Homebrew Cask", "https://github.com/Homebrew/homebrew-cask", "tap homebrew/cask", "untap homebrew/cask")]
[InlineData(
"hashicorp/tap",
"https://github.com/hashicorp/homebrew-tap",
"tap hashicorp/tap https://github.com/hashicorp/homebrew-tap",
"untap hashicorp/tap"
)]
public void AddAndRemoveParametersUseTheTapName(string name, string url, string add, string remove)
{
var manager = new Homebrew();
var source = new ManagerSource(manager, name, new Uri(url));

Assert.Equal(add, string.Join(' ', manager.SourcesHelper.GetAddSourceParameters(source)));
Assert.Equal(remove, string.Join(' ', manager.SourcesHelper.GetRemoveSourceParameters(source)));
}

private static string[] ReadFixtureLines(string relativePath)
{
return PackageEngineFixtureFiles.ReadAllText(relativePath).Replace("\r\n", "\n").Split('\n');
Expand Down
Loading