mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 18:28:02 +00:00
feat: CLI generate shell build script (#1219)
* feat: CLI generate shell build script * dev: update build script sample
This commit is contained in:
parent
b541daf50e
commit
0b4014be80
6 changed files with 255 additions and 6 deletions
|
@ -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" {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"typlite": "target/debug/typlite",
|
||||
"check-msrv": "node scripts/check-msrv.mjs",
|
||||
"generate-ci": "dist generate",
|
||||
"project-build-script:ps1": "cargo run --bin tinymist -- generate-script -o ./scripts/project-build.ps1",
|
||||
"draft-release": "node scripts/draft-release.mjs"
|
||||
},
|
||||
"dependencies": {},
|
||||
|
|
6
scripts/project-build.ps1
Normal file
6
scripts/project-build.ps1
Normal file
|
@ -0,0 +1,6 @@
|
|||
# From file:docs/tinymist/book.typ to file:docs/tinymist/book.typ (pdf)
|
||||
tinymist compile --save-lock --task 'file:docs/tinymist/book.typ' 'docs/tinymist/book.typ' --root '.' --format=pdf
|
||||
# From file:docs/tinymist/ebook.typ to ebook-cover (svg)
|
||||
tinymist compile --save-lock --task 'ebook-cover' 'docs/tinymist/ebook.typ' --root '.' --pages 1-1 --format=svg
|
||||
# From file:docs/tinymist/ebook.typ to file:docs/tinymist/ebook.typ (pdf)
|
||||
tinymist compile --save-lock --task 'file:docs/tinymist/ebook.typ' 'docs/tinymist/ebook.typ' --root '.' --format=pdf
|
Loading…
Add table
Add a link
Reference in a new issue