hyperlit_runner/
runner.rs1use hyperlit_backend::backend::BackendBox;
2use hyperlit_base::result::HyperlitResult;
3use hyperlit_base::{bail, context};
4use hyperlit_core::config::HyperlitConfig;
5use hyperlit_database::DatabaseBox;
6use hyperlit_extractor::git_info::GitInfo;
7use hyperlit_model::directive_evaluation::DirectiveEvaluation;
8use hyperlit_runtime::backend_compile_params_impl::BackendCompileParamsImpl;
9use hyperlit_runtime::evaluate_directive::evaluate_directive;
10use ignore::overrides::OverrideBuilder;
11use ignore::{Walk, WalkBuilder};
12use path_absolutize::Absolutize;
13use std::fs::{File, create_dir_all, remove_dir_all};
14use std::io::{BufRead, BufReader, BufWriter, Write};
15use std::path::{Path, PathBuf};
16use tracing::{debug, info, info_span};
17use walkdir::WalkDir;
18
19pub struct Runner {
21 src_directory: PathBuf,
23 src_globs: Vec<String>,
25 docs_directory: PathBuf,
27 doc_globs: Vec<String>,
29 build_directory: PathBuf,
31 output_directory: PathBuf,
33 backend: BackendBox,
35 database: DatabaseBox,
37 doc_markers: Vec<String>,
39 root_path: PathBuf,
41 source_link_template: Option<String>,
43}
44
45impl Runner {
46 pub fn new() -> HyperlitResult<Self> {
47 let config = HyperlitConfig::from_path("hyperlit.toml")?;
48 Self::with_config(config)
49 }
50
51 pub fn with_config(config: HyperlitConfig) -> HyperlitResult<Self> {
52 let root_path = PathBuf::from(config.config_path)
53 .absolutize()?
54 .parent()
55 .expect("config parent path")
56 .to_path_buf();
57 let docs_directory = resolve_path(&root_path, &config.docs_directory)?;
58 if !docs_directory.exists() {
59 bail!(
60 "Docs directory '{}' does not exist",
61 docs_directory.display()
62 );
63 }
64 Ok(Self {
65 src_directory: resolve_path(&root_path, &config.src_directory)?,
66 docs_directory,
67 build_directory: resolve_path(&root_path, &config.build_directory)?,
68 output_directory: resolve_path(&root_path, &config.output_directory)?,
69 doc_globs: config.doc_globs,
70 src_globs: config.src_globs,
71 backend: Box::new(hyperlit_backend_mdbook::mdbook_backend::MdBookBackend::new()),
72 database: Box::new(hyperlit_database::in_memory_database::InMemoryDatabase::new()),
73 doc_markers: config.doc_markers,
74 root_path,
75 source_link_template: config.source_link_template,
76 })
77 }
78
79 pub fn run(&mut self) -> HyperlitResult<()> {
80 let start_time = std::time::Instant::now();
81 let span = info_span!("run");
82 let _span = span.enter();
83 if self.build_directory.exists() {
84 context!("remove build directory {:?}", self.build_directory => remove_dir_all(&self.build_directory))?;
85 }
86 if self.output_directory.exists() {
87 context!("remove output directory {:?}", self.output_directory => remove_dir_all(&self.output_directory))?;
88 }
89 context!("create build directory {:?}", self.build_directory => create_dir_all(&self.build_directory))?;
90 context!("create output directory {:?}", self.output_directory => create_dir_all(&self.output_directory))?;
91
92 self.extract_segments()?;
93 self.backend.prepare(&mut BackendCompileParamsImpl::new(
94 &self.docs_directory,
95 &self.build_directory,
96 &self.output_directory,
97 self.database.as_mut(),
98 ))?;
99 self.copy_docs()?;
100 context!("run backend" => self.backend.compile(&BackendCompileParamsImpl::new(
101 &self.docs_directory,
102 &self.build_directory,
103 &self.output_directory,
104 self.database.as_mut(),
105 )))?;
106 let run_duration = start_time.elapsed();
107 info!("run completed in {}ms", run_duration.as_millis());
108 Ok(())
109 }
110
111 pub fn copy_docs(&self) -> HyperlitResult<()> {
112 context!("copy docs directory {:?} to build directory {:?}", self.docs_directory, self.build_directory => {
113 let mut overrides = OverrideBuilder::new(&self.docs_directory);
114 for glob in &self.doc_globs {
115 overrides.add(glob)?;
116 }
117 let matcher = overrides.build()?;
118 for entry in WalkDir::new(&self.docs_directory) {
119 let entry = entry?;
120 let source_path = entry.path();
121 let destination_path = self.build_directory.join(source_path.strip_prefix(&self.docs_directory)?);
122 if source_path.is_dir() {
123 create_dir_all(self.build_directory.join(&destination_path))?;
124 } else {
125 let must_process = !matcher.matched(source_path, false).is_ignore();
126 if must_process {
127 debug!("processing file {:?} to {:?} ", source_path, destination_path);
128 self.process_doc(source_path, &destination_path)?;
129 } else {
130 debug!("copying file {:?} to {:?} ", source_path, destination_path);
131 std::fs::copy(source_path, destination_path)?;
132 }
133 }
134 }
135 HyperlitResult::<()>::Ok(())
136 }
137 )
138 }
139
140 fn process_doc(&self, source_path: &Path, destination_path: &Path) -> HyperlitResult<()> {
141 let mut destination_file = BufWriter::new(File::create(destination_path)?);
142 for line in BufReader::new(File::open(source_path)?).lines() {
143 let line = line?;
144 let evaluation = evaluate_directive(&line, self.database.as_ref())?;
145 match evaluation {
146 DirectiveEvaluation::Segments { segments } => {
147 for segment in segments {
148 let text_to_insert = self.backend.transform_segment(segment)?;
149 destination_file.write_all(text_to_insert.as_bytes())?;
150 destination_file.write_all(b"\n")?;
151 }
152 }
153 DirectiveEvaluation::NoDirective => {
154 destination_file.write_all(line.as_bytes())?;
155 destination_file.write_all(b"\n")?;
156 }
157 }
158 }
159 Ok(())
160 }
161
162 fn extract_segments(&mut self) -> HyperlitResult<()> {
163 let span = info_span!("extract segments");
164 let _span = span.enter();
165 let extractor = hyperlit_extractor::extractor::Extractor::new(
166 &self
167 .doc_markers
168 .iter()
169 .map(|s| s.as_str())
170 .collect::<Vec<_>>(),
171 self.root_path.to_string_lossy().to_string(),
172 );
173 let git_info = GitInfo::new()?;
174 let walk = create_walk(&self.src_directory, &self.src_globs)?;
175 for entry in walk {
176 let entry = entry?;
177 let source_path = entry.path();
178 if source_path.is_file() {
179 debug!("extracting file {:?} ", source_path);
180 let mut segments = extractor.extract(&source_path)?;
181 if segments.is_empty() {
182 continue;
183 }
184 let last_modification_info = git_info.get_last_modification_info(source_path)?;
185 for segment in &mut segments {
186 segment.last_modification = last_modification_info.clone();
187 if let Some(ref url) = self.source_link_template {
188 let mut url = url.clone();
189 url = url.replace("{path}", segment.location.filepath());
190 url = url.replace("{line}", &segment.location.line().to_string());
191 segment.location_url = Some(url);
192 }
193 }
194 self.database.add_segments(segments)?;
195 }
196 }
197 Ok(())
198 }
199}
200
201fn resolve_path(root: &Path, path: &str) -> HyperlitResult<PathBuf> {
202 Ok(root.join(path).absolutize()?.to_path_buf())
203}
204
205fn create_walk(base_path: &Path, globs: &[String]) -> HyperlitResult<Walk> {
206 let mut walk_builder = WalkBuilder::new(base_path);
207 let mut overrides = OverrideBuilder::new(base_path);
208 for glob in globs {
209 overrides.add(glob)?;
210 }
211 walk_builder.overrides(overrides.build()?);
212 Ok(walk_builder.build())
213}
214
215#[cfg(test)]
216mod tests {
217 use crate::runner::Runner;
218 use hyperlit_base::result::HyperlitResult;
219 use hyperlit_core::config::HyperlitConfig;
220 use std::path::Path;
221
222 #[test]
223 fn test_run() -> HyperlitResult<()> {
224 let config = HyperlitConfig::from_path("sample/hyperlit.toml")?;
225 let mut runner = Runner::with_config(config)?;
226 runner.run()?;
227 assert!(
228 Path::new("sample/output/index.html").exists(),
229 "Output path index.html should exist"
230 );
231 Ok(())
232 }
233}