Skip to main content

hyperlit_runner/
runner.rs

1use 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
19/// The main hyperlit runner, responsible for running the document generation process
20pub struct Runner {
21    /// Root path to source code. This may be the repository root to collect all files
22    src_directory: PathBuf,
23    /// Globs to use when searching for source files, these may be prefixed with "!" to exclude files or directories
24    src_globs: Vec<String>,
25    /// Path to the docs directory
26    docs_directory: PathBuf,
27    /// Globs to use when searching for documentation files, may be "*" to include all files
28    doc_globs: Vec<String>,
29    /// Path to a build directory used for temporary files
30    build_directory: PathBuf,
31    /// Directory to write the complete documentation output to
32    output_directory: PathBuf,
33    /// The backend used for generating the documentation
34    backend: BackendBox,
35    /// The database used for storing intermediate data
36    database: DatabaseBox,
37    /// List of marker strings used to identify documentation segments to extract from the source code
38    doc_markers: Vec<String>,
39    /// Path to the root of the repository
40    root_path: PathBuf,
41    /// Template used to generate links to source code (e.g. on github, etc.)
42    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}