feat: 初步实现 init 项目等项目生命周期工具

This commit is contained in:
Luke 2025-06-19 18:05:01 +08:00
parent 002b51b71c
commit 4c0522c067
23 changed files with 1270 additions and 47 deletions

View File

@ -1,14 +1,12 @@
package org.jcnc.snow.cli; package org.jcnc.snow.cli;
/** /**
* 所有 CLI 子命令 compilerun 都必须实现的命令接口
* <p> * <p>
* 所有子命令 compilerun 都必须实现的命令接口 * 实现类应为无状态stateless线程安全thread-safe
* 可通过 {@link java.util.ServiceLoader ServiceLoader} 自动发现
* 或直接在 {@link SnowCLI} 中注册
* </p> * </p>
* <ul>
* <li>实现类应当是无状态stateless线程安全thread-safe</li>
* <li>可通过 {@link java.util.ServiceLoader ServiceLoader} 自动发现或直接在 {@link SnowCLI} 注册</li>
* </ul>
* <p>
*/ */
public interface CLICommand { public interface CLICommand {
@ -28,16 +26,19 @@ public interface CLICommand {
/** /**
* 打印命令的专用 usage 信息可选实现 * 打印命令的专用 usage 信息可选实现
* <p>
* 可覆盖此方法自定义帮助信息默认无操作 * 可覆盖此方法自定义帮助信息默认无操作
* </p>
*/ */
default void printUsage() { default void printUsage() {
// 默认实现为空可由子类覆盖
} }
/** /**
* 执行命令逻辑 * 执行命令逻辑
* *
* @param args 传递给子命令的参数不含命令名本身 * @param args 传递给子命令的参数不含命令名本身
* @return 进程退出码0 为成功0为错误 * @return 进程退出码0 为成功 0 为错误
* @throws Exception 可抛出任意异常框架会统一捕获和输出 * @throws Exception 可抛出任意异常框架会统一捕获和输出
*/ */
int execute(String[] args) throws Exception; int execute(String[] args) throws Exception;

View File

@ -32,7 +32,12 @@ public class SnowCLI {
private static final Map<String, Supplier<CLICommand>> COMMANDS = Map.of( private static final Map<String, Supplier<CLICommand>> COMMANDS = Map.of(
"compile", CompileCommand::new, "compile", CompileCommand::new,
"run", RunCommand::new, "run", RunCommand::new,
"version", VersionCommand::new "version", VersionCommand::new,
"init", org.jcnc.snow.cli.commands.InitCommand::new,
"build", org.jcnc.snow.cli.commands.BuildCommand::new,
"install", org.jcnc.snow.cli.commands.InstallCommand::new,
"publish", org.jcnc.snow.cli.commands.PublishCommand::new,
"clean", org.jcnc.snow.cli.commands.CleanCommand::new
); );
/** /**
@ -89,4 +94,4 @@ public class SnowCLI {
System.exit(1); System.exit(1);
} }
} }
} }

View File

@ -0,0 +1,86 @@
package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.pkg.dsl.CloudDSLParser;
import org.jcnc.snow.pkg.lifecycle.LifecycleManager;
import org.jcnc.snow.pkg.lifecycle.LifecyclePhase;
import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.pkg.resolver.DependencyResolver;
import org.jcnc.snow.pkg.tasks.CompileTask;
import org.jcnc.snow.pkg.tasks.PackageTask;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* CLI 命令构建当前项目包含依赖解析编译打包
* <p>
* 该命令会依次执行依赖解析源码编译和产物打包阶段
* </p>
*
* <pre>
* 用法示例
* $ snow build
* </pre>
*/
public final class BuildCommand implements CLICommand {
/**
* 返回命令名称用于 CLI 调用
*
* @return 命令名称 "build"
*/
@Override
public String name() {
return "build";
}
/**
* 返回命令简介用于 CLI 帮助或命令列表展示
*
* @return 命令描述字符串
*/
@Override
public String description() {
return "Build the current project by resolving dependencies, compiling, and packaging in sequence.";
}
/**
* 打印命令用法信息
*/
@Override
public void printUsage() {
System.out.println("Usage: snow build ");
}
/**
* 执行项目构建流程
* <ul>
* <li>解析项目描述文件project.cloud</li>
* <li>依赖解析RESOLVE_DEPENDENCIES</li>
* <li>源码编译COMPILE</li>
* <li>产物打包PACKAGE</li>
* </ul>
*
* @param args CLI 传入的参数数组
* @return 执行结果码0 表示成功
* @throws Exception 执行过程中出现错误时抛出
*/
@Override
public int execute(String[] args) throws Exception {
Path dslFile = Paths.get("project.cloud");
Project project = CloudDSLParser.parse(dslFile);
DependencyResolver resolver = new DependencyResolver(Paths.get(System.getProperty("user.home"), ".snow", "cache"));
LifecycleManager lm = new LifecycleManager();
// 注册各阶段任务
lm.register(LifecyclePhase.RESOLVE_DEPENDENCIES, () -> resolver.resolve(project));
lm.register(LifecyclePhase.COMPILE, new CompileTask(project));
lm.register(LifecyclePhase.PACKAGE, new PackageTask(project));
lm.executeAll();
return 0;
}
}

View File

@ -0,0 +1,63 @@
package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.pkg.lifecycle.LifecycleManager;
import org.jcnc.snow.pkg.lifecycle.LifecyclePhase;
import org.jcnc.snow.pkg.tasks.CleanTask;
/**
* CLI 命令清理构建输出和本地缓存目录
* <p>
* 用于清除项目生成的 builddist 等中间产物保持工作区整洁
* </p>
*
* <pre>
* 用法示例
* $ snow clean
* </pre>
*/
public final class CleanCommand implements CLICommand {
/**
* 返回命令名称用于 CLI 调用
*
* @return 命令名称 "clean"
*/
@Override
public String name() {
return "clean";
}
/**
* 返回命令简介用于 CLI 帮助或命令列表展示
*
* @return 命令描述字符串
*/
@Override
public String description() {
return "Clean build outputs and local cache, remove intermediate artifacts, and free disk space.";
}
/**
* 打印命令用法信息
*/
@Override
public void printUsage() {
System.out.println("Usage: snow clean ");
}
/**
* 执行清理任务
*
* @param args CLI 传入的参数数组
* @return 执行结果码0 表示成功
* @throws Exception 执行过程中出现错误时抛出
*/
@Override
public int execute(String[] args) throws Exception {
LifecycleManager lm = new LifecycleManager();
lm.register(LifecyclePhase.CLEAN, new CleanTask());
lm.executeAll();
return 0;
}
}

View File

@ -24,20 +24,17 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
/** /**
* CLI 命令 .snow 源文件编译为 VM 字节码.water 文件
* <p> * <p>
* 编译器子命令<code>snow compile</code><br> * 支持递归目录多文件编译可选编译后立即运行<br>
* 将一个或多个 .snow 源文件编译为 VM 字节码文件.water * 命令参数支持 run-o-d
* </p> * </p>
* *
* <ul> * <pre>
* <li>-o 指定输出基名去掉 .water 后缀</li> * 用法示例
* <li>-d 递归目录编译输出名自动取目录名</li> * $ snow compile [run] [-o &lt;name&gt;] [-d &lt;srcDir&gt;] [file1.snow file2.snow ]
* <li>run 子命令编译完成立即运行 VM</li>
* </ul>
*
* <pre>用法
* snow compile [run] [-o &lt;name&gt;] [-d &lt;srcDir&gt;] [file1.snow file2.snow ]
* </pre> * </pre>
*/ */
public final class CompileCommand implements CLICommand { public final class CompileCommand implements CLICommand {

View File

@ -0,0 +1,78 @@
package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* CLI 命令创建新的项目骨架
* <p>
* 用于快速初始化标准目录结构和 DSL 配置文件project.cloud
* </p>
*
* <pre>
* 用法示例
* $ snow init
* </pre>
*/
public final class InitCommand implements CLICommand {
/**
* 返回命令名称用于 CLI 调用
*
* @return 命令名称 "init"
*/
@Override
public String name() {
return "init";
}
/**
* 返回命令简介用于 CLI 帮助或命令列表展示
*
* @return 命令描述字符串
*/
@Override
public String description() {
return "Initialize a new project skeleton with directory structure and project.cloud file.";
}
/**
* 打印命令用法信息
*/
@Override
public void printUsage() {
System.out.println("Usage: snow init [--lang <lang>]");
}
/**
* 执行项目初始化流程创建 src 目录和 DSL 配置文件
*
* @param args CLI 传入的参数数组
* @return 执行结果码0 表示成功
* @throws Exception 文件创建过程中出现错误时抛出
*/
@Override
public int execute(String[] args) throws Exception {
// 生成 skeleton `.cloud` 文件和 src 目录
Path dir = Paths.get(".").toAbsolutePath();
Files.createDirectories(dir.resolve("src"));
Path dsl = dir.resolve("project.cloud");
if (Files.notExists(dsl)) {
Files.writeString(dsl, """
# Generated by snow init
project {
group = "com.example"
artifact = "demo-app"
version = "0.0.1-SNAPSHOT"
}
""");
System.out.println("[init] created " + dsl);
} else {
System.out.println("[init] project.cloud already exists");
}
return 0;
}
}

View File

@ -0,0 +1,65 @@
package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.pkg.dsl.CloudDSLParser;
import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.pkg.resolver.DependencyResolver;
import java.nio.file.Paths;
/**
* CLI 命令解析并下载项目依赖到本地缓存
* <p>
* 适用于离线使用和依赖预热场景会自动读取项目描述文件并处理依赖缓存
* </p>
*
* <pre>
* 用法示例
* $ snow install
* </pre>
*/
public final class InstallCommand implements CLICommand {
/**
* 返回命令名称用于 CLI 调用
*
* @return 命令名称 "install"
*/
@Override
public String name() {
return "install";
}
/**
* 返回命令简介用于 CLI 帮助或命令列表展示
*
* @return 命令描述字符串
*/
@Override
public String description() {
return "Resolve and download project dependencies to local cache for offline development or faster builds.";
}
/**
* 打印命令用法信息
*/
@Override
public void printUsage() {
System.out.println("Usage: snow install ");
}
/**
* 执行依赖解析和下载任务
*
* @param args CLI 传入的参数数组
* @return 执行结果码0 表示成功
* @throws Exception 解析或下载依赖过程中出现错误时抛出
*/
@Override
public int execute(String[] args) throws Exception {
Project project = CloudDSLParser.parse(Paths.get("project.cloud"));
DependencyResolver resolver = new DependencyResolver(Paths.get(System.getProperty("user.home"), ".snow", "cache"));
resolver.resolve(project);
return 0;
}
}

View File

@ -0,0 +1,74 @@
package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.pkg.dsl.CloudDSLParser;
import org.jcnc.snow.pkg.lifecycle.LifecycleManager;
import org.jcnc.snow.pkg.lifecycle.LifecyclePhase;
import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.pkg.tasks.PublishTask;
import java.nio.file.Paths;
/**
* CLI 命令将已构建的项目包发布到远程仓库
* <p>
* 用于持续集成交付或分发场景
* 支持自动读取 DSL 项目描述文件并注册和执行发布生命周期阶段的任务
* </p>
*
* <pre>
* 用法示例
* $ snow publish
* </pre>
*/
public final class PublishCommand implements CLICommand {
/**
* 返回该命令的名称用于 CLI 调用
*
* @return 命令名称字符串 "publish"
*/
@Override
public String name() {
return "publish";
}
/**
* 返回命令简介用于 CLI 帮助或命令列表展示
*
* @return 命令描述字符串
*/
@Override
public String description() {
return "Publish the built package to a remote repository, suitable for continuous integration, delivery, or project distribution.";
}
/**
* 打印命令用法信息供终端用户参考
*/
@Override
public void printUsage() {
System.out.println("Usage: snow publish ");
}
/**
* 执行发布命令
* <ul>
* <li>解析项目描述文件 project.cloud</li>
* <li>注册并执行 PUBLISH 阶段的任务</li>
* </ul>
*
* @param args CLI 传入的参数数组
* @return 执行结果码0表示成功
* @throws Exception 执行过程中出现错误时抛出
*/
@Override
public int execute(String[] args) throws Exception {
Project project = CloudDSLParser.parse(Paths.get("project.cloud"));
LifecycleManager lm = new LifecycleManager();
lm.register(LifecyclePhase.PUBLISH, new PublishTask(project));
lm.executeAll();
return 0;
}
}

View File

@ -4,26 +4,22 @@ import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.vm.VMLauncher; import org.jcnc.snow.vm.VMLauncher;
/** /**
* CLI 命令运行已编译的 VM 字节码文件.water
* <p> * <p>
* 命令实现`snow run` * 用于执行 VM 程序文件支持传递额外 VM 参数实际运行由 {@link VMLauncher#main(String[])} 完成
* <br>
* 用于运行已编译的 VM 字节码文件.water
* </p>
* <ul>
* <li>支持传递额外 VM 参数</li>
* <li>实际执行通过 {@link VMLauncher#main(String[])} 入口完成</li>
* </ul>
* <p>
* 用法<br>
* <code>snow run program.water</code>
* </p> * </p>
*
* <pre>
* 用法示例
* $ snow run program.water [additional VM options]
* </pre>
*/ */
public final class RunCommand implements CLICommand { public final class RunCommand implements CLICommand {
/** /**
* 获取命令名 * 返回命令名用于 CLI 调用
* *
* @return "run" * @return 命令名称字符串"run"
*/ */
@Override @Override
public String name() { public String name() {
@ -31,9 +27,9 @@ public final class RunCommand implements CLICommand {
} }
/** /**
* 获取命令描述 * 返回命令简介用于 CLI 帮助或命令列表展示
* *
* @return 命令简介 * @return 命令描述字符串
*/ */
@Override @Override
public String description() { public String description() {
@ -51,11 +47,11 @@ public final class RunCommand implements CLICommand {
} }
/** /**
* 执行 run 命令运行 VM 指令文件 * 执行 run 命令运行指定的 VM 字节码文件
* *
* @param args 剩余参数不含命令名第一个应为 .vm 文件路径 * @param args 剩余参数不含命令名第一个应为 .water 文件路径其后为可选 VM 参数
* @return 0 表示成功1 表示参数错误 * @return 0 表示执行成功1 表示参数错误
* @throws Exception 运行 VM 可能抛出的异常 * @throws Exception VM 启动或执行过程中可能抛出的异常
*/ */
@Override @Override
public int execute(String[] args) throws Exception { public int execute(String[] args) throws Exception {

View File

@ -4,18 +4,22 @@ import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.cli.SnowCLI; import org.jcnc.snow.cli.SnowCLI;
/** /**
* CLI 子命令输出当前 Snow 工具的版本号
* <p> * <p>
* 子命令实现`snow version` * 用于显示当前 CLI 工具版本便于诊断升级兼容性确认等场景
* <br>
* 用于打印当前 Snow 的版本号
* </p> * </p>
*
* <pre>
* 用法示例
* $ snow version
* </pre>
*/ */
public final class VersionCommand implements CLICommand { public final class VersionCommand implements CLICommand {
/** /**
* 获取命令名 * 返回命令名用于 CLI 调用
* *
* @return "version" * @return 命令名称字符串"version"
*/ */
@Override @Override
public String name() { public String name() {
@ -23,9 +27,9 @@ public final class VersionCommand implements CLICommand {
} }
/** /**
* 获取命令描述 * 返回命令简介用于 CLI 帮助或命令列表展示
* *
* @return 命令简介 * @return 命令描述字符串
*/ */
@Override @Override
public String description() { public String description() {
@ -49,8 +53,7 @@ public final class VersionCommand implements CLICommand {
*/ */
@Override @Override
public int execute(String[] args) { public int execute(String[] args) {
System.out.println("snow version " + "\"" + SnowCLI.SNOW_VERSION + "\""); System.out.println("snow version \"" + SnowCLI.SNOW_VERSION + "\"");
return 0; return 0;
} }
} }

View File

@ -0,0 +1,125 @@
package org.jcnc.snow.pkg.dsl;
import org.jcnc.snow.pkg.model.Project;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* CloudDSLParser .cloud 配置文件解析器
* <p>
* 作用
* - 读取 Snow 构建工具自定义的 .cloud 文件
* - 将内容转换为内存中的 {@link Project} 模型
* <p>
* 解析规则
* <p>
* 1. 顶级区块projectpropertiesrepositoriesdependenciesbuild
* sectionName { 开始 } 结束
* <p>
* 2. 区块内部识别 key = value 形式的赋值
* <p>
* 3. build 区块允许嵌套内部键通过 . 展平
* 例如 compile.enabled = true
* <p>
*/
public final class CloudDSLParser {
/* ---------- 正则表达式 ---------- */
/**
* 匹配 sectionName { 形式的行
*/
private static final Pattern SECTION_HEADER = Pattern.compile("^(\\w+)\\s*\\{\\s*$");
/**
* 匹配 key = value
* value 允许空格并忽略行尾 # 注释
*/
private static final Pattern KEY_VALUE = Pattern.compile("^(\\w+)\\s*=\\s*([^#]+?)\\s*(?:#.*)?$");
/**
* 匹配仅包含 } 的行可有前后空白
*/
private static final Pattern BLOCK_END = Pattern.compile("^}\\s*$");
/**
* 私有构造禁止实例化
*/
private CloudDSLParser() {
}
/**
* 解析指定 .cloud 文件并生成 {@link Project} 对象
*
* @param path 文件路径
* @return 解析后的 Project
* @throws IOException 文件读取失败
* @throws IllegalStateException 语法错误如括号不匹配未知语句
*/
public static Project parse(Path path) throws IOException {
Deque<String> sectionStack = new ArrayDeque<>(); // 记录当前区块层级
Map<String, String> flatMap = new LinkedHashMap<>(); // 扁平化 key value
List<String> lines = Files.readAllLines(path);
int lineNo = 0;
for (String raw : lines) {
lineNo++;
String line = raw.trim();
/* 1. 跳过空行和注释 */
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
/* 2. 区块起始sectionName { */
Matcher sec = SECTION_HEADER.matcher(line);
if (sec.matches()) {
sectionStack.push(sec.group(1));
continue;
}
/* 3. 区块结束:} */
if (BLOCK_END.matcher(line).matches()) {
if (sectionStack.isEmpty()) {
throw new IllegalStateException("" + lineNo + " 行出现未配对的 '}'");
}
sectionStack.pop();
continue;
}
/* 4. 键值对key = value */
Matcher kv = KEY_VALUE.matcher(line);
if (kv.matches()) {
String key = kv.group(1).trim();
String value = kv.group(2).trim();
/* 4.1 计算前缀(栈倒序,即从外到内) */
String prefix = String.join(".", (Iterable<String>) sectionStack::descendingIterator);
if (!prefix.isEmpty()) {
key = prefix + "." + key;
}
flatMap.put(key, value);
continue;
}
/* 5. 无法识别的行 */
throw new IllegalStateException("无法解析第 " + lineNo + " 行: " + raw);
}
/* 6. 检查括号是否全部闭合 */
if (!sectionStack.isEmpty()) {
throw new IllegalStateException("文件结束但区块未闭合:" + sectionStack);
}
/* 7. 构造 Project 模型 */
return Project.fromFlatMap(flatMap);
}
}

View File

@ -0,0 +1,63 @@
package org.jcnc.snow.pkg.lifecycle;
import org.jcnc.snow.pkg.tasks.Task;
import java.util.EnumMap;
import java.util.Map;
/**
* 管理不同生命周期阶段与其对应任务的工具类
* <p>
* 可为每个 {@link LifecyclePhase} 注册对应的 {@link Task}并按阶段顺序执行所有任务
* </p>
*
* <pre>
* 示例用法
* LifecycleManager manager = new LifecycleManager();
* manager.register(LifecyclePhase.INIT, new InitTask());
* manager.executeAll();
* </pre>
*/
public final class LifecycleManager {
/**
* 存储生命周期阶段与对应任务的映射关系
*/
private final Map<LifecyclePhase, Task> tasks = new EnumMap<>(LifecyclePhase.class);
/**
* 为指定生命周期阶段注册任务
* <p>若该阶段已有任务则会被新任务覆盖</p>
*
* @param phase 生命周期阶段不能为空
* @param task 对应任务不能为空
* @throws NullPointerException phase task null
*/
public void register(LifecyclePhase phase, Task task) {
if (phase == null) {
throw new NullPointerException("Lifecycle phase must not be null");
}
if (task == null) {
throw new NullPointerException("Task must not be null");
}
tasks.put(phase, task);
}
/**
* {@link LifecyclePhase} 声明顺序依次执行所有已注册任务
* <p>
* 未注册任务的阶段将被跳过任务执行前会打印阶段名
* </p>
*
* @throws Exception 若某个任务执行时抛出异常将直接抛出并终止后续任务执行
*/
public void executeAll() throws Exception {
for (LifecyclePhase phase : LifecyclePhase.values()) {
Task task = tasks.get(phase);
if (task != null) {
System.out.println(">>> Phase: " + phase);
task.run();
}
}
}
}

View File

@ -0,0 +1,19 @@
package org.jcnc.snow.pkg.lifecycle;
/**
* 定义了典型软件包生命周期的各个阶段
*/
public enum LifecyclePhase {
/** 初始化阶段 */
INIT,
/** 解析依赖阶段 */
RESOLVE_DEPENDENCIES,
/** 编译阶段 */
COMPILE,
/** 打包阶段 */
PACKAGE,
/** 发布阶段 */
PUBLISH,
/** 清理阶段 */
CLEAN
}

View File

@ -0,0 +1,63 @@
package org.jcnc.snow.pkg.model;
import java.util.Map;
import java.util.HashMap;
/**
* 构建配置对象封装构建过程中的所有选项
* <p>
* 支持基于模板变量形如{@code @{key}}的选项值替换
* </p>
*/
public final class BuildConfiguration {
/** 存储配置项的键值对 */
private final Map<String, String> options;
/**
* 私有构造函数用于初始化配置项
*
* @param options 配置项键值对
*/
private BuildConfiguration(Map<String, String> options) {
this.options = options;
}
/**
* 基于原始配置项和变量属性创建配置对象
* <p>
* 会将原始配置中的所有值中的{@code @{key}}模板替换为属性props中对应的值
* </p>
*
* @param flat 原始的配置项值中可包含模板变量@{name}
* @param props 用于替换模板变量的属性集
* @return 构建完成的配置对象
*/
public static BuildConfiguration fromFlatMap(Map<String, String> flat, Map<String, String> props) {
Map<String, String> resolved = new HashMap<>();
for (Map.Entry<String, String> e : flat.entrySet()) {
String value = e.getValue();
for (Map.Entry<String, String> p : props.entrySet()) {
value = value.replace("@{" + p.getKey() + "}", p.getValue());
}
resolved.put(e.getKey(), value);
}
return new BuildConfiguration(resolved);
}
/**
* 获取指定key对应的配置值
*
* @param key 配置项名称
* @param def 默认值若未找到key则返回此值
* @return 配置项对应值若不存在则返回默认值
*/
public String get(String key, String def) {
return options.getOrDefault(key, def);
}
@Override
public String toString() {
return options.toString();
}
}

View File

@ -0,0 +1,82 @@
package org.jcnc.snow.pkg.model;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 表示一个 Maven 风格的 group:artifact:version 依赖坐标
* <p>
* 支持通过占位符和属性映射进行动态变量替换可用于构建工具或依赖管理场景
* </p>
*
* <pre>
* 示例
* Dependency dep = Dependency.fromString(
* "core", "com.example:core:@{version}",
* Map.of("version", "1.2.3")
* );
* </pre>
*/
public record Dependency(
String id,
String group,
String artifact,
String version
) {
/**
* 用于匹配 group:artifact:version 格式的正则表达式
*/
private static final Pattern GAV = Pattern.compile("([^:]+):([^:]+):(.+)");
/**
* 根据字符串坐标和属性映射创建依赖对象
* <p>
* 坐标中的占位符如 {@code @{key}} 会用 props 中对应的值替换
* </p>
*
* @param id 依赖唯一标识
* @param coordinate 依赖坐标字符串格式为 group:artifact:version支持变量占位符
* @param props 占位符替换的属性映射
* @return 解析后的 Dependency 实例
* @throws IllegalArgumentException 如果坐标格式非法
*/
public static Dependency fromString(String id, String coordinate, Map<String, String> props) {
// 替换 @{prop} 占位符
String resolved = coordinate;
for (Map.Entry<String, String> p : props.entrySet()) {
resolved = resolved.replace("@{" + p.getKey() + "}", p.getValue());
}
Matcher m = GAV.matcher(resolved);
if (!m.matches()) {
throw new IllegalArgumentException("Invalid dependency format: " + coordinate);
}
return new Dependency(id, m.group(1), m.group(2), m.group(3));
}
/**
* 生成依赖对应的标准仓库 jar 路径
* <p>
* 路径格式通常为groupId/artifactId/version/artifactId-version.jar<br>
* 例如com/example/core/1.2.3/core-1.2.3.jar
* </p>
*
* @return 仓库 jar 文件的相对路径
*/
public String toPath() {
String groupPath = group.replace('.', '/');
return groupPath + "/" + artifact + "/" + version + "/" + artifact + "-" + version + ".jar";
}
/**
* 返回该依赖的 group:artifact:version 字符串表示
*
* @return Maven 坐标字符串
*/
@Override
public String toString() {
return group + ":" + artifact + ":" + version;
}
}

View File

@ -0,0 +1,193 @@
package org.jcnc.snow.pkg.model;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 表示一个软件包/模块的项目信息包括元数据属性仓库依赖和构建配置等
* <p>
* 本类为不可变对象仅提供getter无setter
* 支持通过 {@link #fromFlatMap(Map)} 静态工厂方法从扁平Map快速创建
* </p>
*
* <pre>
* Map&lt;String,String&gt; map = ...;
* Project project = Project.fromFlatMap(map);
* </pre>
*/
public final class Project {
/** 组织/分组名如com.example */
private final String group;
/** 构件/模块名如app-core */
private final String artifact;
/** 项目展示名称 */
private final String name;
/** 版本号如1.0.0 */
private final String version;
/** 项目描述 */
private final String description;
/** 许可证标识 */
private final String license;
/** 项目主页URL */
private final String homepage;
/** 额外属性(不影响主字段,可用于模板/占位符) */
private final Map<String, String> properties;
/** 仓库列表仓库ID -> 仓库对象) */
private final Map<String, Repository> repositories;
/** 依赖列表 */
private final List<Dependency> dependencies;
/** 构建配置 */
private final BuildConfiguration build;
/**
* 构造函数私有请使用 {@link #fromFlatMap(Map)} 创建实例
*/
private Project(
String group,
String artifact,
String name,
String version,
String description,
String license,
String homepage,
Map<String, String> properties,
Map<String, Repository> repositories,
List<Dependency> dependencies,
BuildConfiguration build
) {
this.group = group;
this.artifact = artifact;
this.name = name;
this.version = version;
this.description = description;
this.license = license;
this.homepage = homepage;
this.properties = properties;
this.repositories = repositories;
this.dependencies = dependencies;
this.build = build;
}
/**
* 通过扁平Map创建 Project 实例约定key格式如下
* <ul>
* <li>project.* 项目元数据</li>
* <li>properties.* 额外属性</li>
* <li>repositories.* 仓库</li>
* <li>dependencies.* 依赖</li>
* <li>build.* 构建配置</li>
* </ul>
*
* @param map 扁平的配置map
* @return Project 实例
*/
public static Project fromFlatMap(Map<String, String> map) {
// 1. simple project metadata
String group = map.getOrDefault("project.group", "unknown");
String artifact = map.getOrDefault("project.artifact", "unknown");
String name = map.getOrDefault("project.name", artifact);
String version = map.getOrDefault("project.version", "0.0.1-SNAPSHOT");
String description = map.getOrDefault("project.description", "");
String license = map.getOrDefault("project.license", "");
String homepage = map.getOrDefault("project.homepage", "");
// 2. properties.*
Map<String, String> props = new LinkedHashMap<>();
map.forEach((k, v) -> {
if (k.startsWith("properties.")) {
props.put(k.substring("properties.".length()), v);
}
});
// 3. repositories.*
Map<String, Repository> repos = new LinkedHashMap<>();
map.forEach((k, v) -> {
if (k.startsWith("repositories.")) {
String id = k.substring("repositories.".length());
repos.put(id, new Repository(id, v));
}
});
// 4. dependencies.*
List<Dependency> deps = new ArrayList<>();
map.forEach((k, v) -> {
if (k.startsWith("dependencies.")) {
String id = k.substring("dependencies.".length());
deps.add(Dependency.fromString(id, v, props));
}
});
// 5. build.* simply hand the subtree map
Map<String, String> buildMap = new LinkedHashMap<>();
map.forEach((k, v) -> {
if (k.startsWith("build.")) {
buildMap.put(k.substring("build.".length()), v);
}
});
BuildConfiguration buildCfg = BuildConfiguration.fromFlatMap(buildMap, props);
return new Project(group, artifact, name, version, description, license, homepage, props, repos, deps, buildCfg);
}
/** @return 组织/分组名 */
public String getGroup() {
return group;
}
/** @return 构件/模块名 */
public String getArtifact() {
return artifact;
}
/** @return 项目名称 */
public String getName() {
return name;
}
/** @return 版本号 */
public String getVersion() {
return version;
}
/** @return 项目描述 */
public String getDescription() {
return description;
}
/** @return 许可证 */
public String getLicense() {
return license;
}
/** @return 项目主页URL */
public String getHomepage() {
return homepage;
}
/** @return 额外属性映射 */
public Map<String, String> getProperties() {
return properties;
}
/** @return 仓库映射 */
public Map<String, Repository> getRepositories() {
return repositories;
}
/** @return 依赖列表 */
public List<Dependency> getDependencies() {
return dependencies;
}
/** @return 构建配置 */
public BuildConfiguration getBuild() {
return build;
}
}

View File

@ -0,0 +1,18 @@
package org.jcnc.snow.pkg.model;
/**
* 表示一个远程仓库的基本信息通常用于依赖解析和发布
* <p>
* 每个仓库由唯一的ID和对应的URL确定
* </p>
*
* <pre>
* 示例:
* Repository repo = new Repository("central", "https://");
* </pre>
*
* @param id 仓库唯一标识
* @param url 仓库地址一般为HTTP(S)链接
*/
public record Repository(String id, String url) {
}

View File

@ -0,0 +1,82 @@
package org.jcnc.snow.pkg.resolver;
import org.jcnc.snow.pkg.model.Dependency;
import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.pkg.model.Repository;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Optional;
/**
* 负责解析并下载项目依赖的工具类
* <p>
* 支持本地缓存如果依赖已存在于本地缓存则直接使用否则尝试从项目仓库下载依赖包
* </p>
*/
public final class DependencyResolver {
/** 本地缓存目录 */
private final Path localCache;
/**
* 创建 DependencyResolver 实例
*
* @param localCacheDir 用于缓存依赖的本地目录
*/
public DependencyResolver(Path localCacheDir) {
this.localCache = localCacheDir;
}
/**
* 解析并下载指定项目的所有依赖
* <p>
* 依赖优先从本地缓存读取若未命中则尝试从第一个配置的仓库下载
* </p>
*
* @param project 要解析依赖的项目
* @throws IOException 下载或文件操作失败时抛出
*/
public void resolve(Project project) throws IOException, URISyntaxException {
Files.createDirectories(localCache);
for (Dependency dep : project.getDependencies()) {
Path jarPath = localCache.resolve(dep.toPath());
if (Files.exists(jarPath)) {
System.out.println("[dependency] " + dep + " resolved from cache.");
continue;
}
// 从第一个仓库下载
Optional<Repository> repo = project.getRepositories().values().stream().findFirst();
if (repo.isEmpty()) {
throw new IOException("No repository configured for dependency " + dep);
}
String url = repo.get().url() + "/" + dep.toPath();
download(url, jarPath);
}
}
/**
* 从指定 URL 下载文件到本地目标路径
*
* @param urlStr 远程文件 URL
* @param dest 本地目标路径
* @throws IOException 下载或保存文件时出错
*/
private void download(String urlStr, Path dest) throws IOException, URISyntaxException {
System.out.println("[download] " + urlStr);
Files.createDirectories(dest.getParent());
URL url = new URI(urlStr).toURL();
try (InputStream in = url.openStream()) {
Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);
}
System.out.println("[saved] " + dest);
}
}

View File

@ -0,0 +1,48 @@
package org.jcnc.snow.pkg.tasks;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
/**
* 清理构建输出目录 build dist的任务实现
* <p>
* 实现 {@link Task} 接口通常用于构建流程中的清理阶段
* </p>
*/
public final class CleanTask implements Task {
/**
* 执行清理任务删除 "build" "dist" 目录及其所有内容
*
* @throws IOException 删除目录过程中出现 IO 错误时抛出
*/
@Override
public void run() throws IOException {
deleteDir(Path.of("build"));
deleteDir(Path.of("dist"));
System.out.println("[clean] done.");
}
/**
* 递归删除指定目录及其所有子文件和子目录
* 使用 try-with-resources 自动关闭文件流避免资源泄漏
*
* @param dir 需要删除的目录路径
* @throws IOException 删除过程中出现 IO 错误时抛出
*/
private void deleteDir(Path dir) throws IOException {
if (Files.notExists(dir)) return;
try (var stream = Files.walk(dir)) {
stream.sorted(Comparator.reverseOrder()) // 先删子文件后删父目录
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
}

View File

@ -0,0 +1,43 @@
package org.jcnc.snow.pkg.tasks;
import org.jcnc.snow.pkg.model.Project;
import java.nio.file.Path;
/**
* 编译项目源代码的任务实现
* <p>
* 实现 {@link Task} 接口用于构建流程中的编译阶段当前仅为示例未集成实际编译器
* </p>
*/
public final class CompileTask implements Task {
/** 待编译的项目 */
private final Project project;
/**
* 创建 CompileTask 实例
*
* @param project 目标项目
*/
public CompileTask(Project project) {
this.project = project;
}
/**
* 执行编译任务打印源代码目录和输出目录
* <p>
* 实际编译尚未实现TODO
* </p>
*
* @throws Exception 预留未来集成编译器可能抛出异常
*/
@Override
public void run() throws Exception {
// 获取源码目录和输出目录默认分别为 "src" "build/classes"
Path srcDir = Path.of(project.getProperties().getOrDefault("src_dir", "src"));
Path outDir = Path.of(project.getProperties().getOrDefault("output_dir", "build/classes"));
System.out.println("[compile] sources=" + srcDir + " output=" + outDir);
// TODO: 集成实际的编译器
}
}

View File

@ -0,0 +1,67 @@
package org.jcnc.snow.pkg.tasks;
import org.jcnc.snow.pkg.model.Project;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 项目打包任务将编译输出目录 build/classes打包为 .ice 文件
* <p>
* 实现 {@link Task} 接口通常用于构建流程中的打包阶段
* </p>
*/
public final class PackageTask implements Task {
/** 目标项目 */
private final Project project;
/**
* 创建 PackageTask 实例
*
* @param project 目标项目
*/
public PackageTask(Project project) {
this.project = project;
}
/**
* 执行打包任务将编译输出目录压缩为 artifact-version.ice 文件
*
* @throws Exception 打包过程中出现 IO 或其他异常时抛出
*/
@Override
public void run() throws Exception {
String artifact = project.getArtifact();
String version = project.getVersion();
String fileName = artifact + "-" + version + ".ice";
Path distDir = Path.of("dist");
Files.createDirectories(distDir);
Path packageFile = distDir.resolve(fileName);
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(packageFile))) {
// 仅将编译输出目录打包
Path classesDir = Path.of("build/classes");
if (Files.exists(classesDir)) {
// 使用 try-with-resources 正确关闭 Stream<Path>
try (Stream<Path> stream = Files.walk(classesDir)) {
stream.filter(Files::isRegularFile)
.forEach(p -> {
try {
// 将文件以相对路径加入压缩包
zos.putNextEntry(new ZipEntry(classesDir.relativize(p).toString()));
Files.copy(p, zos);
zos.closeEntry();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
}
System.out.println("[package] created " + packageFile);
}
}

View File

@ -0,0 +1,35 @@
package org.jcnc.snow.pkg.tasks;
import org.jcnc.snow.pkg.model.Project;
/**
* 发布项目构件到远程仓库的任务实现
* <p>
* 实现 {@link Task} 接口通常用于构建流程中的发布阶段目前仅为演示尚未实现实际上传
* </p>
*/
public final class PublishTask implements Task {
/** 目标项目 */
private final Project project;
/**
* 创建 PublishTask 实例
*
* @param project 目标项目
*/
public PublishTask(Project project) {
this.project = project;
}
/**
* 执行发布任务当前仅打印发布提示未实现实际上传逻辑
*
* @throws Exception 预留未来实现上传逻辑时可能抛出异常
*/
@Override
public void run() throws Exception {
// TODO: 实现上传到仓库 HTTP PUT/POST
System.out.println("[publish] uploading artifact " + project.getArtifact() + "-" + project.getVersion());
}
}

View File

@ -0,0 +1,17 @@
package org.jcnc.snow.pkg.tasks;
/**
* 构建任务的通用接口所有具体任务都应实现该接口
* <p>
* 用于统一生命周期内的任务行为例如编译打包清理等
* </p>
*/
public interface Task {
/**
* 执行具体任务的入口方法
*
* @throws Exception 任务执行过程中出现的异常
*/
void run() throws Exception;
}