generateConfigurableHomebrewFormula function

String generateConfigurableHomebrewFormula({
  1. required HomebrewFormulaConfig config,
  2. required List<HomebrewArtifact> artifacts,
})

Renders a product-neutral Formula for the supplied artifacts.

Only the platforms and architectures present in artifacts are rendered, so a product that does not ship, say, a Linux arm64 build produces a Formula that simply does not claim to support it.

Implementation

String generateConfigurableHomebrewFormula({
  required HomebrewFormulaConfig config,
  required List<HomebrewArtifact> artifacts,
}) {
  if (artifacts.isEmpty) {
    throw const ShipworldException(
      'A Homebrew Formula needs at least one artifact',
      code: 'invalid_config',
    );
  }

  HomebrewArtifact? find(String platform, String architecture) {
    for (final item in artifacts) {
      if (item.platform == platform && item.architecture == architecture) {
        return item;
      }
    }
    return null;
  }

  /// Renders `on_macos`/`on_linux` with only the architectures that exist.
  String? platformBlock(String platform) {
    final arm = find(platform, 'arm64');
    final intel = find(platform, 'x64');
    if (arm == null && intel == null) return null;
    final blocks = <String>[
      // Homebrew's DSL names them by CPU family, not by the architecture
      // strings this package uses everywhere else.
      if (intel != null)
        '''
    on_intel do
      url "${intel.url}"
      sha256 "${intel.sha256}"
    end''',
      if (arm != null)
        '''
    on_arm do
      url "${arm.url}"
      sha256 "${arm.sha256}"
    end''',
    ];
    return '  on_$platform do\n${blocks.join('\n')}\n  end';
  }

  final platforms = <String>[
    for (final platform in const ['macos', 'linux']) ?platformBlock(platform),
  ];
  final kegOnly = config.versioned ? '\n  keg_only :versioned_formula\n' : '';
  // Homebrew strips the single top-level directory of the archive, so the
  // staging directory holds the bundle's own `bin/` and `lib/`. The launcher
  // is symlinked rather than copied because its RPATH is relative to the real
  // path of the executable, which keeps `lib/` reachable through the link.
  final install = switch (config.payload) {
    PayloadKind.directory =>
      '''
    libexec.install Dir["*"]
    bin.install_symlink libexec/"bin/${config.executableName}"''',
    PayloadKind.executable => _executableInstall(config, find),
  };

  return '''
class ${config.className} < Formula
  desc "${config.description}"
  homepage "${config.homepage}"
  version "${config.version}"$kegOnly

${platforms.join('\n\n')}

  def install
$install
  end

  test do
    system "#{bin}/${config.executableName}", "--version"
  end
end
''';
}