!16 feat: 支持生成项目脚手架

Merge pull request !16 from Luke/feat/pkg-project-scaffolding-generate
This commit is contained in:
Luke 2025-06-24 10:11:39 +00:00 committed by Gitee
commit c7a953995a
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
31 changed files with 900 additions and 422 deletions

View File

@ -20,7 +20,7 @@ https://gitee.com/jcnc-org/snow/blob/main/doc/Git-Management/Git-Management.md
感谢你的配合!🙏 感谢你的配合!🙏
--> -->
# 描述 (Description) ## 描述 (Description)
请简要描述本次变更的目的和内容。 请简要描述本次变更的目的和内容。

View File

@ -1,5 +1,7 @@
import org.jcnc.snow.cli.api.CLICommand;
module org.jcnc.snow.compiler { module org.jcnc.snow.compiler {
uses org.jcnc.snow.cli.CLICommand; uses CLICommand;
requires java.desktop; requires java.desktop;
requires java.logging; requires java.logging;
exports org.jcnc.snow.compiler.ir.core; exports org.jcnc.snow.compiler.ir.core;

View File

@ -1,8 +1,7 @@
package org.jcnc.snow.cli; package org.jcnc.snow.cli;
import org.jcnc.snow.cli.commands.CompileCommand; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.cli.commands.RunCommand; import org.jcnc.snow.cli.commands.*;
import org.jcnc.snow.cli.commands.VersionCommand;
import org.jcnc.snow.cli.utils.CLIUtils; import org.jcnc.snow.cli.utils.CLIUtils;
import org.jcnc.snow.cli.utils.VersionUtils; import org.jcnc.snow.cli.utils.VersionUtils;
@ -30,14 +29,16 @@ public class SnowCLI {
* 值为返回相应 {@link CLICommand} 实例的 Supplier * 值为返回相应 {@link CLICommand} 实例的 Supplier
*/ */
private static final Map<String, Supplier<CLICommand>> COMMANDS = Map.of( private static final Map<String, Supplier<CLICommand>> COMMANDS = Map.of(
"generate", GenerateCommand::new,
"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, "init", InitCommand::new,
"build", org.jcnc.snow.cli.commands.BuildCommand::new, "build", BuildCommand::new,
"install", org.jcnc.snow.cli.commands.InstallCommand::new, "install", InstallCommand::new,
"publish", org.jcnc.snow.cli.commands.PublishCommand::new, "publish", PublishCommand::new,
"clean", org.jcnc.snow.cli.commands.CleanCommand::new "clean", CleanCommand::new
); );
/** /**

View File

@ -1,4 +1,6 @@
package org.jcnc.snow.cli; package org.jcnc.snow.cli.api;
import org.jcnc.snow.cli.SnowCLI;
/** /**
* 所有 CLI 子命令 compilerun 都必须实现的命令接口 * 所有 CLI 子命令 compilerun 都必须实现的命令接口

View File

@ -1,6 +1,6 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.pkg.dsl.CloudDSLParser; import org.jcnc.snow.pkg.dsl.CloudDSLParser;
import org.jcnc.snow.pkg.lifecycle.LifecycleManager; import org.jcnc.snow.pkg.lifecycle.LifecycleManager;
import org.jcnc.snow.pkg.lifecycle.LifecyclePhase; import org.jcnc.snow.pkg.lifecycle.LifecyclePhase;

View File

@ -1,6 +1,6 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.pkg.lifecycle.LifecycleManager; import org.jcnc.snow.pkg.lifecycle.LifecycleManager;
import org.jcnc.snow.pkg.lifecycle.LifecyclePhase; import org.jcnc.snow.pkg.lifecycle.LifecyclePhase;
import org.jcnc.snow.pkg.tasks.CleanTask; import org.jcnc.snow.pkg.tasks.CleanTask;

View File

@ -1,48 +1,35 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.compiler.backend.alloc.RegisterAllocator; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.compiler.backend.builder.VMCodeGenerator; import org.jcnc.snow.pkg.dsl.CloudDSLParser;
import org.jcnc.snow.compiler.backend.builder.VMProgramBuilder; import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.compiler.backend.core.InstructionGenerator; import org.jcnc.snow.pkg.tasks.CompileTask;
import org.jcnc.snow.compiler.backend.generator.InstructionGeneratorProvider;
import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.compiler.ir.builder.IRProgramBuilder;
import org.jcnc.snow.compiler.ir.core.IRFunction;
import org.jcnc.snow.compiler.ir.core.IRInstruction;
import org.jcnc.snow.compiler.ir.core.IRProgram;
import org.jcnc.snow.compiler.ir.value.IRVirtualRegister;
import org.jcnc.snow.compiler.lexer.core.LexerEngine;
import org.jcnc.snow.compiler.parser.ast.base.Node;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.core.ParserEngine;
import org.jcnc.snow.compiler.parser.function.ASTPrinter;
import org.jcnc.snow.compiler.semantic.core.SemanticAnalyzerRunner;
import org.jcnc.snow.vm.VMLauncher;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** /**
* CLI 命令 .snow 源文件编译为 VM 字节码.water 文件 * CLI 命令编译当前项目
* <p>
* 支持递归目录多文件编译可选编译后立即运行<br>
* 命令参数支持 run-o-d
* </p>
* *
* <pre> * <p>工作模式说明</p>
* 用法示例 * <ul>
* $ snow compile [run] [-o &lt;name&gt;] [-d &lt;srcDir&gt;] [file1.snow file2.snow ] * <li><strong>Cloud 模式</strong>
* </pre> * - 项目根目录存在 {@code project.cloud} 时触发
* - 解析 build 区块自动推导源码目录与输出文件名
* - 用法{@code snow compile [run]}</li>
* <li><strong>Local 模式</strong>
* - 未检测到 {@code project.cloud} 时回退
* - 保持向后兼容{@code snow compile [run] [-o <name>] [-d <srcDir>] [file.snow ]}</li>
* </ul>
*
* <p>两种模式均将最终参数交由 {@link CompileTask} 处理</p>
*/ */
public final class CompileCommand implements CLICommand { public final class CompileCommand implements CLICommand {
/* --------------------------------------------------------------------- */
/* CLICommand 接口实现 */
/* --------------------------------------------------------------------- */
@Override @Override
public String name() { public String name() {
return "compile"; return "compile";
@ -56,202 +43,50 @@ public final class CompileCommand implements CLICommand {
@Override @Override
public void printUsage() { public void printUsage() {
System.out.println("Usage:"); System.out.println("Usage:");
System.out.println(" snow compile [run] [-o <name>] [-d <srcDir>] [file1.snow file2.snow …]"); System.out.println(" snow compile [run] (cloud mode, use project.cloud)");
System.out.println("Options:"); System.out.println(" snow compile [run] [-o <name>] [-d <srcDir>] [file1.snow …] (GOPATH mode)");
System.out.println(" run compile then run");
System.out.println(" -o <name> specify output base name (without .water suffix)");
System.out.println(" -d <srcDir> recursively compile all .snow files in directory");
} }
/* --------------------------------------------------------------------- */
/* 核心:执行 compile 子命令 */
/* --------------------------------------------------------------------- */
@Override @Override
public int execute(String[] args) throws Exception { public int execute(String[] args) throws Exception {
/* ---------------- 解析命令行参数 ---------------- */
boolean runAfterCompile = false;
String outputName = null;
Path dir = null;
List<Path> sources = new ArrayList<>();
for (int i = 0; i < args.length; i++) { Path dslFile = Paths.get("project.cloud");
String arg = args[i]; Project project;
switch (arg) { String[] compileArgs;
case "run" -> runAfterCompile = true;
case "-o" -> { /* ---------- 1. Cloud 模式 ---------- */
if (i + 1 < args.length) outputName = args[++i]; if (Files.exists(dslFile)) {
else { project = CloudDSLParser.parse(dslFile);
System.err.println("Missing argument for -o");
printUsage(); List<String> argList = new ArrayList<>();
return 1;
} // 保留用户在 cloud 模式下传入的 run 标志
} for (String a : args) {
case "-d" -> { if ("run".equals(a)) {
if (i + 1 < args.length) dir = Path.of(args[++i]); argList.add("run");
else {
System.err.println("Missing argument for -d");
printUsage();
return 1;
}
}
default -> {
if (arg.endsWith(".snow")) {
sources.add(Path.of(arg));
} else {
System.err.println("Unknown option or file: " + arg);
printUsage();
return 1;
}
} }
} }
/* 源码目录build.srcDir -> 默认 src */
String srcDir = project.getBuild().get("srcDir", "src");
argList.add("-d");
argList.add(srcDir);
/* 输出名称build.output -> fallback to artifact */
String output = project.getBuild().get("output", project.getArtifact());
argList.add("-o");
argList.add(output);
compileArgs = argList.toArray(new String[0]);
}
/* ---------- 2. Local 模式 ---------- */
else {
project = Project.fromFlatMap(Collections.emptyMap()); // 占位项目保持接口统一
compileArgs = args; // 透传原始 CLI 参数
} }
/* --------- 如果指定了目录则递归收集所有 *.snow --------- */ // 委托给 CompileTask 完成实际编译/运行
if (dir != null) { new CompileTask(project, compileArgs).run();
if (!Files.isDirectory(dir)) {
System.err.println("Not a directory: " + dir);
return 1;
}
try (var stream = Files.walk(dir)) {
stream.filter(p -> p.toString().endsWith(".snow"))
.sorted() // 确保稳定顺序
.forEach(sources::add);
}
}
if (sources.isEmpty()) {
System.err.println("No .snow source files found.");
return 1;
}
/* 多文件但未指定 -o 且非目录编译 —— 提示必须指定输出名 */
if (sources.size() > 1 && outputName == null && dir == null) {
System.err.println("Please specify output name using -o <name>");
return 1;
}
/* ----------------------------------------------------------------- */
/* 1. 词法 + 语法分析;同时打印源代码 */
/* ----------------------------------------------------------------- */
List<Node> allAst = new ArrayList<>();
System.out.println("## 编译器输出");
System.out.println("### Snow 源代码"); // ========== 新增二级标题 ==========
for (Path p : sources) {
if (!Files.exists(p)) {
System.err.println("File not found: " + p);
return 1;
}
String code = Files.readString(p, StandardCharsets.UTF_8);
// ------- 打印每个文件的源码 -------
System.out.println("#### " + p.getFileName());
System.out.println(code);
// --------------------------------------------------------
/* 词法 + 语法 */
LexerEngine lexer = new LexerEngine(code, p.toString());
ParserContext ctx = new ParserContext(lexer.getAllTokens(), p.toString());
allAst.addAll(new ParserEngine(ctx).parse());
}
/* ----------------------------------------------------------------- */
/* 2. 语义分析 */
/* ----------------------------------------------------------------- */
SemanticAnalyzerRunner.runSemanticAnalysis(allAst, false);
/* ----------------------------------------------------------------- */
/* 3. AST → IR并把 main 函数调到首位 */
/* ----------------------------------------------------------------- */
IRProgram program = new IRProgramBuilder().buildProgram(allAst);
program = reorderForEntry(program);
/* ---------------- 打印 AST / IR ---------------- */
System.out.println("### AST");
ASTPrinter.printJson(allAst);
System.out.println("### IR");
System.out.println(program);
/* ----------------------------------------------------------------- */
/* 4. IR → VM 指令 */
/* ----------------------------------------------------------------- */
VMProgramBuilder builder = new VMProgramBuilder();
List<InstructionGenerator<? extends IRInstruction>> generators =
InstructionGeneratorProvider.defaultGenerators();
for (IRFunction fn : program.functions()) {
Map<IRVirtualRegister, Integer> slotMap =
new RegisterAllocator().allocate(fn);
new VMCodeGenerator(slotMap, builder, generators).generate(fn);
}
List<String> finalCode = builder.build();
System.out.println("### VM code");
finalCode.forEach(System.out::println);
/* ----------------------------------------------------------------- */
/* 5. 写出 .water 文件 */
/* ----------------------------------------------------------------- */
Path outputFile = deriveOutputPath(sources, outputName, dir);
Files.write(outputFile, finalCode, StandardCharsets.UTF_8);
System.out.println("Written to " + outputFile.toAbsolutePath());
/* ----------------------------------------------------------------- */
/* 6. 可选:立即运行 VM */
/* ----------------------------------------------------------------- */
if (runAfterCompile) {
System.out.println("\n=== Launching VM ===");
VMLauncher.main(new String[]{outputFile.toString()});
}
return 0; return 0;
} }
/* --------------------------------------------------------------------- */
/* 辅助方法 */
/* --------------------------------------------------------------------- */
/**
* 根据输入情况推断 .water 输出文件名
* <ul>
* <li>若指定 -o则直接使用</li>
* <li>目录编译取目录名</li>
* <li>单文件编译取文件名去掉 .snow</li>
* <li>其他情况兜底为 "program"</li>
* </ul>
*/
private static Path deriveOutputPath(List<Path> sources, String outName, Path dir) {
String base;
if (outName != null) {
base = outName;
} else if (dir != null) {
base = dir.getFileName().toString();
} else if (sources.size() == 1) {
base = sources.getFirst().getFileName().toString()
.replaceFirst("\\.snow$", "");
} else {
base = "program";
}
return Path.of(base + ".water");
}
/**
* main 函数交换到程序函数列表首位确保 PC=0 即入口
*/
private static IRProgram reorderForEntry(IRProgram in) {
List<IRFunction> ordered = new ArrayList<>(in.functions());
for (int i = 0; i < ordered.size(); i++) {
if ("main".equals(ordered.get(i).name())) {
Collections.swap(ordered, 0, i);
break;
}
}
IRProgram out = new IRProgram();
ordered.forEach(out::add);
return out;
}
} }

View File

@ -0,0 +1,87 @@
package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.api.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.GenerateTask;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* CLI 命令根据 project.cloud 生成项目目录结构
* <p>
* 负责解析云项目描述文件并通过 {@link GenerateTask}
* INIT 生命周期阶段内生成基础目录结构
* </p>
*
* <pre>
* 用法示例
* $ snow generate
* </pre>
*
* <p>
* 注意事项
* - 若当前目录不存在 project.cloud则提示用户先执行 `snow init`
* - 执行成功后会输出已创建的目录/文件
* </p>
*/
public final class GenerateCommand implements CLICommand {
/**
* 返回命令名称用于 CLI 调用
*
* @return 命令名称 "generate"
*/
@Override
public String name() {
return "generate";
}
/**
* 返回命令简介用于 CLI 帮助或命令列表展示
*
* @return 命令描述字符串
*/
@Override
public String description() {
return "Generate project directory structure based on project.cloud.";
}
/**
* 打印命令用法信息
*/
@Override
public void printUsage() {
System.out.println("Usage: snow generate");
}
/**
* 执行生成任务
*
* @param args CLI 传入的参数数组此命令不接受参数
* @return 执行结果码0 表示成功1 表示 project.cloud 缺失
* @throws Exception 执行过程中出现错误时抛出
*/
@Override
public int execute(String[] args) throws Exception {
Path dsl = Paths.get("project.cloud");
if (Files.notExists(dsl)) {
System.err.println("project.cloud not found. Please run `snow init` first.");
return 1;
}
/* 1. 解析 DSL */
Project project = CloudDSLParser.parse(dsl);
/* 2. 执行生成任务 —— 复用 Lifecycle INIT 阶段 */
LifecycleManager lm = new LifecycleManager();
lm.register(LifecyclePhase.INIT, new GenerateTask(project));
lm.executeAll();
return 0;
}
}

View File

@ -1,15 +1,16 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.cli.utils.ProjectCloudExample;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
/** /**
* CLI 命令创建新的项目骨架 * CLI 命令初始化项目配置文件
* <p> * <p>
* 用于快速初始化标准目录结构和 DSL 配置文件project.cloud * 用于快速生成 DSL 配置文件project.cloud
* </p> * </p>
* *
* <pre> * <pre>
@ -36,7 +37,7 @@ public final class InitCommand implements CLICommand {
*/ */
@Override @Override
public String description() { public String description() {
return "Initialize a new project skeleton with directory structure and project.cloud file."; return "Initialize a new project with project.cloud file.";
} }
/** /**
@ -44,11 +45,11 @@ public final class InitCommand implements CLICommand {
*/ */
@Override @Override
public void printUsage() { public void printUsage() {
System.out.println("Usage: snow init [--lang <lang>]"); System.out.println("Usage: snow init");
} }
/** /**
* 执行项目初始化流程创建 src 目录和 DSL 配置文件 * 执行项目初始化流程创建 DSL 配置文件
* *
* @param args CLI 传入的参数数组 * @param args CLI 传入的参数数组
* @return 执行结果码0 表示成功 * @return 执行结果码0 表示成功
@ -56,19 +57,11 @@ public final class InitCommand implements CLICommand {
*/ */
@Override @Override
public int execute(String[] args) throws Exception { public int execute(String[] args) throws Exception {
// 生成 skeleton `.cloud` 文件 src 目录 // 生成 `.cloud` 文件
Path dir = Paths.get(".").toAbsolutePath(); Path dir = Paths.get(".").toAbsolutePath();
Files.createDirectories(dir.resolve("src"));
Path dsl = dir.resolve("project.cloud"); Path dsl = dir.resolve("project.cloud");
if (Files.notExists(dsl)) { if (Files.notExists(dsl)) {
Files.writeString(dsl, """ Files.writeString(dsl, ProjectCloudExample.getProjectCloud());
# Generated by snow init
project {
group = "com.example"
artifact = "demo-app"
version = "0.0.1-SNAPSHOT"
}
""");
System.out.println("[init] created " + dsl); System.out.println("[init] created " + dsl);
} else { } else {
System.out.println("[init] project.cloud already exists"); System.out.println("[init] project.cloud already exists");

View File

@ -1,6 +1,6 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.pkg.dsl.CloudDSLParser; import org.jcnc.snow.pkg.dsl.CloudDSLParser;
import org.jcnc.snow.pkg.model.Project; import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.pkg.resolver.DependencyResolver; import org.jcnc.snow.pkg.resolver.DependencyResolver;

View File

@ -1,6 +1,6 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.pkg.dsl.CloudDSLParser; import org.jcnc.snow.pkg.dsl.CloudDSLParser;
import org.jcnc.snow.pkg.lifecycle.LifecycleManager; import org.jcnc.snow.pkg.lifecycle.LifecycleManager;
import org.jcnc.snow.pkg.lifecycle.LifecyclePhase; import org.jcnc.snow.pkg.lifecycle.LifecyclePhase;

View File

@ -1,25 +1,26 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.api.CLICommand;
import org.jcnc.snow.vm.VMLauncher; import org.jcnc.snow.pkg.tasks.RunTask;
/** /**
* CLI 命令运行已编译的 VM 字节码文件.water * CLI 命令运行已编译的 VM 字节码文件.water
* <p> * <p>
* 用于执行 VM 程序文件支持传递额外 VM 参数实际运行由 {@link VMLauncher#main(String[])} 完成 * 仅解析参数并委托给 {@link RunTask}
* VM 运行逻辑下沉至 pkg 保持 CLI 无状态薄封装
* </p> * </p>
* *
* <pre> * <pre>
* 用法示例 * 用法示例
* $ snow run program.water [additional VM options] * $ snow run main.water
* </pre> * </pre>
*/ */
public final class RunCommand implements CLICommand { public final class RunCommand implements CLICommand {
/** /**
* 返回命令名用于 CLI 调用 * 返回命令名用于 CLI 调用
* *
* @return 命令名称字符串"run" * @return 命令名称 "run"
*/ */
@Override @Override
public String name() { public String name() {
@ -33,25 +34,15 @@ public final class RunCommand implements CLICommand {
*/ */
@Override @Override
public String description() { public String description() {
return "Execute compiled VM instructions."; return "Run the compiled VM bytecode file (.water)";
} }
/** /**
* 打印该命令的用法说明 * 执行运行任务
*/
@Override
public void printUsage() {
System.out.println("""
Usage: snow run <program.water> [additional VM options]
""");
}
/**
* 执行 run 命令运行指定的 VM 字节码文件
* *
* @param args 剩余参数不含命令名第一个应为 .water 文件路径其后为可选 VM 参数 * @param args CLI 传入的参数数组
* @return 0 表示执行成功1 表示参数错误 * @return 执行结果码0 表示成功 0 表示失败
* @throws Exception VM 启动或执行过程中可能抛出的异常 * @throws Exception 执行过程中出现错误时抛出
*/ */
@Override @Override
public int execute(String[] args) throws Exception { public int execute(String[] args) throws Exception {
@ -59,7 +50,17 @@ public final class RunCommand implements CLICommand {
printUsage(); printUsage();
return 1; return 1;
} }
VMLauncher.main(args); // 委托给 RunTask 执行字节码运行逻辑
new RunTask(args).run();
return 0; return 0;
} }
/**
* 打印命令用法信息
*/
@Override
public void printUsage() {
System.out.println("Usage:");
System.out.println(" snow run <program.water>");
}
} }

View File

@ -1,7 +1,7 @@
package org.jcnc.snow.cli.commands; package org.jcnc.snow.cli.commands;
import org.jcnc.snow.cli.CLICommand;
import org.jcnc.snow.cli.SnowCLI; import org.jcnc.snow.cli.SnowCLI;
import org.jcnc.snow.cli.api.CLICommand;
/** /**
* CLI 子命令输出当前 Snow 工具的版本号 * CLI 子命令输出当前 Snow 工具的版本号

View File

@ -1,6 +1,6 @@
package org.jcnc.snow.cli.utils; package org.jcnc.snow.cli.utils;
import org.jcnc.snow.cli.CLICommand; import org.jcnc.snow.cli.api.CLICommand;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;

View File

@ -0,0 +1,31 @@
package org.jcnc.snow.cli.utils;
public class ProjectCloudExample {
/**
* 工具类构造方法禁止实例化
*/
private ProjectCloudExample() {
// 工具类不允许实例化
}
/**
* 获取 main.snow 示例模块的内容字符串
*
* @return main.snow 示例模块的完整代码
*/
public static String getProjectCloud() {
return """
# Generated by snow init
project {
group = "com.example"
artifact = "demo-app"
version = "0.0.1-SNAPSHOT"
}
build {
srcDir = "src"
output = "build/demo-app"
}
""";
}
}

View File

@ -0,0 +1,64 @@
# Snow Build - 包管理模块
> Snow 构建工具中的 **pkg** 子系统 —— 负责解析 `.cloud` 配置、解析依赖、 orchestrate 构建生命周期并执行各阶段任务
## 项目简介
**包管理模块pkg** 是 Snow 构建工具的关键组成部分,承担“从配置到产物” 的整条流水线:
1. **DSL 解析**:读取并解析 `.cloud` 配置文件,生成统一的项目/依赖/构建配置模型;
2. **生命周期编排**:按 *INIT → RESOLVE\_DEPENDENCIES → COMPILE → PACKAGE → PUBLISH → CLEAN* 的顺序驱动构建流程;
3. **依赖解析与缓存**:按需下载缺失依赖并存入本地缓存,离线优先;
4. **任务执行**:在各生命周期阶段调用对应 `Task` 实现(清理、编译、打包、发布等);
5. **配置模板展开**:支持在配置中使用 `@{key}` 形式的占位符,并在构建前统一替换。
整个模块强调 **可扩展性****内聚职责**DSL → Model、Lifecycle → Task、Resolver → Repository 各自解耦,可独立演进。
## 核心功能
| 功能 | 关键类 | 说明 |
|-----------------|----------------------------------------------------------|-------------------------------------------------|
| **CloudDSL 解析** | `CloudDSLParser` | 支持区块语法、嵌套 build 展平、注释过滤,解析为扁平 `Map` |
| **模型对象** | `Project / Dependency / Repository / BuildConfiguration` | 只读数据类,提供静态工厂方法 *fromFlatMap()* |
| **生命周期管理** | `LifecycleManager`, `LifecyclePhase` | 注册并顺序执行阶段任务,阶段可扩展 |
| **任务体系** | `Task` + `*Task` 实现 | Clean / Compile / Package / Publish 四大内置任务,易于新增 |
| **依赖解析器** | `DependencyResolver` | 支持本地缓存、HTTP 下载、URI 解析、断点续传(基于 NIO |
| **模板变量替换** | `BuildConfiguration` | 在加载阶段把 `@{key}` 替换为外部属性值 |
## 模块结构
```
pkg/
├── dsl/
│ └── CloudDSLParser.java // .cloud DSL 解析
├── lifecycle/
│ ├── LifecycleManager.java // 阶段编排
│ └── LifecyclePhase.java // 阶段枚举
├── model/
│ ├── Project.java // 项目信息
│ ├── BuildConfiguration.java // 构建配置(支持变量)
│ ├── Dependency.java // 依赖描述
│ └── Repository.java // 仓库描述
├── resolver/
│ └── DependencyResolver.java // 依赖解析与缓存
└── tasks/
├── Task.java // 任务接口
├── CleanTask.java // 清理
├── CompileTask.java // 编译
├── PackageTask.java // 打包
└── PublishTask.java // 发布
```
## 典型流程概览
1. 解析配置
2. 注册任务并执行
3. 解析依赖(在 RESOLVE\_DEPENDENCIES 阶段内部)
## 开发环境
* JDK 24 或更高版本
* Maven 构建管理
* 推荐 IDEIntelliJ IDEA

View File

@ -10,61 +10,61 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* CloudDSLParser .cloud 配置文件解析器 * CloudDSL 配置文件解析器
* <p>
* 作用
* - 读取 Snow 构建工具自定义的 .cloud 文件
* - 将内容转换为内存中的 {@link Project} 模型
* <p>
* 解析规则
* <p>
* 1. 顶级区块projectpropertiesrepositoriesdependenciesbuild
* sectionName { 开始 } 结束
* <p>
* 2. 区块内部识别 key = value 形式的赋值
* <p>
* 3. build 区块允许嵌套内部键通过 . 展平
* 例如 compile.enabled = true
* <p> * <p>
* 负责将自定义的 .cloud 构建配置文件解析为 {@link Project} 模型
* </p>
*
* <ul>
* <li>顶级区块 projectpropertiesrepositoriesdependenciesbuild <code>sectionName &#123;</code> 开始 <code>&#125;</code> 结束</li>
* <li>区块内部只识别 <code>key = value</code> 赋值行尾可有 <code># 注释</code></li>
* <li>build 区块支持嵌套内部键通过 <code>.</code> 展平例如 <code>compile.enabled = true</code></li>
* <li><strong>新增</strong> <code>"value"</code> <code>'value'</code> 形式的字面量自动去引号调用方得到的均是不含引号的裸字符串</li>
* </ul>
*
* <pre>
* 示例 .cloud 文件片段
* project {
* group = com.example
* artifact = "demo-app"
* version = 1.0.0
* }
* </pre>
*/ */
public final class CloudDSLParser { public final class CloudDSLParser {
/* ---------- 正则表达式 ---------- */ /** 匹配 sectionName { 的行 */
/**
* 匹配 sectionName { 形式的行
*/
private static final Pattern SECTION_HEADER = Pattern.compile("^(\\w+)\\s*\\{\\s*$"); private static final Pattern SECTION_HEADER = Pattern.compile("^(\\w+)\\s*\\{\\s*$");
/** /**
* 匹配 key = value * 匹配 key = value 忽略行尾注释
* value 允许空格并忽略行尾 # 注释 * 使用非贪婪匹配 <code>.*?</code>确保 <code>value</code> 内部允许出现空格或 =
*/ */
private static final Pattern KEY_VALUE = Pattern.compile("^(\\w+)\\s*=\\s*([^#]+?)\\s*(?:#.*)?$"); private static final Pattern KEY_VALUE = Pattern.compile("^(\\w+)\\s*=\\s*(.*?)\\s*(?:#.*)?$");
/** /** 匹配仅为 } 的行 */
* 匹配仅包含 } 的行可有前后空白
*/
private static final Pattern BLOCK_END = Pattern.compile("^}\\s*$"); private static final Pattern BLOCK_END = Pattern.compile("^}\\s*$");
/** /** 工具类禁止实例化 */
* 私有构造禁止实例化 private CloudDSLParser() {}
*/
private CloudDSLParser() {
}
/** /**
* 解析指定 .cloud 文件并生成 {@link Project} 对象 * 解析指定 .cloud 文件为 {@link Project} 对象
* <ul>
* <li>遇到语法错误括号不配对无法识别的行时抛出异常</li>
* <li>支持嵌套区块和注释</li>
* <li>对字面量自动去除成对单/双引号</li>
* </ul>
* *
* @param path 文件路径 * @param path .cloud 文件路径
* @return 解析后的 Project * @return 解析得到的 Project 实例
* @throws IOException 文件读取失败 * @throws IOException 文件读取失败
* @throws IllegalStateException 语法错误如括号不匹配未知语句 * @throws IllegalStateException 文件内容格式非法或语法错误
*/ */
public static Project parse(Path path) throws IOException { public static Project parse(Path path) throws IOException {
Deque<String> sectionStack = new ArrayDeque<>(); // 记录当前区块层级 Deque<String> sectionStack = new ArrayDeque<>(); // 当前区块栈
Map<String, String> flatMap = new LinkedHashMap<>(); // 扁平化 key value Map<String, String> flatMap = new LinkedHashMap<>(); // 扁平化后的 key value
List<String> lines = Files.readAllLines(path); List<String> lines = Files.readAllLines(path);
@ -73,19 +73,19 @@ public final class CloudDSLParser {
lineNo++; lineNo++;
String line = raw.trim(); String line = raw.trim();
/* 1. 跳过空行和注释 */ // 跳过空行和注释
if (line.isEmpty() || line.startsWith("#")) { if (line.isEmpty() || line.startsWith("#")) {
continue; continue;
} }
/* 2. 区块起始sectionName { */ // 区块起始
Matcher sec = SECTION_HEADER.matcher(line); Matcher sec = SECTION_HEADER.matcher(line);
if (sec.matches()) { if (sec.matches()) {
sectionStack.push(sec.group(1)); sectionStack.push(sec.group(1));
continue; continue;
} }
/* 3. 区块结束:} */ // 区块结束
if (BLOCK_END.matcher(line).matches()) { if (BLOCK_END.matcher(line).matches()) {
if (sectionStack.isEmpty()) { if (sectionStack.isEmpty()) {
throw new IllegalStateException("" + lineNo + " 行出现未配对的 '}'"); throw new IllegalStateException("" + lineNo + " 行出现未配对的 '}'");
@ -94,13 +94,14 @@ public final class CloudDSLParser {
continue; continue;
} }
/* 4. 键值对key = value */ // 键值对
Matcher kv = KEY_VALUE.matcher(line); Matcher kv = KEY_VALUE.matcher(line);
if (kv.matches()) { if (kv.matches()) {
String key = kv.group(1).trim(); String key = kv.group(1).trim();
String value = kv.group(2).trim(); String value = kv.group(2).trim();
value = unquote(value); // 去除首尾成对引号
/* 4.1 计算前缀(栈倒序,即从外到内) */ // 计算区块前缀
String prefix = String.join(".", (Iterable<String>) sectionStack::descendingIterator); String prefix = String.join(".", (Iterable<String>) sectionStack::descendingIterator);
if (!prefix.isEmpty()) { if (!prefix.isEmpty()) {
key = prefix + "." + key; key = prefix + "." + key;
@ -110,16 +111,29 @@ public final class CloudDSLParser {
continue; continue;
} }
/* 5. 无法识别的行 */ // 无法识别的行
throw new IllegalStateException("无法解析第 " + lineNo + " 行: " + raw); throw new IllegalStateException("无法解析第 " + lineNo + " 行: " + raw);
} }
/* 6. 检查括号是否全部闭合 */ // 检查区块是否全部闭合
if (!sectionStack.isEmpty()) { if (!sectionStack.isEmpty()) {
throw new IllegalStateException("文件结束但区块未闭合:" + sectionStack); throw new IllegalStateException("文件结束但区块未闭合:" + sectionStack);
} }
/* 7. 构造 Project 模型 */ // 构建 Project 模型
return Project.fromFlatMap(flatMap); return Project.fromFlatMap(flatMap);
} }
/**
* 如果字符串首尾包裹成对单引号或双引号则去掉引号后返回否则直接返回原字符串
*/
private static String unquote(String s) {
if (s == null || s.length() < 2) return s;
char first = s.charAt(0);
char last = s.charAt(s.length() - 1);
if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) {
return s.substring(1, s.length() - 1);
}
return s;
}
} }

View File

@ -6,9 +6,10 @@ import java.util.EnumMap;
import java.util.Map; import java.util.Map;
/** /**
* 管理不同生命周期阶段与其对应任务的工具类 * 生命周期任务管理器<br>
* 用于管理不同生命周期阶段与其对应 {@link Task}并支持顺序执行所有已注册任务
* <p> * <p>
* 可为每个 {@link LifecyclePhase} 注册对应的 {@link Task}按阶段顺序执行所有任务 * 可为每个 {@link LifecyclePhase} 注册对应的 {@link Task}在构建/部署流程中自动执行
* </p> * </p>
* *
* <pre> * <pre>
@ -20,9 +21,7 @@ import java.util.Map;
*/ */
public final class LifecycleManager { public final class LifecycleManager {
/** /** 生命周期阶段与对应任务的映射关系 */
* 存储生命周期阶段与对应任务的映射关系
*/
private final Map<LifecyclePhase, Task> tasks = new EnumMap<>(LifecyclePhase.class); private final Map<LifecyclePhase, Task> tasks = new EnumMap<>(LifecyclePhase.class);
/** /**
@ -45,11 +44,13 @@ public final class LifecycleManager {
/** /**
* {@link LifecyclePhase} 声明顺序依次执行所有已注册任务 * {@link LifecyclePhase} 声明顺序依次执行所有已注册任务
* <p> * <ul>
* 未注册任务的阶段将被跳过任务执行前会打印阶段名 * <li>未注册任务的阶段会被自动跳过</li>
* </p> * <li>每个任务执行前会输出当前阶段名</li>
* <li>执行中遇到异常将立即抛出并终止后续执行</li>
* </ul>
* *
* @throws Exception 若某个任务执行时抛出异常将直接抛出并终止后续任务执行 * @throws Exception 若某个任务执行时抛出异常将直接抛出
*/ */
public void executeAll() throws Exception { public void executeAll() throws Exception {
for (LifecyclePhase phase : LifecyclePhase.values()) { for (LifecyclePhase phase : LifecyclePhase.values()) {

View File

@ -1,7 +1,10 @@
package org.jcnc.snow.pkg.lifecycle; package org.jcnc.snow.pkg.lifecycle;
/** /**
* 定义了典型软件包生命周期的各个阶段 * 定义典型软件包生命周期的各个阶段枚举
* <p>
* 用于区分构建依赖发布等不同阶段的任务调度与管理
* </p>
*/ */
public enum LifecyclePhase { public enum LifecyclePhase {
/** 初始化阶段 */ /** 初始化阶段 */

View File

@ -6,16 +6,16 @@ import java.util.HashMap;
/** /**
* 构建配置对象封装构建过程中的所有选项 * 构建配置对象封装构建过程中的所有选项
* <p> * <p>
* 支持基于模板变量形如{@code @{key}}的选项值替换 * 支持模板变量形如 <code>@{key}</code>的值自动替换
* </p> * </p>
*/ */
public final class BuildConfiguration { public final class BuildConfiguration {
/** 存储配置项的键值对 */ /** 存储所有配置项 */
private final Map<String, String> options; private final Map<String, String> options;
/** /**
* 私有构造函数于初始化配置项 * 私有构造函数仅供工厂方法调
* *
* @param options 配置项键值对 * @param options 配置项键值对
*/ */
@ -24,14 +24,15 @@ public final class BuildConfiguration {
} }
/** /**
* 基于原始配置项和变量属性创建配置对象 * 基于原始配置项和属性集创建配置对象
* <p> * <ul>
* 会将原始配置中的所有值中的{@code @{key}}模板替换为属性props中对应的值 * <li>会将所有值中的 <code>@{key}</code> 模板变量替换为 props 中对应的值</li>
* </p> * <li>属性未匹配到时保留原模板</li>
* </ul>
* *
* @param flat 原始配置项值中可包含模板变量@{name} * @param flat 原始配置项值中可包含模板变量
* @param props 用于替换模板变量的属性集 * @param props 变量替换用的属性集
* @return 构建完成的配置对象 * @return 处理后生成的配置对象
*/ */
public static BuildConfiguration fromFlatMap(Map<String, String> flat, Map<String, String> props) { public static BuildConfiguration fromFlatMap(Map<String, String> flat, Map<String, String> props) {
Map<String, String> resolved = new HashMap<>(); Map<String, String> resolved = new HashMap<>();
@ -46,11 +47,11 @@ public final class BuildConfiguration {
} }
/** /**
* 获取指定key对应的配置值 * 获取指定配置项的
* *
* @param key 配置项名称 * @param key 配置项名称
* @param def 默认值若未找到key则返回此值 * @param def 默认值未找到时返回
* @return 配置项对应若不存在则返回默认值 * @return 配置项若不存在则返回默认值
*/ */
public String get(String key, String def) { public String get(String key, String def) {
return options.getOrDefault(key, def); return options.getOrDefault(key, def);

View File

@ -5,18 +5,23 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* 表示一个 Maven 风格的 group:artifact:version 依赖坐标 * group:artifact:version 依赖坐标对象
* <p> * <p>
* 支持通过占位符和属性映射进行动态变量替换可用于构建工具或依赖管理场景 * 支持占位符和属性映射进行动态变量替换适用于 Snow 语言包管理和源码依赖场景
* </p> * </p>
* *
* <pre> * <pre>
* 示例 * 示例用法
* Dependency dep = Dependency.fromString( * Dependency dep = Dependency.fromString(
* "core", "com.example:core:@{version}", * "core", "com.example:core:@{version}",
* Map.of("version", "1.2.3") * Map.of("version", "1.2.3")
* ); * );
* </pre> * </pre>
*
* @param id 依赖唯一标识
* @param group 组织/分组名
* @param artifact 构件名
* @param version 版本号
*/ */
public record Dependency( public record Dependency(
String id, String id,
@ -25,25 +30,23 @@ public record Dependency(
String version String version
) { ) {
/** /** 匹配 group:artifact:version 格式的正则表达式。 */
* 用于匹配 group:artifact:version 格式的正则表达式
*/
private static final Pattern GAV = Pattern.compile("([^:]+):([^:]+):(.+)"); private static final Pattern GAV = Pattern.compile("([^:]+):([^:]+):(.+)");
/** /**
* 根据字符串坐标和属性映射创建依赖对象 * 根据字符串坐标和属性映射创建依赖对象
* <p> * <p>
* 坐标中的占位符 {@code @{key}} 会用 props 中对应的值替换 * 坐标中的占位符 <code>@{key}</code>会用 props 中对应的值替换
* </p> * </p>
* *
* @param id 依赖唯一标识 * @param id 依赖唯一标识
* @param coordinate 依赖坐标字符串格式为 group:artifact:version支持变量占位符 * @param coordinate 坐标字符串格式为 group:artifact:version支持占位符
* @param props 占位符替换属性映射 * @param props 占位符替换属性映射
* @return 解析后的 Dependency 实例 * @return 解析后的 Dependency 实例
* @throws IllegalArgumentException 如果坐标格式非法 * @throws IllegalArgumentException 坐标格式非法时抛出
*/ */
public static Dependency fromString(String id, String coordinate, Map<String, String> props) { public static Dependency fromString(String id, String coordinate, Map<String, String> props) {
// 替换 @{prop} 占位符 // 替换占位符
String resolved = coordinate; String resolved = coordinate;
for (Map.Entry<String, String> p : props.entrySet()) { for (Map.Entry<String, String> p : props.entrySet()) {
resolved = resolved.replace("@{" + p.getKey() + "}", p.getValue()); resolved = resolved.replace("@{" + p.getKey() + "}", p.getValue());
@ -57,23 +60,23 @@ public record Dependency(
} }
/** /**
* 生成依赖对应的标准仓库 jar 路径 * 生成依赖对应的源码文件路径
* <p> * <p>
* 路径格式通常为groupId/artifactId/version/artifactId-version.jar<br> * 路径格式groupId/artifactId/version/artifactId.snow
* 例如com/example/core/1.2.3/core-1.2.3.jar * 例如com/example/core/1.2.3/core.snow
* </p> * </p>
* *
* @return 仓库 jar 文件的相对路径 * @return 仓库源码文件的相对路径
*/ */
public String toPath() { public String toPath() {
String groupPath = group.replace('.', '/'); String groupPath = group.replace('.', '/');
return groupPath + "/" + artifact + "/" + version + "/" + artifact + "-" + version + ".jar"; return groupPath + "/" + artifact + "/" + version + "/" + artifact + ".snow";
} }
/** /**
* 返回该依赖的 group:artifact:version 字符串表示 * 返回该依赖的 group:artifact:version 字符串表示
* *
* @return Maven 坐标字符串 * @return 坐标字符串
*/ */
@Override @Override
public String toString() { public String toString() {

View File

@ -8,42 +8,41 @@ import java.util.Map;
/** /**
* 表示一个软件包/模块的项目信息包括元数据属性仓库依赖和构建配置等 * 表示一个软件包/模块的项目信息包括元数据属性仓库依赖和构建配置等
* <p> * <p>
* 本类为不可变对象仅提供gettersetter * 本类为不可变对象仅提供 getter 方法 setter<br>
* 支持通过 {@link #fromFlatMap(Map)} 静态工厂方法从扁平Map快速创建 * 支持通过 {@link #fromFlatMap(Map)} 静态工厂方法从扁平 Map 快速创建实例
* </p> * </p>
* *
* <pre> * <pre>
* Map&lt;String,String&gt; map = ...; * Map&lt;String, String&gt; map = ...;
* Project project = Project.fromFlatMap(map); * Project project = Project.fromFlatMap(map);
* </pre> * </pre>
*/ */
public final class Project { public final class Project {
/** 组织/分组名(如com.example */ /** 组织/分组名(如 com.example */
private final String group; private final String group;
/** 构件/模块名(如app-core */ /** 构件/模块名(如 app-core */
private final String artifact; private final String artifact;
/** 项目展示名称 */ /** 项目展示名称 */
private final String name; private final String name;
/** 版本号(如1.0.0 */ /** 版本号(如 1.0.0 */
private final String version; private final String version;
/** 项目描述 */ /** 项目描述 */
private final String description; private final String description;
/** 许可证标识 */ /** 许可证标识 */
private final String license; private final String license;
/** 项目主页URL */ /** 项目主页 URL */
private final String homepage; private final String homepage;
/** 额外属性(不影响主字段,可用于模板/占位符) */ /** 额外属性(不影响主字段,可用于模板/占位符) */
private final Map<String, String> properties; private final Map<String, String> properties;
/** 仓库列表(仓库ID -> 仓库对象) */ /** 仓库列表(仓库 ID -> 仓库对象) */
private final Map<String, Repository> repositories; private final Map<String, Repository> repositories;
/** 依赖列表 */ /** 依赖列表 */
private final List<Dependency> dependencies; private final List<Dependency> dependencies;
/** 构建配置 */ /** 构建配置 */
private final BuildConfiguration build; private final BuildConfiguration build;
/** /**
* 构造函数私有请使用 {@link #fromFlatMap(Map)} 创建实例 * 构造函数私有请使用 {@link #fromFlatMap(Map)} 创建实例
*/ */
@ -74,7 +73,7 @@ public final class Project {
} }
/** /**
* 通过扁平Map创建 Project 实例约定key格式如下 * 通过扁平 Map 创建 Project 实例key 格式约定如下
* <ul> * <ul>
* <li>project.* 项目元数据</li> * <li>project.* 项目元数据</li>
* <li>properties.* 额外属性</li> * <li>properties.* 额外属性</li>
@ -83,12 +82,11 @@ public final class Project {
* <li>build.* 构建配置</li> * <li>build.* 构建配置</li>
* </ul> * </ul>
* *
* @param map 扁平的配置map * @param map 扁平的配置 map
* @return Project 实例 * @return Project 实例
*/ */
public static Project fromFlatMap(Map<String, String> map) { public static Project fromFlatMap(Map<String, String> map) {
// 1. 基本元数据
// 1. simple project metadata
String group = map.getOrDefault("project.group", "unknown"); String group = map.getOrDefault("project.group", "unknown");
String artifact = map.getOrDefault("project.artifact", "unknown"); String artifact = map.getOrDefault("project.artifact", "unknown");
String name = map.getOrDefault("project.name", artifact); String name = map.getOrDefault("project.name", artifact);
@ -123,14 +121,13 @@ public final class Project {
} }
}); });
// 5. build.* simply hand the subtree map // 5. build.*
Map<String, String> buildMap = new LinkedHashMap<>(); Map<String, String> buildMap = new LinkedHashMap<>();
map.forEach((k, v) -> { map.forEach((k, v) -> {
if (k.startsWith("build.")) { if (k.startsWith("build.")) {
buildMap.put(k.substring("build.".length()), v); buildMap.put(k.substring("build.".length()), v);
} }
}); });
BuildConfiguration buildCfg = BuildConfiguration.fromFlatMap(buildMap, props); BuildConfiguration buildCfg = BuildConfiguration.fromFlatMap(buildMap, props);
return new Project(group, artifact, name, version, description, license, homepage, props, repos, deps, buildCfg); return new Project(group, artifact, name, version, description, license, homepage, props, repos, deps, buildCfg);
@ -161,12 +158,12 @@ public final class Project {
return description; return description;
} }
/** @return 许可证 */ /** @return 许可证标识 */
public String getLicense() { public String getLicense() {
return license; return license;
} }
/** @return 项目主页URL */ /** @return 项目主页 URL */
public String getHomepage() { public String getHomepage() {
return homepage; return homepage;
} }

View File

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

View File

@ -6,17 +6,27 @@ import java.nio.file.Path;
import java.util.Comparator; import java.util.Comparator;
/** /**
* 清理构建输出目录 build dist的任务实现 * 用于清理构建输出目录 {@code build} {@code dist}的任务实现
* <p> * <p>
* 实现 {@link Task} 接口常用于构建流程的清理阶段 * 实现 {@link Task} 接口常用于自动化构建流程的清理阶段负责递归删除指定的构建产物目录
* </p> * </p>
* <p>
* 本类为无状态实现线程安全
* </p>
*
* <p><b>示例用法</b></p>
* <pre>{@code
* Task clean = new CleanTask();
* clean.run();
* }</pre>
*/ */
public final class CleanTask implements Task { public final class CleanTask implements Task {
/** /**
* 执行清理任务删除 "build" "dist" 目录及其所有内容 * 执行清理任务递归删除当前目录下的 {@code build} {@code dist} 目录及其所有内容
* 如果目标目录不存在则跳过不处理
* *
* @throws IOException 删除目录过程中出现 IO 错误时抛出 * @throws IOException 删除目录或文件过程中发生 IO 错误时抛出
*/ */
@Override @Override
public void run() throws IOException { public void run() throws IOException {
@ -27,10 +37,15 @@ public final class CleanTask implements Task {
/** /**
* 递归删除指定目录及其所有子文件和子目录 * 递归删除指定目录及其所有子文件和子目录
* 使用 try-with-resources 自动关闭文件流避免资源泄漏 * <p>
* 若目录不存在则直接返回
* </p>
* <p>
* 内部使用 try-with-resources 保证文件流自动关闭避免资源泄漏
* </p>
* *
* @param dir 需要删除的目录路径 * @param dir 需要删除的目录路径
* @throws IOException 删除过程中出现 IO 错误时抛出 * @throws IOException 删除目录或文件过程中发生 IO 错误时抛出
*/ */
private void deleteDir(Path dir) throws IOException { private void deleteDir(Path dir) throws IOException {
if (Files.notExists(dir)) return; if (Files.notExists(dir)) return;

View File

@ -1,43 +1,264 @@
package org.jcnc.snow.pkg.tasks; package org.jcnc.snow.pkg.tasks;
import org.jcnc.snow.cli.commands.CompileCommand;
import org.jcnc.snow.pkg.model.Project; import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.compiler.backend.alloc.RegisterAllocator;
import org.jcnc.snow.compiler.backend.builder.VMCodeGenerator;
import org.jcnc.snow.compiler.backend.builder.VMProgramBuilder;
import org.jcnc.snow.compiler.backend.core.InstructionGenerator;
import org.jcnc.snow.compiler.backend.generator.InstructionGeneratorProvider;
import org.jcnc.snow.compiler.ir.builder.IRProgramBuilder;
import org.jcnc.snow.compiler.ir.core.IRFunction;
import org.jcnc.snow.compiler.ir.core.IRInstruction;
import org.jcnc.snow.compiler.ir.core.IRProgram;
import org.jcnc.snow.compiler.ir.value.IRVirtualRegister;
import org.jcnc.snow.compiler.lexer.core.LexerEngine;
import org.jcnc.snow.compiler.parser.ast.base.Node;
import org.jcnc.snow.compiler.parser.context.ParserContext;
import org.jcnc.snow.compiler.parser.core.ParserEngine;
import org.jcnc.snow.compiler.parser.function.ASTPrinter;
import org.jcnc.snow.compiler.semantic.core.SemanticAnalyzerRunner;
import org.jcnc.snow.vm.VMLauncher;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*;
/** /**
* 编译项目源代码的任务实现 * CLI 任务编译 .snow 源文件为 VM 字节码.water 文件
* <p> * <p>
* 实现 {@link Task} 接口用于构建流程中的编译阶段当前仅为示例未集成实际编译器 * 支持单文件多文件和目录递归编译并可在编译后立即运行虚拟机<br>
* 命令行参数支持 run-o-d 及直接指定源文件
* </p> * </p>
*
* <pre>
* 用法示例
* $ snow compile [run] [-o &lt;name&gt;] [-d &lt;srcDir&gt;] [file1.snow file2.snow ...]
* </pre>
*/ */
public final class CompileTask implements Task { public final class CompileTask implements Task {
/** 项目信息 */
/** 待编译的项目 */
private final Project project; private final Project project;
/** 原始命令行参数 */
private final String[] args;
/** /**
* 创建 CompileTask 实例 * 创建一个编译任务
* *
* @param project 目标项目 * @param project 项目信息对象
* @param args 命令行参数数组
*/ */
public CompileTask(Project project) { public CompileTask(Project project, String[] args) {
this.project = project; this.project = project;
this.args = args;
} }
/** /**
* 执行编译任务打印源代码目录和输出目录 * 创建一个不带参数的编译任务
* <p>
* 实际编译尚未实现TODO
* </p>
* *
* @throws Exception 预留未来集成编译器可能抛出异常 * @param project 项目信息对象
*/
public CompileTask(Project project) {
this(project, new String[0]);
}
/**
* 执行编译任务该方法会解析参数并调用 {@link #execute(String[])} 进行实际编译流程
*
* @throws Exception 执行过程中出现任意异常时抛出
*/ */
@Override @Override
public void run() throws Exception { public void run() throws Exception {
// 获取源码目录和输出目录默认分别为 "src" "build/classes" execute(this.args);
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: 集成实际的编译器 * 编译 .snow 源文件为 VM 字节码并可选择立即运行
* <ul>
* <li>支持参数 run编译后运行-o输出文件名-d递归目录直接指定多个源文件</li>
* <li>输出源代码ASTIR最终 VM code并写出 .water 文件</li>
* </ul>
*
* @param args 命令行参数数组
* @return 0 表示成功 0 表示失败
* @throws Exception 编译或写入过程中出现异常时抛出
*/
public int execute(String[] args) throws Exception {
// ---------------- 解析命令行参数 ----------------
boolean runAfterCompile = false;
String outputName = null;
Path dir = null;
List<Path> sources = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
switch (arg) {
case "run" -> runAfterCompile = true;
case "-o" -> {
if (i + 1 < args.length) outputName = args[++i];
else {
System.err.println("Missing argument for -o");
new CompileCommand().printUsage();
return 1;
}
}
case "-d" -> {
if (i + 1 < args.length) dir = Path.of(args[++i]);
else {
System.err.println("Missing argument for -d");
new CompileCommand().printUsage();
return 1;
}
}
default -> {
if (arg.endsWith(".snow")) {
sources.add(Path.of(arg));
} else {
System.err.println("Unknown option or file: " + arg);
new CompileCommand().printUsage();
return 1;
}
}
}
}
// --------- 如果指定了目录则递归收集所有 *.snow ---------
if (dir != null) {
if (!Files.isDirectory(dir)) {
System.err.println("Not a directory: " + dir);
return 1;
}
try (var stream = Files.walk(dir)) {
stream.filter(p -> p.toString().endsWith(".snow"))
.sorted() // 确保稳定顺序
.forEach(sources::add);
}
}
if (sources.isEmpty()) {
System.err.println("No .snow source files found.");
return 1;
}
// 多文件但未指定 -o 且非目录编译 提示必须指定输出名
if (sources.size() > 1 && outputName == null && dir == null) {
System.err.println("Please specify output name using -o <name>");
return 1;
}
// ---------------- 1. 词法/语法分析并打印源代码 ----------------
List<Node> allAst = new ArrayList<>();
System.out.println("## 编译器输出");
System.out.println("### Snow 源代码");
for (Path p : sources) {
if (!Files.exists(p)) {
System.err.println("File not found: " + p);
return 1;
}
String code = Files.readString(p, StandardCharsets.UTF_8);
// 打印源码
System.out.println("#### " + p.getFileName());
System.out.println(code);
// 词法语法分析
LexerEngine lexer = new LexerEngine(code, p.toString());
ParserContext ctx = new ParserContext(lexer.getAllTokens(), p.toString());
allAst.addAll(new ParserEngine(ctx).parse());
}
// ---------------- 2. 语义分析 ----------------
SemanticAnalyzerRunner.runSemanticAnalysis(allAst, false);
// ---------------- 3. AST IR并将 main 函数移动至首位 ----------------
IRProgram program = new IRProgramBuilder().buildProgram(allAst);
program = reorderForEntry(program);
// 打印 AST IR
System.out.println("### AST");
ASTPrinter.printJson(allAst);
System.out.println("### IR");
System.out.println(program);
// ---------------- 4. IR VM 指令 ----------------
VMProgramBuilder builder = new VMProgramBuilder();
List<InstructionGenerator<? extends IRInstruction>> generators =
InstructionGeneratorProvider.defaultGenerators();
for (IRFunction fn : program.functions()) {
Map<IRVirtualRegister, Integer> slotMap =
new RegisterAllocator().allocate(fn);
new VMCodeGenerator(slotMap, builder, generators).generate(fn);
}
List<String> finalCode = builder.build();
System.out.println("### VM code");
finalCode.forEach(System.out::println);
// ---------------- 5. 写出 .water 文件 ----------------
Path outputFile = deriveOutputPath(sources, outputName, dir);
Files.write(outputFile, finalCode, StandardCharsets.UTF_8);
System.out.println("Written to " + outputFile.toAbsolutePath());
// ---------------- 6. 可选立即运行 VM ----------------
if (runAfterCompile) {
System.out.println("\n=== Launching VM ===");
VMLauncher.main(new String[]{outputFile.toString()});
}
return 0;
}
/**
* 推断 .water 输出文件名
* <ul>
* <li>如果指定 -o直接使用该名称</li>
* <li>目录编译时取目录名</li>
* <li>单文件编译时取文件名去掉 .snow 后缀</li>
* <li>否则默认 "program"</li>
* </ul>
*
* @param sources 源文件路径列表
* @param outName 输出文件名如有指定否则为 null
* @param dir 源码目录如有指定否则为 null
* @return 推断出的输出文件路径.water 文件
*/
private static Path deriveOutputPath(List<Path> sources, String outName, Path dir) {
String base;
if (outName != null) {
base = outName;
} else if (dir != null) {
base = dir.getFileName().toString();
} else if (sources.size() == 1) {
base = sources.getFirst().getFileName().toString()
.replaceFirst("\\.snow$", "");
} else {
base = "program";
}
return Path.of(base + ".water");
}
/**
* main 函数调整至函数列表首位确保程序入口为 PC=0
*
* @param in 原始 IRProgram
* @return 调整入口后的 IRProgram
*/
private static IRProgram reorderForEntry(IRProgram in) {
List<IRFunction> ordered = new ArrayList<>(in.functions());
for (int i = 0; i < ordered.size(); i++) {
if ("main".equals(ordered.get(i).name())) {
Collections.swap(ordered, 0, i);
break;
}
}
IRProgram out = new IRProgram();
ordered.forEach(out::add);
return out;
} }
} }

View File

@ -0,0 +1,95 @@
package org.jcnc.snow.pkg.tasks;
import org.jcnc.snow.pkg.model.Project;
import org.jcnc.snow.pkg.utils.SnowExample;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* 项目脚手架生成任务<br>
* 根据 {@link Project} 元数据自动创建标准项目目录结构并生成示例入口文件
* <code>main.snow</code>
*
* <p>
* 生成内容包括
* <ul>
* <li><code>src/</code> 源码根目录</li>
* <li><code>src/{group package}/</code> <code>project.group</code> 创建的包路径
* <code>com.example</code> <code>src/com/example/</code></li>
* <li><code>test/</code> 测试源码目录</li>
* <li><code>build/</code> 编译输出目录</li>
* <li><code>dist/</code> 打包输出目录</li>
* <li><code>src/{group package}/main.snow</code> 示例入口文件</li>
* </ul>
* 如目录或入口文件已存在则自动跳过不会覆盖
* </p>
*/
public final class GenerateTask implements Task {
/** 项目信息元数据 */
private final Project project;
/**
* 创建项目生成任务
*
* @param project 项目信息元数据对象
*/
public GenerateTask(Project project) {
this.project = project;
}
/**
* 执行脚手架生成流程创建标准目录和入口示例文件
* <ul>
* <li>若相关目录不存在则创建</li>
* <li>若设置了 <code>project.group</code>则在 <code>src/</code> 下新建对应包路径</li>
* <li>示例入口文件 <code>main.snow</code> 写入包路径目录</li>
* <li>生成过程输出进度信息</li>
* </ul>
*
* @throws IOException 创建目录或写入文件时发生 IO 错误时抛出
*/
@Override
public void run() throws IOException {
Path root = Paths.get(".").toAbsolutePath();
/* ---------- 1. 构造待创建目录列表 ---------- */
List<Path> dirs = new ArrayList<>(List.of(
root.resolve("src"),
root.resolve("test"),
root.resolve("build"),
root.resolve("dist")
));
/* ---------- 2. 处理 group追加包目录 ---------- */
String group = project != null ? project.getGroup() : null;
Path srcDir = root.resolve("src");
Path packageDir = srcDir; // 默认直接在 src
if (group != null && !group.isBlank()) {
packageDir = srcDir.resolve(group.replace('.', '/'));
dirs.add(packageDir);
}
/* ---------- 3. 创建目录 ---------- */
for (Path dir : dirs) {
if (Files.notExists(dir)) {
Files.createDirectories(dir);
System.out.println("[generate] created directory " + root.relativize(dir));
}
}
/* ---------- 4. 写入示例入口文件 main.snow ---------- */
Path mainSnow = packageDir.resolve("main.snow");
if (Files.notExists(mainSnow)) {
Files.writeString(mainSnow, SnowExample.getMainModule());
System.out.println("[generate] created " + root.relativize(mainSnow));
}
System.out.println("[generate] project scaffold is ready.");
}
}

View File

@ -9,29 +9,38 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
/** /**
* 项目打包任务将编译输出目录 build/classes打包为 .ice 文件 * 项目打包任务将编译输出目录 <code>build/classes</code>打包为 .ice 文件
* <p> * <p>
* 实现 {@link Task} 接口通常用于构建流程中的打包阶段 * 实现 {@link Task} 接口通常用于构建流程的打包阶段<br>
* 只会打包 build/classes 目录下所有文件不含其他目录
* </p>
* <p>
* 输出文件位于 dist 目录命名为 artifact-version.ice
* </p> * </p>
*/ */
public final class PackageTask implements Task { public final class PackageTask implements Task {
/** 目标项目 */ /** 目标项目元数据 */
private final Project project; private final Project project;
/** /**
* 创建 PackageTask 实例 * 创建打包任务
* *
* @param project 目标项目 * @param project 目标项目元数据
*/ */
public PackageTask(Project project) { public PackageTask(Project project) {
this.project = project; this.project = project;
} }
/** /**
* 执行打包任务将编译输出目录压缩为 artifact-version.ice 文件 * 执行打包任务 build/classes 目录下所有文件压缩为 dist/artifact-version.ice
* <ul>
* <li>若输出目录 dist 不存在会自动创建</li>
* <li>只打包 build/classes 下所有普通文件保持相对目录结构</li>
* <li>如无 build/classes 则不会生成包</li>
* </ul>
* *
* @throws Exception 打包过程中出现 IO 或其他异常时抛出 * @throws Exception 打包过程中发生 IO 或其他异常时抛出
*/ */
@Override @Override
public void run() throws Exception { public void run() throws Exception {
@ -43,15 +52,15 @@ public final class PackageTask implements Task {
Path packageFile = distDir.resolve(fileName); Path packageFile = distDir.resolve(fileName);
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(packageFile))) { try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(packageFile))) {
// 仅将编译输出目录打包 // 仅将 build/classes 目录打包
Path classesDir = Path.of("build/classes"); Path classesDir = Path.of("build/classes");
if (Files.exists(classesDir)) { if (Files.exists(classesDir)) {
// 使用 try-with-resources 正确关闭 Stream<Path> // 遍历所有文件并写入 zip
try (Stream<Path> stream = Files.walk(classesDir)) { try (Stream<Path> stream = Files.walk(classesDir)) {
stream.filter(Files::isRegularFile) stream.filter(Files::isRegularFile)
.forEach(p -> { .forEach(p -> {
try { try {
// 将文件以相对路径加入压缩包 // classesDir 为根的相对路径存入 zip
zos.putNextEntry(new ZipEntry(classesDir.relativize(p).toString())); zos.putNextEntry(new ZipEntry(classesDir.relativize(p).toString()));
Files.copy(p, zos); Files.copy(p, zos);
zos.closeEntry(); zos.closeEntry();

View File

@ -5,25 +5,26 @@ import org.jcnc.snow.pkg.model.Project;
/** /**
* 发布项目构件到远程仓库的任务实现 * 发布项目构件到远程仓库的任务实现
* <p> * <p>
* 实现 {@link Task} 接口通常用于构建流程中的发布阶段目前仅为演示尚未实现实际上传 * 实现 {@link Task} 接口通常用于构建流程中的发布阶段
* 当前仅输出发布提示尚未实现实际上传功能
* </p> * </p>
*/ */
public final class PublishTask implements Task { public final class PublishTask implements Task {
/** 目标项目 */ /** 目标项目元数据 */
private final Project project; private final Project project;
/** /**
* 创建 PublishTask 实例 * 创建发布任务
* *
* @param project 目标项目 * @param project 目标项目元数据
*/ */
public PublishTask(Project project) { public PublishTask(Project project) {
this.project = project; this.project = project;
} }
/** /**
* 执行发布任务当前仅打印发布提示未实现实际上传逻辑 * 执行发布任务目前仅打印发布提示信息未实现实际上传逻辑
* *
* @throws Exception 预留未来实现上传逻辑时可能抛出异常 * @throws Exception 预留未来实现上传逻辑时可能抛出异常
*/ */

View File

@ -0,0 +1,44 @@
package org.jcnc.snow.pkg.tasks;
import org.jcnc.snow.vm.VMLauncher;
/**
* 任务执行已编译的 VM 字节码文件.water
* <p>
* 作为 CLIIDE 插件或其他宿主环境启动虚拟机的统一入口<br>
* 通过调用 {@link VMLauncher#main(String[])} 启动 VM 并执行指定程序
* </p>
*/
public final class RunTask implements Task {
/**
* 传递给虚拟机的完整参数列表第一个应为 .water 文件路径
*/
private final String[] args;
/**
* 创建运行任务
*
* @param args VM 参数数组第一个为 .water 程序路径其后为可选参数
*/
public RunTask(String... args) {
this.args = args;
}
/**
* 执行运行任务内部委托 {@link VMLauncher#main(String[])} 启动 VM
* <ul>
* <li>如果参数为空则抛出 {@link IllegalArgumentException}</li>
* <li>异常由虚拟机本身抛出直接透出</li>
* </ul>
*
* @throws Exception 虚拟机启动或运行期间抛出的异常
*/
@Override
public void run() throws Exception {
if (args == null || args.length == 0) {
throw new IllegalArgumentException("VM run requires at least the program file path.");
}
VMLauncher.main(args);
}
}

View File

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

View File

@ -0,0 +1,58 @@
package org.jcnc.snow.pkg.utils;
/**
* 示例模块模板工具类提供 main.snow 的标准示例代码字符串
* <p>
* 用于项目脚手架生成或帮助用户快速上手 .snow 语言
* </p>
*/
public final class SnowExample {
/**
* 工具类构造方法禁止实例化
*/
private SnowExample() {
// 工具类不允许实例化
}
/**
* 获取 main.snow 示例模块的内容字符串
*
* @return main.snow 示例模块的完整代码
*/
public static String getMainModule() {
return """
module: Math
function: main
parameter:
return_type: int
body:
Math.factorial(6)
return 0
end body
end function
function: factorial
parameter:
declare n:int
return_type: int
body:
declare num1:int = 1
loop:
initializer:
declare counter:int = 1
condition:
counter <= n
update:
counter = counter + 1
body:
num1 = num1 * counter
end body
end loop
return num1
end body
end function
end module
""";
}
}