Skip to content
Closed
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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
test:
name: Test
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read

Expand All @@ -32,3 +33,19 @@ jobs:
run: echo 'object W' > /tmp/W.scala && scala-cli compile /tmp/W.scala

- run: ./mill _.test

nix:
name: Validate flake
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31
with:
nix_path: nixpkgs=channel:nixos-unstable

- run: nix flake show
47 changes: 47 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ jobs:
permissions:
contents: write
id-token: write
pull-requests: write

steps:
- name: Download all artifacts
Expand All @@ -122,3 +123,49 @@ jobs:
with:
files: artifacts/*
generate_release_notes: true

- name: Save checksums before checkout
run: cp artifacts/checksums.txt /tmp/checksums.txt

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main

- name: Update flake.nix
run: |
VERSION="${GITHUB_REF_NAME#v}"

# Convert hex sha256 to Nix SRI format (sha256-<base64>)
sri_hash() {
echo -n "$1" | xxd -r -p | base64 | tr -d '\n' | sed 's/^/sha256-/'
}

LINUX_X86=$(sri_hash "$(grep "linux-x86_64.tar.gz" /tmp/checksums.txt | awk '{print $1}')")
LINUX_ARM=$(sri_hash "$(grep "linux-aarch64.tar.gz" /tmp/checksums.txt | awk '{print $1}')")
MACOS_ARM=$(sri_hash "$(grep "macos-arm64.tar.gz" /tmp/checksums.txt | awk '{print $1}')")
MACOS_X86=$(sri_hash "$(grep "macos-x86_64.tar.gz" /tmp/checksums.txt | awk '{print $1}')")

sed -i "s|version = \".*\";|version = \"${VERSION}\";|" flake.nix
sed -i "/linux-x86_64/{n;s|hash = \".*\";|hash = \"${LINUX_X86}\";|}" flake.nix
sed -i "/linux-aarch64/{n;s|hash = \".*\";|hash = \"${LINUX_ARM}\";|}" flake.nix
sed -i "/macos-x86_64/{n;s|hash = \".*\";|hash = \"${MACOS_X86}\";|}" flake.nix
sed -i "/macos-arm64/{n;s|hash = \".*\";|hash = \"${MACOS_ARM}\";|}" flake.nix

- name: Create PR for flake update
run: |
VERSION="${GITHUB_REF_NAME#v}"
BRANCH="update-flake-${VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add flake.nix
git diff --cached --quiet && echo "No changes" && exit 0
git checkout -b "${BRANCH}"
git commit -m "Update flake.nix to ${VERSION}"
git push -u origin "${BRANCH}"
gh pr create \
--title "Update flake.nix to ${VERSION}" \
--body "Automated update of flake.nix hashes for release v${VERSION}." \
--base main \
--head "${BRANCH}"
env:
GH_TOKEN: ${{ github.token }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ langoustine-tracer
.claude
ai
.cellar
result
5 changes: 5 additions & 0 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Query any published artifact by explicit coordinate (`group:artifact:version`):
cellar deps <coordinate> # dependency tree

- Coordinates must be explicit: `group:artifact_3:version` (no `::` shorthand)
- For sbt plugins, use the full Scala and sbt suffix: `group:artifact_2.12_1.0:version` (e.g. `org.scala-native:sbt-scala-native_2.12_1.0:latest`)
- For compiler plugins and other artifacts with full Scala version suffixes, use the full version: `group:artifact_3.3.8:version`
- Use `latest` as the version to resolve the most recent release
- `-r`, `--repository <url>`: extra Maven repository (repeatable)

Expand Down Expand Up @@ -63,6 +65,9 @@ cellar get-source org.typelevel:cats-core_3:2.10.0 cats.Monad
# Dependency tree
cellar deps org.typelevel:cats-effect_3:3.5.4

# sbt plugin (use full Scala + sbt suffix)
cellar deps org.scala-native:sbt-scala-native_2.12_1.0:latest

# Project-aware (from a Mill project root)
cellar get --module lib cats.Monad
cellar list --module core cats
Expand Down
5 changes: 3 additions & 2 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ trait CommonScalaModule extends ScalaModule with ScalafixModule {
object lib extends CommonScalaModule {
def mvnDeps = Seq(
mvn"org.typelevel::cats-effect:3.5.7",
mvn"co.fs2::fs2-core:3.11.0",
mvn"co.fs2::fs2-io:3.11.0",
mvn"io.get-coursier:interface:1.0.28",
mvn"ch.epfl.scala::tasty-query:1.7.0",
mvn"org.scala-lang:scala3-tasty-inspector_3:3.8.1"
mvn"org.scala-lang:scala3-tasty-inspector_3:3.8.1",
mvn"com.github.pureconfig::pureconfig-core:0.17.10"
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
{"pattern": "\\Qcoursier/coursier.properties\\E"},
{"pattern": "\\Qcoursier/launcher/coursier.properties\\E"},
{"pattern": "\\Qlibrary.properties\\E"},
{"pattern": "\\Qjre.bin\\E"}
{"pattern": "\\Qjre.bin\\E"},
{"pattern": "\\Qreference.conf\\E"}
]
}
}
21 changes: 13 additions & 8 deletions cli/src/cellar/cli/CellarApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cellar.handlers.{DepsHandler, GetHandler, GetSourceHandler, ListHandler,
import com.monovore.decline.*
import com.monovore.decline.effect.*
import coursierapi.{MavenRepository, Repository}
import java.nio.file.Path
import fs2.io.file.Path

object CellarApp
extends CommandIOApp(
Expand All @@ -23,6 +23,8 @@ object CellarApp
searchSubcmd orElse searchExternalSubcmd orElse
depsSubcmd

private given Argument[Path] = Argument[java.nio.file.Path].map(Path.fromNioPath)

private val coordArg: Opts[String] =
Opts.argument[String]("coordinate")

Expand All @@ -48,30 +50,33 @@ object CellarApp
private val noCacheOpt: Opts[Boolean] =
Opts.flag("no-cache", "Skip classpath cache (re-extract from build tool)").orFalse

private val configOpt: Opts[IO[Config]] =
Opts.option[Path]("config", "Path to config file", "c").orNone.map(Config.load)

private def parseAndResolve(raw: String, extraRepos: List[Repository]): IO[Either[String, MavenCoordinate]] =
MavenCoordinate.parse(raw) match
case Left(err) => IO.pure(Left(err))
case Right(coord) => coord.resolveLatest(extraRepos).map(Right(_))

private val getSubcmd: Opts[IO[ExitCode]] =
Opts.subcommand("get", "Fetch symbol info from the current project") {
(symbolArg, moduleOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, javaHome, noCache) =>
ProjectGetHandler.run(fqn, module, javaHome, noCache)
(symbolArg, moduleOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, configIO, javaHome, noCache) =>
configIO.flatMap(ProjectGetHandler.run(fqn, module, _, javaHome, noCache))
}
}

private val listSubcmd: Opts[IO[ExitCode]] =
Opts.subcommand("list", "List symbols in a package or class from the current project") {
(symbolArg, moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, javaHome, noCache) =>
ProjectListHandler.run(fqn, module, limit, javaHome, noCache)
(symbolArg, moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN { (fqn, module, limit, configIO, javaHome, noCache) =>
configIO.flatMap(ProjectListHandler.run(fqn, module, limit, _, javaHome, noCache))
}
}

private val searchSubcmd: Opts[IO[ExitCode]] =
Opts.subcommand("search", "Substring search for symbol names in the current project") {
(Opts.argument[String]("query"), moduleOpt, limitOpt, javaHomeOpt, noCacheOpt).mapN {
(query, module, limit, javaHome, noCache) =>
ProjectSearchHandler.run(query, module, limit, javaHome, noCache)
(Opts.argument[String]("query"), moduleOpt, limitOpt, configOpt, javaHomeOpt, noCacheOpt).mapN {
(query, module, limit, configIO, javaHome, noCache) =>
configIO.flatMap(ProjectSearchHandler.run(query, module, limit, _, javaHome, noCache))
}
}

Expand Down
27 changes: 27 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
{
description = "Look up the public API of any JVM dependency from the terminal";

inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

outputs = { self, nixpkgs }:
let
version = "0.1.0-M4";

# Per-platform release artifact metadata
platforms = {
x86_64-linux = {
archive = "cellar-${version}-linux-x86_64.tar.gz";
hash = "sha256-w5MXP2a19HxJhN27a648zODMqEsVN/WKb5Y+5p2zyxY=";
};
aarch64-linux = {
archive = "cellar-${version}-linux-aarch64.tar.gz";
hash = "sha256-3c3cXX1bzIPvDNQOh9ujPoyW81XGVvrW9OAXEe69gc4=";
};
x86_64-darwin = {
archive = "cellar-${version}-macos-x86_64.tar.gz";
hash = "sha256-+zrj/SCv6cdos6+vZWXe0hQD7rpCPjHujpTBdSHMsCc=";
};
aarch64-darwin = {
archive = "cellar-${version}-macos-arm64.tar.gz";
hash = "sha256-fEHoxWJKJwXhqCEfF1ZH8wsZDpHKDG05ao3fqLp257I=";
};
};

eachSystem = nixpkgs.lib.genAttrs (builtins.attrNames platforms);
in
{
packages = eachSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
meta = platforms.${system};
src = pkgs.fetchurl {
url = "https://github.com/VirtusLab/cellar/releases/download/v${version}/${meta.archive}";
hash = meta.hash;
};
in
{
default = pkgs.stdenv.mkDerivation {
pname = "cellar";
inherit version src;

sourceRoot = ".";

nativeBuildInputs = pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
pkgs.autoPatchelfHook
pkgs.glibc
];

unpackPhase = ''
tar xzf $src
'';

installPhase = ''
mkdir -p $out/bin
cp cellar $out/bin/cellar
chmod +x $out/bin/cellar
'';

meta = with pkgs.lib; {
description = "Look up the public API of any JVM dependency from the terminal";
homepage = "https://github.com/VirtusLab/cellar";
license = licenses.mpl20;
platforms = [ system ];
mainProgram = "cellar";
};
};

# Install a locally-built binary into the nix profile.
# Usage: ./mill cli.nativeImage && nix profile install --impure .#dev
dev = let
binary = builtins.path {
path = builtins.toPath "${builtins.getEnv "PWD"}/out/cli/nativeImage.dest/native-executable";
name = "cellar-native-executable";
};
in pkgs.stdenv.mkDerivation {
pname = "cellar";
version = "dev";
dontUnpack = true;

installPhase = ''
mkdir -p $out/bin
cp ${binary} $out/bin/cellar
chmod +x $out/bin/cellar
'';

meta = with pkgs.lib; {
description = "Look up the public API of any JVM dependency from the terminal (dev build)";
homepage = "https://github.com/VirtusLab/cellar";
license = licenses.mpl20;
platforms = [ system ];
mainProgram = "cellar";
};
};
}
);

overlays.default = final: prev: {
cellar = self.packages.${final.system}.default;
};
};
}
15 changes: 15 additions & 0 deletions lib/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
mill {
# When running mill, which binary to invoke
binary = "./mill"
binary = ${?CELLAR_MILL_BINARY}
}

sbt {
# When running sbt, which binary to invoke (sbt or sbtn)
binary = "sbt"
binary = ${?CELLAR_SBT_BINARY}
# Extra arguments to pass to sbt, for example to run in client mode (--client)
# Several arguments can be passed by separating them with spaces
extra-args = ""
extra-args = ${?CELLAR_SBT_EXTRA_ARGS}
}
2 changes: 1 addition & 1 deletion lib/src/cellar/CellarError.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cellar

import cellar.build.BuildToolKind
import java.nio.file.Path
import fs2.io.file.Path

sealed trait CellarError extends Throwable

Expand Down
35 changes: 35 additions & 0 deletions lib/src/cellar/Config.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package cellar

import cats.effect.IO
import cats.syntax.all.*
import fs2.io.file.{Files, Path}
import pureconfig.*

case class MillConfig(binary: String) derives ConfigReader

case class SbtConfig(binary: String, extraArgs: String) derives ConfigReader {
def effectiveExtraArgs: List[String] = extraArgs.split("\\s+").filter(_.nonEmpty).toList
}

case class Config(mill: MillConfig, sbt: SbtConfig) derives ConfigReader

object Config {
lazy val default: IO[Config] = load(None)

val defaultUserPath: Option[Path] =
sys.props.get("user.home").map(Path(_).resolve(".cellar").resolve("cellar.conf"))
val defaultProjectPath: Path = Path(".cellar").resolve("cellar.conf")

def load(path: Option[Path]): IO[Config] = {
def load0(path: List[Path]) =
IO.blocking {
path.foldLeft(ConfigSource.default)((cs, p) => ConfigSource.file(p.toNioPath).withFallback(cs)).loadOrThrow[Config]
}

path match
case sp: Some[_] => load0(sp.toList)
case None =>
val relevantPaths = defaultUserPath.toList ++ List(defaultProjectPath)
relevantPaths.filterA(p => Files[IO].exists(p)).flatMap(load0)
}
}
Loading
Loading