Skip to content
Merged
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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ Commands:
These options are supported by all commands:

```
--config=<configFile>
Path to user configuration file (default: JPM_CONFIG
environment variable, ~/.config/jpm/config.yml, or
~/.jpmcfg.yml)
-c, --cache=<cacheDir>
Directory where downloaded artifacts will be cached
(default: value of JPM_CACHE environment variable;
Expand Down Expand Up @@ -535,6 +539,49 @@ actions:
clean: "rm -f *.class"
```

## Configuration File

`jpm` supports user-level configuration files to set default values for common command-line options. This allows you to configure preferences once instead of passing the same options repeatedly.

### Configuration File Locations

Configuration files are searched in the following order:

1. **Explicit path** - Specified via `--config` option or `JPM_CONFIG` environment variable
2. **`~/.config/jpm/config.yml`** - XDG Base Directory standard location (primary)
3. **`~/.jpmcfg.yml`** - Fallback location (home directory)

The first file found is used. If no configuration file exists, jpm uses built-in defaults.

### Configuration File Format

```yaml
config:
cache: ~/my-jpm-cache
directory: libs
no-links: false
repositories:
myrepo: https://my.repo.com/maven2
private: https://private.repo.com/releases
```

### Configuration Options

- **`cache`** - Directory for caching downloaded artifacts (equivalent to `--cache` option)
- **`directory`** - Default directory to copy artifacts to (equivalent to `--directory` option)
- **`no-links`** - Whether to copy files instead of creating symlinks (equivalent to `--no-links` option)
- **`repositories`** - Map of repository names to URLs (merged with `--repo` options)

### Path Expansion

Paths in the configuration file support home directory expansion:

```yaml
config:
cache: ~/jpm-cache # Expands to /home/user/jpm-cache
directory: ~/.local/lib/jpm # Expands to /home/user/.local/lib/jpm
```

## Development

To build the project simply run:
Expand Down
143 changes: 117 additions & 26 deletions src/main/java/org/codejive/jpm/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
//DEPS org.yaml:snakeyaml:2.5
//DEPS org.jline:jline-console-ui:3.30.6 org.jline:jline-terminal-jni:3.30.6
//DEPS org.slf4j:slf4j-api:2.0.17 org.slf4j:slf4j-simple:2.0.17
//SOURCES Jpm.java config/AppInfo.java search/Search.java search/SearchSmoRestImpl.java search/SearchSmoApiImpl.java
//SOURCES Jpm.java config/AppInfo.java config/UserConfig.java
//SOURCES search/Search.java search/SearchSolrRestImpl.java search/SearchSmoApiImpl.java
//SOURCES util/CommandsParser.java util/FileUtils.java util/Resolver.java util/ScriptUtils.java util/SyncResult.java
//SOURCES util/Version.java
// spotless:on
Expand All @@ -20,6 +21,7 @@
import java.util.*;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import org.codejive.jpm.config.UserConfig;
import org.codejive.jpm.search.Search.Backends;
import org.codejive.jpm.util.SyncResult;
import org.codejive.jpm.util.Version;
Expand Down Expand Up @@ -63,8 +65,10 @@
public class Main {

static boolean verbose = false;
static Path configFile = null;

@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;

@Command(
name = "copy",
Expand All @@ -75,6 +79,7 @@ public class Main {
+ "Example:\n jpm copy org.apache.httpcomponents:httpclient:4.5.14\n")
static class Copy implements Callable<Integer> {
@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;
@Mixin QuietMixin quietMixin;
@Mixin ArtifactsMixin artifactsMixin;

Expand All @@ -89,8 +94,8 @@ static class Copy implements Callable<Integer> {
public Integer call() throws Exception {
SyncResult stats =
Jpm.builder()
.directory(artifactsMixin.directory)
.noLinks(artifactsMixin.noLinks)
.directory(artifactsMixin.getDirectory())
.noLinks(artifactsMixin.getNoLinks())
.cacheDir(artifactsMixin.getCacheDir())
.build()
.copy(
Expand All @@ -115,6 +120,7 @@ public Integer call() throws Exception {
+ "Example:\n jpm search httpclient\n")
static class Search implements Callable<Integer> {
@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;
@Mixin QuietMixin quietMixin;
@Mixin DepsMixin depsMixin;
@Mixin AppInfoFileMixin appInfoFileMixin;
Expand Down Expand Up @@ -162,8 +168,8 @@ public Integer call() throws Exception {
if ("install".equals(artifactAction)) {
SyncResult stats =
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.directory(depsMixin.getDirectory())
.noLinks(depsMixin.getNoLinks())
.cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
Expand All @@ -176,8 +182,8 @@ public Integer call() throws Exception {
} else if ("copy".equals(artifactAction)) {
SyncResult stats =
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.directory(depsMixin.getDirectory())
.noLinks(depsMixin.getNoLinks())
.cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
Expand Down Expand Up @@ -218,8 +224,8 @@ public Integer call() throws Exception {
String[] search(String artifactPattern) {
try {
return Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.directory(depsMixin.getDirectory())
.noLinks(depsMixin.getNoLinks())
.cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
Expand Down Expand Up @@ -311,6 +317,7 @@ private static String getSelectedId(
+ "Example:\n jpm install org.apache.httpcomponents:httpclient:4.5.14\n")
static class Install implements Callable<Integer> {
@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;
@Mixin QuietMixin quietMixin;
@Mixin OptionalArtifactsMixin optionalArtifactsMixin;
@Mixin AppInfoFileMixin appInfoFileMixin;
Expand All @@ -319,8 +326,8 @@ static class Install implements Callable<Integer> {
public Integer call() throws Exception {
SyncResult stats =
Jpm.builder()
.directory(optionalArtifactsMixin.directory)
.noLinks(optionalArtifactsMixin.noLinks)
.directory(optionalArtifactsMixin.getDirectory())
.noLinks(optionalArtifactsMixin.getNoLinks())
.cacheDir(optionalArtifactsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
Expand All @@ -343,15 +350,16 @@ public Integer call() throws Exception {
+ "Example:\n jpm path org.apache.httpcomponents:httpclient:4.5.14\n")
static class PrintPath implements Callable<Integer> {
@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;
@Mixin OptionalArtifactsMixin optionalArtifactsMixin;
@Mixin AppInfoFileMixin appInfoFileMixin;

@Override
public Integer call() throws Exception {
List<Path> files =
Jpm.builder()
.directory(optionalArtifactsMixin.directory)
.noLinks(optionalArtifactsMixin.noLinks)
.directory(optionalArtifactsMixin.getDirectory())
.noLinks(optionalArtifactsMixin.getNoLinks())
.cacheDir(optionalArtifactsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
Expand Down Expand Up @@ -396,6 +404,7 @@ public Integer call() throws Exception {
+ " jpm exec @kotlinc -cp {{deps}} -d out/classes src/main/kotlin/App.kt\n")
static class Exec implements Callable<Integer> {
@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;
@Mixin DepsMixin depsMixin;
@Mixin QuietMixin quietMixin;
@Mixin AppInfoFileMixin appInfoFileMixin;
Expand All @@ -408,8 +417,8 @@ public Integer call() throws Exception {
String cmd = String.join(" ", command);
try {
return Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.directory(depsMixin.getDirectory())
.noLinks(depsMixin.getNoLinks())
.cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.verbose(!quietMixin.quiet)
Expand All @@ -436,6 +445,7 @@ public Integer call() throws Exception {
+ " jpm do build -a --fresh test -a verbose\n")
static class Do implements Callable<Integer> {
@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;
@Mixin DepsMixin depsMixin;
@Mixin QuietMixin quietMixin;
@Mixin AppInfoFileMixin appInfoFileMixin;
Expand Down Expand Up @@ -467,8 +477,8 @@ public Integer call() throws Exception {
if (list) {
List<String> actionNames =
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.directory(depsMixin.getDirectory())
.noLinks(depsMixin.getNoLinks())
.cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
Expand Down Expand Up @@ -513,8 +523,8 @@ public Integer call() throws Exception {
}
int exitCode =
Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.directory(depsMixin.getDirectory())
.noLinks(depsMixin.getNoLinks())
.cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.verbose(!quietMixin.quiet)
Expand All @@ -535,6 +545,7 @@ public Integer call() throws Exception {

abstract static class DoAlias implements Callable<Integer> {
@Mixin VerboseMixin verboseMixin;
@Mixin ConfigMixin configMixin;
@Mixin DepsMixin depsMixin;
@Mixin AppInfoFileMixin appInfoFileMixin;

Expand All @@ -547,8 +558,8 @@ public Integer call() throws Exception {
try {
// Use only unmatched args for pass-through to preserve ordering
return Jpm.builder()
.directory(depsMixin.directory)
.noLinks(depsMixin.noLinks)
.directory(depsMixin.getDirectory())
.noLinks(depsMixin.getNoLinks())
.cacheDir(depsMixin.getCacheDir())
.appFile(appInfoFileMixin.appInfoFile)
.build()
Expand Down Expand Up @@ -601,17 +612,19 @@ String actionName() {
}

static class DepsMixin {
// Cached user configuration loaded from ~/.config/jpm/config.yml or ~/.jpmcfg.yml
private transient UserConfig userConfig;

@Option(
names = {"-d", "--directory"},
description = "Directory to copy artifacts to",
defaultValue = "deps")
description = "Directory to copy artifacts to (default: 'deps')")
Path directory;

@Option(
names = {"-L", "--no-links"},
description = "Always copy artifacts, don't try to create symlinks",
defaultValue = "false")
boolean noLinks;
description =
"Always copy artifacts, don't try to create symlinks (default: false)")
Boolean noLinks;

@Option(
names = {"-r", "--repo"},
Expand All @@ -625,10 +638,33 @@ static class DepsMixin {
"Directory where downloaded artifacts will be cached (default: value of JPM_CACHE environment variable; whatever is set in Maven's settings.xml or $HOME/.m2/repository")
Path cacheDir;

/**
* Loads and caches the user configuration. Priority: --config option > JPM_CONFIG env var >
* ~/.config/jpm/config.yml > ~/.jpmcfg.yml.
*
* @return The user configuration (never null)
*/
UserConfig getUserConfig() {
if (userConfig == null) {
userConfig = UserConfig.read(Main.configFile);
}
return userConfig;
}

/**
* Returns the cache directory to use. Priority: CLI option > UserConfig > JPM_CACHE env var
* > Maven default.
*
* @return The cache directory path or null to use Maven default
*/
Path getCacheDir() {
if (cacheDir != null) {
return cacheDir;
}
Path userConfigCache = getUserConfig().cache();
if (userConfigCache != null) {
return userConfigCache;
}
String envCache = System.getenv("JPM_CACHE");
if (envCache != null && !envCache.isEmpty()) {
try {
Expand All @@ -642,8 +678,53 @@ Path getCacheDir() {
return null;
}

/**
* Returns the directory to use for artifacts. Priority: CLI option > UserConfig > hardcoded
* default ('deps').
*
* @return The directory path
*/
Path getDirectory() {
if (directory != null) {
return directory; // User explicitly set via CLI
}
Path userConfigDir = getUserConfig().directory();
if (userConfigDir != null) {
return userConfigDir; // From user config
}
return Path.of("deps"); // Hardcoded default
}

/**
* Returns whether to disable symlinks. Priority: CLI option > UserConfig > hardcoded
* default (false).
*
* @return true to disable symlinks, false otherwise
*/
boolean getNoLinks() {
if (noLinks != null) {
return noLinks; // User explicitly set via CLI
}
Boolean userConfigNoLinks = getUserConfig().noLinks();
if (userConfigNoLinks != null) {
return userConfigNoLinks; // From user config
}
return false; // Hardcoded default
}

/**
* Returns the repository map. Priority: UserConfig repositories (base) + CLI repositories
* (override).
*
* @return Map of repository name to URL
*/
Map<String, String> getRepositoryMap() {
Map<String, String> repoMap = new HashMap<>();

// Start with UserConfig repositories (lowest priority)
repoMap.putAll(getUserConfig().repositories());

// Add/override with CLI repositories
for (String repo : repositories) {
String name;
String url;
Expand Down Expand Up @@ -705,6 +786,16 @@ public void setVerbose(boolean verbose) {
}
}

static class ConfigMixin {
@Option(
names = {"--config"},
description =
"Path to user configuration file (default: JPM_CONFIG environment variable, ~/.config/jpm/config.yml, or ~/.jpmcfg.yml)")
public void setConfigFile(Path config) {
Main.configFile = config;
}
}

static class QuietMixin {
@Option(
names = {"-q", "--quiet"},
Expand Down
Loading