diff --git a/src/main/java/org/jcnc/snow/cli/CLICommand.java b/src/main/java/org/jcnc/snow/cli/CLICommand.java index ffd1e03..0874cf1 100644 --- a/src/main/java/org/jcnc/snow/cli/CLICommand.java +++ b/src/main/java/org/jcnc/snow/cli/CLICommand.java @@ -1,14 +1,12 @@ package org.jcnc.snow.cli; /** + * 所有 CLI 子命令(如 compile、run 等)都必须实现的命令接口。 *
- * 所有子命令(如 compile、run 等)都必须实现的命令接口。 + * 实现类应为无状态(stateless)、线程安全(thread-safe)。 + * 可通过 {@link java.util.ServiceLoader ServiceLoader} 自动发现, + * 或直接在 {@link SnowCLI} 中注册。 *
- **/ public interface CLICommand { @@ -28,16 +26,19 @@ public interface CLICommand { /** * 打印命令的专用 usage 信息(可选实现)。 + *
* 可覆盖此方法自定义帮助信息,默认无操作。 + *
*/ default void printUsage() { + // 默认实现为空,可由子类覆盖 } /** * 执行命令逻辑。 * * @param args 传递给子命令的参数(不含命令名本身) - * @return 进程退出码(0 为成功,非0为错误) + * @return 进程退出码(0 为成功,非 0 为错误) * @throws Exception 可抛出任意异常,框架会统一捕获和输出 */ int execute(String[] args) throws Exception; diff --git a/src/main/java/org/jcnc/snow/cli/SnowCLI.java b/src/main/java/org/jcnc/snow/cli/SnowCLI.java index 1b135f7..5fae2a7 100644 --- a/src/main/java/org/jcnc/snow/cli/SnowCLI.java +++ b/src/main/java/org/jcnc/snow/cli/SnowCLI.java @@ -32,7 +32,12 @@ public class SnowCLI { private static final Map+ * 该命令会依次执行依赖解析、源码编译和产物打包阶段。 + *
+ * + *+ * 用法示例: + * $ snow build + *+ */ +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 "); + } + + /** + * 执行项目构建流程。 + *
+ * 用于清除项目生成的 build、dist 等中间产物,保持工作区整洁。 + *
+ * + *+ * 用法示例: + * $ snow clean + *+ */ +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; + } +} diff --git a/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java b/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java index 3fd0439..8d77e8d 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/CompileCommand.java @@ -24,20 +24,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; + /** + * CLI 命令:将 .snow 源文件编译为 VM 字节码(.water 文件)。 *
- * 编译器子命令:snow compile
- * 将一个或多个 .snow 源文件编译为 VM 字节码文件(.water)。
+ * 支持递归目录、多文件编译,可选编译后立即运行。
+ * 命令参数支持 run、-o、-d 等。
*
用法: - * snow compile [run] [-o <name>] [-d <srcDir>] [file1.snow file2.snow …] + *+ * 用法示例: + * $ snow compile [run] [-o <name>] [-d <srcDir>] [file1.snow file2.snow …] **/ public final class CompileCommand implements CLICommand { diff --git a/src/main/java/org/jcnc/snow/cli/commands/InitCommand.java b/src/main/java/org/jcnc/snow/cli/commands/InitCommand.java new file mode 100644 index 0000000..62a80b3 --- /dev/null +++ b/src/main/java/org/jcnc/snow/cli/commands/InitCommand.java @@ -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 命令:创建新的项目骨架。 + *+ * 用于快速初始化标准目录结构和 DSL 配置文件(project.cloud)。 + *
+ * + *+ * 用法示例: + * $ snow init + *+ */ +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]"); + } + + /** + * 执行项目初始化流程,创建 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; + } +} diff --git a/src/main/java/org/jcnc/snow/cli/commands/InstallCommand.java b/src/main/java/org/jcnc/snow/cli/commands/InstallCommand.java new file mode 100644 index 0000000..7cd0a2b --- /dev/null +++ b/src/main/java/org/jcnc/snow/cli/commands/InstallCommand.java @@ -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 命令:解析并下载项目依赖到本地缓存。 + * + * 适用于离线使用和依赖预热场景,会自动读取项目描述文件并处理依赖缓存。 + *
+ * + *+ * 用法示例: + * $ snow install + *+ */ +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; + } +} diff --git a/src/main/java/org/jcnc/snow/cli/commands/PublishCommand.java b/src/main/java/org/jcnc/snow/cli/commands/PublishCommand.java new file mode 100644 index 0000000..2073f91 --- /dev/null +++ b/src/main/java/org/jcnc/snow/cli/commands/PublishCommand.java @@ -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 命令:将已构建的项目包发布到远程仓库。 + *+ * 用于持续集成、交付或分发场景。 + * 支持自动读取 DSL 项目描述文件,并注册和执行发布生命周期阶段的任务。 + *
+ * + *+ * 用法示例: + * $ snow publish + *+ */ +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 "); + } + + /** + * 执行发布命令。 + *+ *
+ * + * @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; + } +} diff --git a/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java b/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java index 63b4d34..09a10d4 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/RunCommand.java @@ -4,26 +4,22 @@ import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.vm.VMLauncher; /** + * CLI 命令:运行已编译的 VM 字节码文件(.water)。 *- 解析项目描述文件(如 project.cloud)
+ *- 注册并执行 PUBLISH 阶段的任务
+ *- * 命令实现:`snow run` - *
- *
- * 用于运行已编译的 VM 字节码文件(.water)。 - *- *
- *- 支持传递额外 VM 参数。
- *- 实际执行通过 {@link VMLauncher#main(String[])} 入口完成。
- *- * 用法:
+ * + *
- *snow run program.water+ * 用于执行 VM 程序文件。支持传递额外 VM 参数,实际运行由 {@link VMLauncher#main(String[])} 完成。 *+ * 用法示例: + * $ snow run program.water [additional VM options] + **/ public final class RunCommand implements CLICommand { /** - * 获取命令名。 + * 返回命令名,用于 CLI 调用。 * - * @return "run" + * @return 命令名称字符串("run") */ @Override public String name() { @@ -31,9 +27,9 @@ public final class RunCommand implements CLICommand { } /** - * 获取命令描述。 + * 返回命令简介,用于 CLI 帮助或命令列表展示。 * - * @return 命令简介 + * @return 命令描述字符串 */ @Override public String description() { @@ -51,11 +47,11 @@ public final class RunCommand implements CLICommand { } /** - * 执行 run 命令,运行 VM 指令文件。 + * 执行 run 命令,运行指定的 VM 字节码文件。 * - * @param args 剩余参数(不含命令名),第一个应为 .vm 文件路径 - * @return 0 表示成功,1 表示参数错误 - * @throws Exception 运行 VM 时可能抛出的异常 + * @param args 剩余参数(不含命令名),第一个应为 .water 文件路径,其后为可选 VM 参数 + * @return 0 表示执行成功,1 表示参数错误 + * @throws Exception VM 启动或执行过程中可能抛出的异常 */ @Override public int execute(String[] args) throws Exception { diff --git a/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java b/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java index f45b007..c8f3af8 100644 --- a/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java +++ b/src/main/java/org/jcnc/snow/cli/commands/VersionCommand.java @@ -4,18 +4,22 @@ import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.SnowCLI; /** + * CLI 子命令:输出当前 Snow 工具的版本号。 *- * 子命令实现:`snow version` - *
+ * + *
- * 用于打印当前 Snow 的版本号。 + * 用于显示当前 CLI 工具版本,便于诊断、升级、兼容性确认等场景。 *+ * 用法示例: + * $ snow version + **/ public final class VersionCommand implements CLICommand { /** - * 获取命令名。 + * 返回命令名,用于 CLI 调用。 * - * @return "version" + * @return 命令名称字符串("version") */ @Override public String name() { @@ -23,9 +27,9 @@ public final class VersionCommand implements CLICommand { } /** - * 获取命令描述。 + * 返回命令简介,用于 CLI 帮助或命令列表展示。 * - * @return 命令简介 + * @return 命令描述字符串 */ @Override public String description() { @@ -49,8 +53,7 @@ public final class VersionCommand implements CLICommand { */ @Override public int execute(String[] args) { - System.out.println("snow version " + "\"" + SnowCLI.SNOW_VERSION + "\""); - + System.out.println("snow version \"" + SnowCLI.SNOW_VERSION + "\""); return 0; } } diff --git a/src/main/java/org/jcnc/snow/pkg/dsl/CloudDSLParser.java b/src/main/java/org/jcnc/snow/pkg/dsl/CloudDSLParser.java new file mode 100644 index 0000000..c707a65 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/dsl/CloudDSLParser.java @@ -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 配置文件解析器 + *+ * 作用: + * - 读取 Snow 构建工具自定义的 .cloud 文件 + * - 将内容转换为内存中的 {@link Project} 模型 + *
+ * 解析规则(: + *
+ * 1. 顶级区块(project、properties、repositories、dependencies、build) + * 以 “sectionName {” 开始,以 “}” 结束。 + *
+ * 2. 区块内部识别 “key = value” 形式的赋值。 + *
+ * 3. build 区块允许嵌套;内部键通过 “.” 展平, + * 例如 compile.enabled = true。 + *
+ */ +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
sectionStack = new ArrayDeque<>(); // 记录当前区块层级 + Map flatMap = new LinkedHashMap<>(); // 扁平化 key → value + + List 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 ) 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); + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecycleManager.java b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecycleManager.java new file mode 100644 index 0000000..60f5ebc --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecycleManager.java @@ -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; + +/** + * 管理不同生命周期阶段与其对应任务的工具类。 + * + * 可为每个 {@link LifecyclePhase} 注册对应的 {@link Task},并按阶段顺序执行所有任务。 + *
+ * + *+ * 示例用法: + * LifecycleManager manager = new LifecycleManager(); + * manager.register(LifecyclePhase.INIT, new InitTask()); + * manager.executeAll(); + *+ */ +public final class LifecycleManager { + + /** + * 存储生命周期阶段与对应任务的映射关系。 + */ + private final Maptasks = new EnumMap<>(LifecyclePhase.class); + + /** + * 为指定生命周期阶段注册任务。 + * 若该阶段已有任务,则会被新任务覆盖。
+ * + * @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} 声明顺序依次执行所有已注册任务。 + *+ * 未注册任务的阶段将被跳过。任务执行前会打印阶段名。 + *
+ * + * @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(); + } + } + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecyclePhase.java b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecyclePhase.java new file mode 100644 index 0000000..aaa464c --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/lifecycle/LifecyclePhase.java @@ -0,0 +1,19 @@ +package org.jcnc.snow.pkg.lifecycle; + +/** + * 定义了典型软件包生命周期的各个阶段。 + */ +public enum LifecyclePhase { + /** 初始化阶段 */ + INIT, + /** 解析依赖阶段 */ + RESOLVE_DEPENDENCIES, + /** 编译阶段 */ + COMPILE, + /** 打包阶段 */ + PACKAGE, + /** 发布阶段 */ + PUBLISH, + /** 清理阶段 */ + CLEAN +} diff --git a/src/main/java/org/jcnc/snow/pkg/model/BuildConfiguration.java b/src/main/java/org/jcnc/snow/pkg/model/BuildConfiguration.java new file mode 100644 index 0000000..c0f0075 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/model/BuildConfiguration.java @@ -0,0 +1,63 @@ +package org.jcnc.snow.pkg.model; + +import java.util.Map; +import java.util.HashMap; + +/** + * 构建配置对象,封装构建过程中的所有选项。 + *+ * 支持基于模板变量(形如{@code @{key}})的选项值替换。 + *
+ */ +public final class BuildConfiguration { + + /** 存储配置项的键值对 */ + private final Mapoptions; + + /** + * 私有构造函数,用于初始化配置项。 + * + * @param options 配置项键值对 + */ + private BuildConfiguration(Map options) { + this.options = options; + } + + /** + * 基于原始配置项和变量属性创建配置对象。 + * + * 会将原始配置中的所有值中的{@code @{key}}模板,替换为属性props中对应的值。 + *
+ * + * @param flat 原始的配置项,值中可包含模板变量(如@{name}) + * @param props 用于替换模板变量的属性集 + * @return 构建完成的配置对象 + */ + public static BuildConfiguration fromFlatMap(Mapflat, Map props) { + Map resolved = new HashMap<>(); + for (Map.Entry e : flat.entrySet()) { + String value = e.getValue(); + for (Map.Entry 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(); + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/model/Dependency.java b/src/main/java/org/jcnc/snow/pkg/model/Dependency.java new file mode 100644 index 0000000..50f4bea --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/model/Dependency.java @@ -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 依赖坐标。 + * + * 支持通过占位符和属性映射进行动态变量替换,可用于构建工具或依赖管理场景。 + *
+ * + *+ * 示例: + * Dependency dep = Dependency.fromString( + * "core", "com.example:core:@{version}", + * Map.of("version", "1.2.3") + * ); + *+ */ +public record Dependency( + String id, + String group, + String artifact, + String version +) { + + /** + * 用于匹配 group:artifact:version 格式的正则表达式。 + */ + private static final Pattern GAV = Pattern.compile("([^:]+):([^:]+):(.+)"); + + /** + * 根据字符串坐标和属性映射创建依赖对象。 + *+ * 坐标中的占位符如 {@code @{key}} 会用 props 中对应的值替换。 + *
+ * + * @param id 依赖唯一标识 + * @param coordinate 依赖坐标字符串,格式为 group:artifact:version,支持变量占位符 + * @param props 占位符替换的属性映射 + * @return 解析后的 Dependency 实例 + * @throws IllegalArgumentException 如果坐标格式非法 + */ + public static Dependency fromString(String id, String coordinate, Mapprops) { + // 替换 @{prop} 占位符 + String resolved = coordinate; + for (Map.Entry 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 路径。 + * + * 路径格式通常为:groupId/artifactId/version/artifactId-version.jar
+ * + * @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; + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/model/Project.java b/src/main/java/org/jcnc/snow/pkg/model/Project.java new file mode 100644 index 0000000..2b09cdb --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/model/Project.java @@ -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; + +/** + * 表示一个软件包/模块的项目信息,包括元数据、属性、仓库、依赖和构建配置等。 + *
+ * 例如:com/example/core/1.2.3/core-1.2.3.jar + *+ * 本类为不可变对象,仅提供getter,无setter。 + * 支持通过 {@link #fromFlatMap(Map)} 静态工厂方法从扁平Map快速创建。 + *
+ * + *+ * Map<String,String> map = ...; + * Project project = Project.fromFlatMap(map); + *+ */ +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 Mapproperties; + /** 仓库列表(仓库ID -> 仓库对象) */ + private final Map repositories; + /** 依赖列表 */ + private final List 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 properties, + Map repositories, + List 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格式如下: + * + *
+ * + * @param map 扁平的配置map + * @return Project 实例 + */ + public static Project fromFlatMap(Map- project.* —— 项目元数据
+ *- properties.* —— 额外属性
+ *- repositories.* —— 仓库
+ *- dependencies.* —— 依赖
+ *- build.* —— 构建配置
+ *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 props = new LinkedHashMap<>(); + map.forEach((k, v) -> { + if (k.startsWith("properties.")) { + props.put(k.substring("properties.".length()), v); + } + }); + + // 3. repositories.* + Map 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 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 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 getProperties() { + return properties; + } + + /** @return 仓库映射 */ + public Map getRepositories() { + return repositories; + } + + /** @return 依赖列表 */ + public List getDependencies() { + return dependencies; + } + + /** @return 构建配置 */ + public BuildConfiguration getBuild() { + return build; + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/model/Repository.java b/src/main/java/org/jcnc/snow/pkg/model/Repository.java new file mode 100644 index 0000000..3e839f9 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/model/Repository.java @@ -0,0 +1,18 @@ +package org.jcnc.snow.pkg.model; + +/** + * 表示一个远程仓库的基本信息,通常用于依赖解析和发布。 + * + * 每个仓库由唯一的ID和对应的URL确定。 + *
+ * + *+ * 示例: + * Repository repo = new Repository("central", "https://"); + *+ * + * @param id 仓库唯一标识 + * @param url 仓库地址(一般为HTTP(S)链接) + */ +public record Repository(String id, String url) { +} diff --git a/src/main/java/org/jcnc/snow/pkg/resolver/DependencyResolver.java b/src/main/java/org/jcnc/snow/pkg/resolver/DependencyResolver.java new file mode 100644 index 0000000..7c9dcf8 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/resolver/DependencyResolver.java @@ -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; + +/** + * 负责解析并下载项目依赖的工具类。 + *+ * 支持本地缓存,如果依赖已存在于本地缓存则直接使用,否则尝试从项目仓库下载依赖包。 + *
+ */ +public final class DependencyResolver { + + /** 本地缓存目录 */ + private final Path localCache; + + /** + * 创建 DependencyResolver 实例。 + * + * @param localCacheDir 用于缓存依赖的本地目录 + */ + public DependencyResolver(Path localCacheDir) { + this.localCache = localCacheDir; + } + + /** + * 解析并下载指定项目的所有依赖。 + *+ * 依赖优先从本地缓存读取,若未命中,则尝试从第一个配置的仓库下载。 + *
+ * + * @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; + } + + // 从第一个仓库下载 + Optionalrepo = 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); + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/CleanTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/CleanTask.java new file mode 100644 index 0000000..0b5c1c5 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/tasks/CleanTask.java @@ -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)的任务实现。 + * + * 实现 {@link Task} 接口,通常用于构建流程中的清理阶段。 + *
+ */ +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); + } + }); + } + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/CompileTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/CompileTask.java new file mode 100644 index 0000000..fb3fb99 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/tasks/CompileTask.java @@ -0,0 +1,43 @@ +package org.jcnc.snow.pkg.tasks; + +import org.jcnc.snow.pkg.model.Project; + +import java.nio.file.Path; + +/** + * 编译项目源代码的任务实现。 + *+ * 实现 {@link Task} 接口,用于构建流程中的编译阶段。当前仅为示例,未集成实际编译器。 + *
+ */ +public final class CompileTask implements Task { + + /** 待编译的项目 */ + private final Project project; + + /** + * 创建 CompileTask 实例。 + * + * @param project 目标项目 + */ + public CompileTask(Project project) { + this.project = project; + } + + /** + * 执行编译任务,打印源代码目录和输出目录。 + *+ * 实际编译尚未实现(TODO)。 + *
+ * + * @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: 集成实际的编译器 + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/PackageTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/PackageTask.java new file mode 100644 index 0000000..7524f73 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/tasks/PackageTask.java @@ -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 文件。 + *+ * 实现 {@link Task} 接口,通常用于构建流程中的打包阶段。 + *
+ */ +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+ try (Stream 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); + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/PublishTask.java b/src/main/java/org/jcnc/snow/pkg/tasks/PublishTask.java new file mode 100644 index 0000000..d67b0e1 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/tasks/PublishTask.java @@ -0,0 +1,35 @@ +package org.jcnc.snow.pkg.tasks; + +import org.jcnc.snow.pkg.model.Project; + +/** + * 发布项目构件到远程仓库的任务实现。 + * + * 实现 {@link Task} 接口,通常用于构建流程中的发布阶段。目前仅为演示,尚未实现实际上传。 + *
+ */ +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()); + } +} diff --git a/src/main/java/org/jcnc/snow/pkg/tasks/Task.java b/src/main/java/org/jcnc/snow/pkg/tasks/Task.java new file mode 100644 index 0000000..755adc2 --- /dev/null +++ b/src/main/java/org/jcnc/snow/pkg/tasks/Task.java @@ -0,0 +1,17 @@ +package org.jcnc.snow.pkg.tasks; + +/** + * 构建任务的通用接口,所有具体任务都应实现该接口。 + *+ * 用于统一生命周期内的任务行为,例如编译、打包、清理等。 + *
+ */ +public interface Task { + + /** + * 执行具体任务的入口方法。 + * + * @throws Exception 任务执行过程中出现的异常 + */ + void run() throws Exception; +}