feat: CLI generate shell build script (#1219)

* feat: CLI generate shell build script

* dev: update build script sample
This commit is contained in:
Myriad-Dreamin 2025-01-28 15:13:59 +08:00 committed by GitHub
parent b541daf50e
commit 0b4014be80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 255 additions and 6 deletions

View file

@ -179,6 +179,12 @@ display_possible_values!(OutputFormat);
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PathPattern(pub String);
impl fmt::Display for PathPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl PathPattern {
/// Creates a new path pattern.
pub fn new(pattern: &str) -> Self {
@ -406,6 +412,22 @@ impl ResourcePath {
),
}
}
/// Converts the resource path to a path relative to the `base` (usually the
/// directory storing the lockfile).
pub fn to_rel_path(&self, base: &Path) -> Option<PathBuf> {
if self.0 == "file" {
let path = Path::new(&self.1);
if path.is_absolute() {
Some(pathdiff::diff_paths(path, base).unwrap_or_else(|| path.to_owned()))
} else {
Some(path.to_owned())
}
} else {
None
}
}
/// Converts the resource path to an absolute file system path.
pub fn to_abs_path(&self, base: &Path) -> Option<PathBuf> {
if self.0 == "file" {

View file

@ -4,7 +4,7 @@ use sync_lsp::transport::MirrorArgs;
use tinymist::{
project::{DocCommands, TaskCommands},
tool::project::CompileArgs,
tool::project::{CompileArgs, GenerateScriptArgs},
CompileFontArgs, CompileOnceArgs,
};
use tinymist_core::LONG_VERSION;
@ -18,6 +18,7 @@ pub struct CliArguments {
}
#[derive(Debug, Clone, clap::Subcommand)]
#[clap(rename_all = "kebab-case")]
pub enum Commands {
/// Probes existence (Nop run)
Probe,
@ -35,6 +36,9 @@ pub enum Commands {
/// Runs compile command like `typst-cli compile`
Compile(CompileArgs),
/// Generates build script for compilation
#[clap(hide(true))] // still in development
GenerateScript(GenerateScriptArgs),
/// Runs language query
#[clap(hide(true))] // still in development
#[clap(subcommand)]

View file

@ -23,7 +23,7 @@ use sync_lsp::{
transport::{with_stdio_transport, MirrorArgs},
LspBuilder, LspClientRoot, LspResult,
};
use tinymist::world::TaskInputs;
use tinymist::{tool::project::generate_script_main, world::TaskInputs};
use tinymist::{
tool::project::{compile_main, project_main, task_main},
CompileConfig, Config, RegularInit, ServerState, SuperInit, UserActionTask,
@ -88,6 +88,7 @@ fn main() -> Result<()> {
match args.command.unwrap_or_default() {
Commands::Completion(args) => completion(args),
Commands::Compile(args) => RUNTIMES.tokio_runtime.block_on(compile_main(args)),
Commands::GenerateScript(args) => generate_script_main(args),
Commands::Query(query_cmds) => query_main(query_cmds),
Commands::Lsp(args) => lsp_main(args),
Commands::TraceLsp(args) => trace_lsp_main(args),

View file

@ -1,13 +1,17 @@
//! Project management tools.
use std::path::{Path, PathBuf};
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use reflexo::ImmutPath;
use tinymist_std::error::prelude::*;
use clap_complete::Shell;
use reflexo::{path::unix_slash, ImmutPath};
use tinymist_std::{bail, error::prelude::*};
use crate::{project::*, task::ExportTask};
/// Common arguments of compile, watch, and query.
/// Arguments for project compilation.
#[derive(Debug, Clone, clap::Parser)]
pub struct CompileArgs {
/// Inherits the compile task arguments.
@ -24,6 +28,18 @@ pub struct CompileArgs {
pub lockfile: Option<PathBuf>,
}
/// Arguments for generating a build script.
#[derive(Debug, Clone, clap::Parser)]
pub struct GenerateScriptArgs {
/// The shell to generate the completion script for. If not provided, it
/// will be inferred from the environment.
#[clap(value_enum)]
pub shell: Option<Shell>,
/// The path to the output script.
#[clap(short, long)]
pub output: Option<String>,
}
trait LockFileExt {
fn preview(&mut self, doc_id: Id, args: &TaskPreviewArgs) -> Result<Id>;
}
@ -89,6 +105,205 @@ pub async fn compile_main(args: CompileArgs) -> Result<()> {
Ok(())
}
/// Generates a build script for compilation
pub fn generate_script_main(args: GenerateScriptArgs) -> Result<()> {
let Some(shell) = args.shell.or_else(Shell::from_env) else {
bail!("could not infer shell");
};
let output = Path::new(args.output.as_deref().unwrap_or("build"));
let output = match shell {
Shell::Bash | Shell::Zsh | Shell::Elvish | Shell::Fish => output.with_extension("sh"),
Shell::PowerShell => output.with_extension("ps1"),
_ => bail!("unsupported shell: {shell:?}"),
};
let script = match shell {
Shell::Bash | Shell::Zsh | Shell::PowerShell => shell_build_script(shell)?,
_ => bail!("unsupported shell: {shell:?}"),
};
std::fs::write(output, script).context("write script")?;
Ok(())
}
/// Generates a build script for shell-like shells
fn shell_build_script(shell: Shell) -> Result<String> {
let mut output = String::new();
match shell {
Shell::Bash => {
output.push_str("#!/usr/bin/env bash\n\n");
}
Shell::Zsh => {
output.push_str("#!/usr/bin/env zsh\n\n");
}
Shell::PowerShell => {}
_ => {}
}
let lock_dir = std::env::current_dir().context("current directory")?;
let lock = LockFile::read(&lock_dir)?;
struct CmdBuilder(Vec<Cow<'static, str>>);
impl CmdBuilder {
fn new() -> Self {
Self(vec![])
}
fn extend(&mut self, args: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) {
for arg in args {
self.0.push(arg.into());
}
}
fn push(&mut self, arg: impl Into<Cow<'static, str>>) {
self.0.push(arg.into());
}
fn build(self) -> String {
self.0.join(" ")
}
}
let quote_escape = |s: &str| s.replace("'", r#"'"'"'"#);
let quote = |s: &str| format!("'{}'", s.replace("'", r#"'"'"'"#));
let path_of = |p: &ResourcePath, loc: &str| {
let Some(path) = p.to_rel_path(&lock_dir) else {
log::error!("could not resolve path for {loc}, path: {p:?}");
return String::default();
};
quote(&unix_slash(&path))
};
let base_cmd: Vec<&str> = vec!["tinymist", "compile", "--save-lock"];
for task in lock.task.iter() {
let Some(input) = lock.get_document(&task.document) else {
log::warn!(
"could not find document for task {:?}, whose document is {:?}",
task.id,
task.doc_id()
);
continue;
};
// todo: preview/query commands
let Some(export) = task.task.as_export() else {
continue;
};
let mut cmd = CmdBuilder::new();
cmd.extend(base_cmd.iter().copied());
cmd.push("--task");
cmd.push(quote(&task.id.to_string()));
cmd.push(path_of(&input.main, "main"));
if let Some(root) = &input.root {
cmd.push("--root");
cmd.push(path_of(root, "root"));
}
for (k, v) in &input.inputs {
cmd.push(format!(
r#"--input='{}={}'"#,
quote_escape(k),
quote_escape(v)
));
}
for p in &input.font_paths {
cmd.push("--font-path");
cmd.push(path_of(p, "font-path"));
}
if !input.system_fonts {
cmd.push("--ignore-system-fonts");
}
if let Some(p) = &input.package_path {
cmd.push("--package-path");
cmd.push(path_of(p, "package-path"));
}
if let Some(p) = &input.package_cache_path {
cmd.push("--package-cache-path");
cmd.push(path_of(p, "package-cache-path"));
}
if let Some(p) = &export.output {
cmd.push("--output");
cmd.push(quote(&p.to_string()));
}
for t in &export.transform {
match t {
ExportTransform::Pretty { .. } => {
cmd.push("--pretty");
}
ExportTransform::Pages { ranges } => {
for r in ranges {
cmd.push("--pages");
cmd.push(r.to_string());
}
}
// todo: export me
ExportTransform::Merge { .. } | ExportTransform::Script { .. } => {}
}
}
match &task.task {
ProjectTask::Preview(..) | ProjectTask::Query(..) => {}
ProjectTask::ExportPdf(task) => {
cmd.push("--format=pdf");
for s in &task.pdf_standards {
cmd.push("--pdf-standard");
let s = serde_json::to_string(s).context("pdf standard")?;
cmd.push(s);
}
if let Some(output) = &task.creation_timestamp {
cmd.push("--creation-timestamp");
cmd.push(output.to_string());
}
}
ProjectTask::ExportSvg(..) => {
cmd.push("--format=svg");
}
ProjectTask::ExportMarkdown(..) => {
cmd.push("--format=md");
}
ProjectTask::ExportPng(..) => {
cmd.push("--format=png");
}
ProjectTask::ExportText(..) => {
cmd.push("--format=txt");
}
ProjectTask::ExportHtml(..) => {
cmd.push("--format=html");
}
}
let ext = task.task.extension();
output.push_str(&format!(
"# From {} to {} ({ext})\n",
task.doc_id(),
task.id
));
output.push_str(&cmd.build());
output.push('\n');
}
Ok(output)
}
/// Project document commands' main
pub fn project_main(args: DocCommands) -> Result<()> {
LockFile::update(Path::new("."), |state| {