1215 lines
27 KiB
Rust
1215 lines
27 KiB
Rust
/*!
|
|
This module is concerned with how `rust-script` extracts the manfiest from a script file.
|
|
*/
|
|
use regex;
|
|
|
|
use self::regex::Regex;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
use crate::consts;
|
|
use crate::error::{MainError, MainResult};
|
|
use crate::templates;
|
|
use crate::Input;
|
|
use lazy_static::lazy_static;
|
|
use log::{error, info};
|
|
use std::ffi::OsString;
|
|
|
|
lazy_static! {
|
|
static ref RE_SHORT_MANIFEST: Regex =
|
|
Regex::new(r"^(?i)\s*//\s*cargo-deps\s*:(.*?)(\r\n|\n)").unwrap();
|
|
static ref RE_MARGIN: Regex = Regex::new(r"^\s*\*( |$)").unwrap();
|
|
static ref RE_SPACE: Regex = Regex::new(r"^(\s+)").unwrap();
|
|
static ref RE_NESTING: Regex = Regex::new(r"/\*|\*/").unwrap();
|
|
static ref RE_COMMENT: Regex = Regex::new(r"^\s*//(!|/)").unwrap();
|
|
static ref RE_SHEBANG: Regex = Regex::new(r"^#![^\[].*?(\r\n|\n)").unwrap();
|
|
static ref RE_CRATE_COMMENT: Regex = {
|
|
Regex::new(
|
|
r"(?x)
|
|
# We need to find the first `/*!` or `//!` that *isn't* preceeded by something that would make it apply to anything other than the crate itself. Because we can't do this accurately, we'll just require that the doc comment is the *first* thing in the file (after the optional shebang, which should already have been stripped).
|
|
^\s*
|
|
(/\*!|//(!|/))
|
|
"
|
|
).unwrap()
|
|
};
|
|
}
|
|
|
|
/**
|
|
Splits input into a complete Cargo manifest and unadultered Rust source.
|
|
|
|
Unless we have prelude items to inject, in which case it will be *slightly* adulterated.
|
|
*/
|
|
pub fn split_input(
|
|
input: &Input,
|
|
deps: &[(String, String)],
|
|
prelude_items: &[String],
|
|
input_id: &OsString,
|
|
bin_name: Option<String>,
|
|
) -> MainResult<(String, String)> {
|
|
fn contains_main_method(line: &str) -> bool {
|
|
let line = line.trim_start();
|
|
line.starts_with("fn main(")
|
|
|| line.starts_with("pub fn main(")
|
|
|| line.starts_with("async fn main(")
|
|
|| line.starts_with("pub async fn main(")
|
|
}
|
|
|
|
let template_buf;
|
|
let (part_mani, source, template, sub_prelude) = match *input {
|
|
Input::File(_, _, content, _) => {
|
|
assert_eq!(prelude_items.len(), 0);
|
|
let content = strip_shebang(content);
|
|
let (manifest, source) =
|
|
find_embedded_manifest(content).unwrap_or((Manifest::Toml(""), content));
|
|
|
|
let source = if source.lines().any(contains_main_method) {
|
|
source.to_string()
|
|
} else {
|
|
format!("fn main() -> Result<(), Box<dyn std::error::Error+Sync+Send>> {{\n {{\n {} }}\n Ok(())\n}}", source)
|
|
};
|
|
(manifest, source, templates::get_template("file")?, false)
|
|
}
|
|
Input::Expr(content, template) => {
|
|
template_buf = templates::get_template(template.unwrap_or("expr"))?;
|
|
let (manifest, template_src) = find_embedded_manifest(&template_buf)
|
|
.unwrap_or((Manifest::Toml(""), &template_buf));
|
|
(manifest, content.to_string(), template_src.into(), true)
|
|
}
|
|
Input::Loop(content, count) => {
|
|
let templ = if count { "loop-count" } else { "loop" };
|
|
(
|
|
Manifest::Toml(""),
|
|
content.to_string(),
|
|
templates::get_template(templ)?,
|
|
true,
|
|
)
|
|
}
|
|
};
|
|
|
|
let mut prelude_str;
|
|
let mut subs = HashMap::with_capacity(2);
|
|
|
|
subs.insert(consts::SCRIPT_BODY_SUB, &source[..]);
|
|
|
|
if sub_prelude {
|
|
prelude_str =
|
|
String::with_capacity(prelude_items.iter().map(|i| i.len() + 1).sum::<usize>());
|
|
for i in prelude_items {
|
|
prelude_str.push_str(i);
|
|
prelude_str.push('\n');
|
|
}
|
|
subs.insert(consts::SCRIPT_PRELUDE_SUB, &prelude_str[..]);
|
|
}
|
|
|
|
let source = templates::expand(&template, &subs)?;
|
|
|
|
info!("part_mani: {:?}", part_mani);
|
|
info!("source: {:?}", source);
|
|
|
|
let part_mani = part_mani.into_toml()?;
|
|
info!("part_mani: {:?}", part_mani);
|
|
|
|
// It's-a mergin' time!
|
|
let def_mani = default_manifest(input, input_id, bin_name)?;
|
|
let dep_mani = deps_manifest(deps)?;
|
|
|
|
let mani = merge_manifest(def_mani, part_mani)?;
|
|
let mani = merge_manifest(mani, dep_mani)?;
|
|
|
|
// Fix up relative paths.
|
|
let mani = fix_manifest_paths(mani, &input.base_path())?;
|
|
info!("mani: {:?}", mani);
|
|
|
|
let mani_str = format!("{}", toml::Value::Table(mani));
|
|
info!("mani_str: {}", mani_str);
|
|
|
|
Ok((mani_str, source))
|
|
}
|
|
|
|
#[rustversion::before(1.59)]
|
|
#[cfg(test)]
|
|
pub const STRIP_SECTION: &str = "\n";
|
|
#[rustversion::since(1.59)]
|
|
#[cfg(test)]
|
|
pub const STRIP_SECTION: &str = r##"
|
|
[profile.release]
|
|
strip = true
|
|
"##;
|
|
|
|
#[test]
|
|
fn test_split_input() {
|
|
let input_id = OsString::from("input_id");
|
|
macro_rules! si {
|
|
($i:expr) => {
|
|
split_input(&$i, &[], &[], &input_id).ok()
|
|
};
|
|
}
|
|
|
|
let dummy_path: ::std::path::PathBuf = "p".into();
|
|
let dummy_path = &dummy_path;
|
|
let f = |c| Input::File("n", dummy_path, c, 0);
|
|
|
|
macro_rules! r {
|
|
($m:expr, $r:expr) => {
|
|
Some(($m.into(), $r.into()))
|
|
};
|
|
}
|
|
|
|
assert_eq!(
|
|
si!(f(r#"fn main() {}"#)),
|
|
r!(
|
|
format!(
|
|
"{}{}",
|
|
r#"[[bin]]
|
|
name = "n_input_id"
|
|
path = "n.rs"
|
|
|
|
[dependencies]
|
|
|
|
[package]
|
|
authors = ["Anonymous"]
|
|
edition = "2018"
|
|
name = "n"
|
|
version = "0.1.0""#,
|
|
STRIP_SECTION
|
|
),
|
|
r#"fn main() {}"#
|
|
)
|
|
);
|
|
|
|
// Ensure removed prefix manifests don't work.
|
|
assert_eq!(
|
|
si!(f(r#"
|
|
---
|
|
fn main() {}
|
|
"#)),
|
|
r!(
|
|
format!(
|
|
"{}{}",
|
|
r#"[[bin]]
|
|
name = "n_input_id"
|
|
path = "n.rs"
|
|
|
|
[dependencies]
|
|
|
|
[package]
|
|
authors = ["Anonymous"]
|
|
edition = "2018"
|
|
name = "n"
|
|
version = "0.1.0""#,
|
|
STRIP_SECTION
|
|
),
|
|
r#"
|
|
---
|
|
fn main() {}
|
|
"#
|
|
)
|
|
);
|
|
|
|
assert_eq!(
|
|
si!(f(r#"[dependencies]
|
|
time="0.1.25"
|
|
---
|
|
fn main() {}
|
|
"#)),
|
|
r!(
|
|
format!(
|
|
"{}{}",
|
|
r#"[[bin]]
|
|
name = "n_input_id"
|
|
path = "n.rs"
|
|
|
|
[dependencies]
|
|
|
|
[package]
|
|
authors = ["Anonymous"]
|
|
edition = "2018"
|
|
name = "n"
|
|
version = "0.1.0""#,
|
|
STRIP_SECTION
|
|
),
|
|
r#"[dependencies]
|
|
time="0.1.25"
|
|
---
|
|
fn main() {}
|
|
"#
|
|
)
|
|
);
|
|
|
|
assert_eq!(
|
|
si!(f(r#"
|
|
// Cargo-Deps: time="0.1.25"
|
|
fn main() {}
|
|
"#)),
|
|
r!(
|
|
format!(
|
|
"{}{}",
|
|
r#"[[bin]]
|
|
name = "n_input_id"
|
|
path = "n.rs"
|
|
|
|
[dependencies]
|
|
time = "0.1.25"
|
|
|
|
[package]
|
|
authors = ["Anonymous"]
|
|
edition = "2018"
|
|
name = "n"
|
|
version = "0.1.0""#,
|
|
STRIP_SECTION
|
|
),
|
|
r#"
|
|
// Cargo-Deps: time="0.1.25"
|
|
fn main() {}
|
|
"#
|
|
)
|
|
);
|
|
|
|
assert_eq!(
|
|
si!(f(r#"
|
|
// Cargo-Deps: time="0.1.25", libc="0.2.5"
|
|
fn main() {}
|
|
"#)),
|
|
r!(
|
|
format!(
|
|
"{}{}",
|
|
r#"[[bin]]
|
|
name = "n_input_id"
|
|
path = "n.rs"
|
|
|
|
[dependencies]
|
|
libc = "0.2.5"
|
|
time = "0.1.25"
|
|
|
|
[package]
|
|
authors = ["Anonymous"]
|
|
edition = "2018"
|
|
name = "n"
|
|
version = "0.1.0""#,
|
|
STRIP_SECTION
|
|
),
|
|
r#"
|
|
// Cargo-Deps: time="0.1.25", libc="0.2.5"
|
|
fn main() {}
|
|
"#
|
|
)
|
|
);
|
|
|
|
assert_eq!(
|
|
si!(f(r#"
|
|
/*!
|
|
Here is a manifest:
|
|
|
|
```cargo
|
|
[dependencies]
|
|
time = "0.1.25"
|
|
```
|
|
*/
|
|
fn main() {}
|
|
"#)),
|
|
r!(
|
|
format!(
|
|
"{}{}",
|
|
r#"[[bin]]
|
|
name = "n_input_id"
|
|
path = "n.rs"
|
|
|
|
[dependencies]
|
|
time = "0.1.25"
|
|
|
|
[package]
|
|
authors = ["Anonymous"]
|
|
edition = "2018"
|
|
name = "n"
|
|
version = "0.1.0""#,
|
|
STRIP_SECTION
|
|
),
|
|
r#"
|
|
/*!
|
|
Here is a manifest:
|
|
|
|
```cargo
|
|
[dependencies]
|
|
time = "0.1.25"
|
|
```
|
|
*/
|
|
fn main() {}
|
|
"#
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
Returns a slice of the input string with the leading shebang, if there is one, omitted.
|
|
*/
|
|
fn strip_shebang(s: &str) -> &str {
|
|
match RE_SHEBANG.find(s) {
|
|
Some(m) => &s[m.end()..],
|
|
None => s,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_strip_shebang() {
|
|
assert_eq!(
|
|
strip_shebang(
|
|
"\
|
|
#!/usr/bin/env rust-script
|
|
and the rest
|
|
\
|
|
"
|
|
),
|
|
"\
|
|
and the rest
|
|
\
|
|
"
|
|
);
|
|
assert_eq!(
|
|
strip_shebang(
|
|
"\
|
|
#![thingy]
|
|
and the rest
|
|
\
|
|
"
|
|
),
|
|
"\
|
|
#![thingy]
|
|
and the rest
|
|
\
|
|
"
|
|
);
|
|
}
|
|
|
|
/**
|
|
Represents the kind, and content of, an embedded manifest.
|
|
*/
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
enum Manifest<'s> {
|
|
/// The manifest is a valid TOML fragment.
|
|
Toml(&'s str),
|
|
/// The manifest is a valid TOML fragment (owned).
|
|
// TODO: Change to Cow<'s, str>.
|
|
TomlOwned(String),
|
|
/// The manifest is a comma-delimited list of dependencies.
|
|
DepList(&'s str),
|
|
}
|
|
|
|
impl<'s> Manifest<'s> {
|
|
pub fn into_toml(self) -> MainResult<toml::value::Table> {
|
|
use self::Manifest::*;
|
|
match self {
|
|
Toml(s) => toml::from_str(s),
|
|
TomlOwned(ref s) => toml::from_str(s),
|
|
DepList(s) => Manifest::dep_list_to_toml(s),
|
|
}
|
|
.map_err(|e| {
|
|
MainError::Tag(
|
|
"could not parse embedded manifest".into(),
|
|
Box::new(MainError::Other(Box::new(e))),
|
|
)
|
|
})
|
|
}
|
|
|
|
fn dep_list_to_toml(s: &str) -> ::std::result::Result<toml::value::Table, toml::de::Error> {
|
|
let mut r = String::new();
|
|
r.push_str("[dependencies]\n");
|
|
for dep in s.trim().split(',') {
|
|
// If there's no version specified, add one.
|
|
match dep.contains('=') {
|
|
true => {
|
|
r.push_str(dep);
|
|
r.push('\n');
|
|
}
|
|
false => {
|
|
r.push_str(dep);
|
|
r.push_str("=\"*\"\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
toml::from_str(&r)
|
|
}
|
|
}
|
|
|
|
/**
|
|
Locates a manifest embedded in Rust source.
|
|
|
|
Returns `Some((manifest, source))` if it finds a manifest, `None` otherwise.
|
|
*/
|
|
fn find_embedded_manifest(s: &str) -> Option<(Manifest, &str)> {
|
|
find_short_comment_manifest(s).or_else(|| find_code_block_manifest(s))
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_embedded_manifest() {
|
|
use self::Manifest::*;
|
|
|
|
let fem = find_embedded_manifest;
|
|
|
|
assert_eq!(fem("fn main() {}"), None);
|
|
|
|
assert_eq!(
|
|
fem("
|
|
fn main() {}
|
|
"),
|
|
None
|
|
);
|
|
|
|
// Ensure removed prefix manifests don't work.
|
|
assert_eq!(
|
|
fem(r#"
|
|
---
|
|
fn main() {}
|
|
"#),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
fem("[dependencies]
|
|
time = \"0.1.25\"
|
|
---
|
|
fn main() {}
|
|
"),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
fem("[dependencies]
|
|
time = \"0.1.25\"
|
|
|
|
fn main() {}
|
|
"),
|
|
None
|
|
);
|
|
|
|
// Make sure we aren't just grabbing the *last* line.
|
|
assert_eq!(
|
|
fem("[dependencies]
|
|
time = \"0.1.25\"
|
|
|
|
fn main() {
|
|
println!(\"Hi!\");
|
|
}
|
|
"),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
fem("// cargo-deps: time=\"0.1.25\"
|
|
fn main() {}
|
|
"),
|
|
Some((
|
|
DepList(" time=\"0.1.25\""),
|
|
"// cargo-deps: time=\"0.1.25\"
|
|
fn main() {}
|
|
"
|
|
))
|
|
);
|
|
|
|
assert_eq!(
|
|
fem("// cargo-deps: time=\"0.1.25\", libc=\"0.2.5\"
|
|
fn main() {}
|
|
"),
|
|
Some((
|
|
DepList(" time=\"0.1.25\", libc=\"0.2.5\""),
|
|
"// cargo-deps: time=\"0.1.25\", libc=\"0.2.5\"
|
|
fn main() {}
|
|
"
|
|
))
|
|
);
|
|
|
|
assert_eq!(
|
|
fem("
|
|
// cargo-deps: time=\"0.1.25\" \n\
|
|
fn main() {}
|
|
"),
|
|
Some((
|
|
DepList(" time=\"0.1.25\" "),
|
|
"
|
|
// cargo-deps: time=\"0.1.25\" \n\
|
|
fn main() {}
|
|
"
|
|
))
|
|
);
|
|
|
|
assert_eq!(
|
|
fem("/* cargo-deps: time=\"0.1.25\" */
|
|
fn main() {}
|
|
"),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
fem(r#"//! [dependencies]
|
|
//! time = "0.1.25"
|
|
fn main() {}
|
|
"#),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
fem(r#"//! ```Cargo
|
|
//! [dependencies]
|
|
//! time = "0.1.25"
|
|
//! ```
|
|
fn main() {}
|
|
"#),
|
|
Some((
|
|
TomlOwned(
|
|
r#"[dependencies]
|
|
time = "0.1.25"
|
|
"#
|
|
.into()
|
|
),
|
|
r#"//! ```Cargo
|
|
//! [dependencies]
|
|
//! time = "0.1.25"
|
|
//! ```
|
|
fn main() {}
|
|
"#
|
|
))
|
|
);
|
|
|
|
assert_eq!(
|
|
fem(r#"/*!
|
|
[dependencies]
|
|
time = "0.1.25"
|
|
*/
|
|
fn main() {}
|
|
"#),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
fem(r#"/*!
|
|
```Cargo
|
|
[dependencies]
|
|
time = "0.1.25"
|
|
```
|
|
*/
|
|
fn main() {}
|
|
"#),
|
|
Some((
|
|
TomlOwned(
|
|
r#"[dependencies]
|
|
time = "0.1.25"
|
|
"#
|
|
.into()
|
|
),
|
|
r#"/*!
|
|
```Cargo
|
|
[dependencies]
|
|
time = "0.1.25"
|
|
```
|
|
*/
|
|
fn main() {}
|
|
"#
|
|
))
|
|
);
|
|
|
|
assert_eq!(
|
|
fem(r#"/*!
|
|
* [dependencies]
|
|
* time = "0.1.25"
|
|
*/
|
|
fn main() {}
|
|
"#),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
fem(r#"/*!
|
|
* ```Cargo
|
|
* [dependencies]
|
|
* time = "0.1.25"
|
|
* ```
|
|
*/
|
|
fn main() {}
|
|
"#),
|
|
Some((
|
|
TomlOwned(
|
|
r#"[dependencies]
|
|
time = "0.1.25"
|
|
"#
|
|
.into()
|
|
),
|
|
r#"/*!
|
|
* ```Cargo
|
|
* [dependencies]
|
|
* time = "0.1.25"
|
|
* ```
|
|
*/
|
|
fn main() {}
|
|
"#
|
|
))
|
|
);
|
|
}
|
|
|
|
/**
|
|
Locates a "short comment manifest" in Rust source.
|
|
*/
|
|
fn find_short_comment_manifest(s: &str) -> Option<(Manifest, &str)> {
|
|
/*
|
|
This is pretty simple: the only valid syntax for this is for the first, non-blank line to contain a single-line comment whose first token is `cargo-deps:`. That's it.
|
|
*/
|
|
let re = &*RE_SHORT_MANIFEST;
|
|
if let Some(cap) = re.captures(s) {
|
|
if let Some(m) = cap.get(1) {
|
|
return Some((Manifest::DepList(m.as_str()), s));
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/**
|
|
Locates a "code block manifest" in Rust source.
|
|
*/
|
|
fn find_code_block_manifest(s: &str) -> Option<(Manifest, &str)> {
|
|
/*
|
|
This has to happen in a few steps.
|
|
|
|
First, we will look for and slice out a contiguous, inner doc comment which must be *the very first thing* in the file. `#[doc(...)]` attributes *are not supported*. Multiple single-line comments cannot have any blank lines between them.
|
|
|
|
Then, we need to strip off the actual comment markers from the content. Including indentation removal, and taking out the (optional) leading line markers for block comments. *sigh*
|
|
|
|
Then, we need to take the contents of this doc comment and feed it to a Markdown parser. We are looking for *the first* fenced code block with a language token of `cargo`. This is extracted and pasted back together into the manifest.
|
|
*/
|
|
let start = match RE_CRATE_COMMENT.captures(s) {
|
|
Some(cap) => match cap.get(1) {
|
|
Some(m) => m.start(),
|
|
None => return None,
|
|
},
|
|
None => return None,
|
|
};
|
|
|
|
let comment = match extract_comment(&s[start..]) {
|
|
Ok(s) => s,
|
|
Err(err) => {
|
|
error!("error slicing comment: {}", err);
|
|
return None;
|
|
}
|
|
};
|
|
|
|
scrape_markdown_manifest(&comment).map(|m| (Manifest::TomlOwned(m), s))
|
|
}
|
|
|
|
/**
|
|
Extracts the first `Cargo` fenced code block from a chunk of Markdown.
|
|
*/
|
|
fn scrape_markdown_manifest(content: &str) -> Option<String> {
|
|
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
|
|
|
// To match librustdoc/html/markdown.rs, opts.
|
|
let exts = Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES;
|
|
|
|
let md = Parser::new_ext(content, exts);
|
|
|
|
let mut found = false;
|
|
let mut output = None;
|
|
|
|
for item in md {
|
|
match item {
|
|
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info)))
|
|
if info.to_lowercase() == "cargo" && output.is_none() =>
|
|
{
|
|
found = true;
|
|
}
|
|
Event::Text(ref text) if found => {
|
|
let s = output.get_or_insert(String::new());
|
|
s.push_str(&*text);
|
|
}
|
|
Event::End(Tag::CodeBlock(_)) if found => {
|
|
found = false;
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
#[test]
|
|
fn test_scrape_markdown_manifest() {
|
|
macro_rules! smm {
|
|
($c:expr) => {
|
|
scrape_markdown_manifest($c)
|
|
};
|
|
}
|
|
|
|
assert_eq!(
|
|
smm!(
|
|
r#"There is no manifest in this comment.
|
|
"#
|
|
),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
smm!(
|
|
r#"There is no manifest in this comment.
|
|
|
|
```
|
|
This is not a manifest.
|
|
```
|
|
|
|
```rust
|
|
println!("Nor is this.");
|
|
```
|
|
|
|
Or this.
|
|
"#
|
|
),
|
|
None
|
|
);
|
|
|
|
assert_eq!(
|
|
smm!(
|
|
r#"This is a manifest:
|
|
|
|
```cargo
|
|
dependencies = { time = "*" }
|
|
```
|
|
"#
|
|
),
|
|
Some(
|
|
r#"dependencies = { time = "*" }
|
|
"#
|
|
.into()
|
|
)
|
|
);
|
|
|
|
assert_eq!(
|
|
smm!(
|
|
r#"This is *not* a manifest:
|
|
|
|
```
|
|
He's lying, I'm *totally* a manifest!
|
|
```
|
|
|
|
This *is*:
|
|
|
|
```cargo
|
|
dependencies = { time = "*" }
|
|
```
|
|
"#
|
|
),
|
|
Some(
|
|
r#"dependencies = { time = "*" }
|
|
"#
|
|
.into()
|
|
)
|
|
);
|
|
|
|
assert_eq!(
|
|
smm!(
|
|
r#"This is a manifest:
|
|
|
|
```cargo
|
|
dependencies = { time = "*" }
|
|
```
|
|
|
|
So is this, but it doesn't count:
|
|
|
|
```cargo
|
|
dependencies = { explode = true }
|
|
```
|
|
"#
|
|
),
|
|
Some(
|
|
r#"dependencies = { time = "*" }
|
|
"#
|
|
.into()
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
Extracts the contents of a Rust doc comment.
|
|
*/
|
|
fn extract_comment(s: &str) -> MainResult<String> {
|
|
use std::cmp::min;
|
|
|
|
fn n_leading_spaces(s: &str, n: usize) -> MainResult<()> {
|
|
if !s.chars().take(n).all(|c| c == ' ') {
|
|
return Err(format!("leading {:?} chars aren't all spaces: {:?}", n, s).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn extract_block(s: &str) -> MainResult<String> {
|
|
/*
|
|
On every line:
|
|
|
|
- update nesting level and detect end-of-comment
|
|
- if margin is None:
|
|
- if there appears to be a margin, set margin.
|
|
- strip off margin marker
|
|
- update the leading space counter
|
|
- strip leading space
|
|
- append content
|
|
*/
|
|
let mut r = String::new();
|
|
|
|
let margin_re = &*RE_MARGIN;
|
|
let space_re = &*RE_SPACE;
|
|
let nesting_re = &*RE_NESTING;
|
|
|
|
let mut leading_space = None;
|
|
let mut margin = None;
|
|
let mut depth: u32 = 1;
|
|
|
|
for line in s.lines() {
|
|
if depth == 0 {
|
|
break;
|
|
}
|
|
|
|
// Update nesting and look for end-of-comment.
|
|
let mut end_of_comment = None;
|
|
|
|
for (end, marker) in nesting_re.find_iter(line).map(|m| (m.start(), m.as_str())) {
|
|
match (marker, depth) {
|
|
("/*", _) => depth += 1,
|
|
("*/", 1) => {
|
|
end_of_comment = Some(end);
|
|
depth = 0;
|
|
break;
|
|
}
|
|
("*/", _) => depth -= 1,
|
|
_ => panic!("got a comment marker other than /* or */"),
|
|
}
|
|
}
|
|
|
|
let line = end_of_comment.map(|end| &line[..end]).unwrap_or(line);
|
|
|
|
// Detect and strip margin.
|
|
margin = margin.or_else(|| margin_re.find(line).map(|m| m.as_str()));
|
|
|
|
let line = if let Some(margin) = margin {
|
|
let end = line
|
|
.char_indices()
|
|
.take(margin.len())
|
|
.map(|(i, c)| i + c.len_utf8())
|
|
.last()
|
|
.unwrap_or(0);
|
|
&line[end..]
|
|
} else {
|
|
line
|
|
};
|
|
|
|
// Detect and strip leading indentation.
|
|
leading_space = leading_space.or_else(|| space_re.find(line).map(|m| m.end()));
|
|
|
|
/*
|
|
Make sure we have only leading spaces.
|
|
|
|
If we see a tab, fall over. I *would* expand them, but that gets into the question of how *many* spaces to expand them to, and *where* is the tab, because tabs are tab stops and not just N spaces.
|
|
|
|
Eurgh.
|
|
*/
|
|
n_leading_spaces(line, leading_space.unwrap_or(0))?;
|
|
|
|
let strip_len = min(leading_space.unwrap_or(0), line.len());
|
|
let line = &line[strip_len..];
|
|
|
|
// Done.
|
|
r.push_str(line);
|
|
|
|
// `lines` removes newlines. Ideally, it wouldn't do that, but hopefully this shouldn't cause any *real* problems.
|
|
r.push('\n');
|
|
}
|
|
|
|
Ok(r)
|
|
}
|
|
|
|
fn extract_line(s: &str) -> MainResult<String> {
|
|
let mut r = String::new();
|
|
|
|
let comment_re = &*RE_COMMENT;
|
|
let space_re = &*RE_SPACE;
|
|
|
|
let mut leading_space = None;
|
|
|
|
for line in s.lines() {
|
|
// Strip leading comment marker.
|
|
let content = match comment_re.find(line) {
|
|
Some(m) => &line[m.end()..],
|
|
None => break,
|
|
};
|
|
|
|
// Detect and strip leading indentation.
|
|
leading_space = leading_space.or_else(|| {
|
|
space_re
|
|
.captures(content)
|
|
.and_then(|c| c.get(1))
|
|
.map(|m| m.end())
|
|
});
|
|
|
|
/*
|
|
Make sure we have only leading spaces.
|
|
|
|
If we see a tab, fall over. I *would* expand them, but that gets into the question of how *many* spaces to expand them to, and *where* is the tab, because tabs are tab stops and not just N spaces.
|
|
|
|
Eurgh.
|
|
*/
|
|
n_leading_spaces(content, leading_space.unwrap_or(0))?;
|
|
|
|
let strip_len = min(leading_space.unwrap_or(0), content.len());
|
|
let content = &content[strip_len..];
|
|
|
|
// Done.
|
|
r.push_str(content);
|
|
|
|
// `lines` removes newlines. Ideally, it wouldn't do that, but hopefully this shouldn't cause any *real* problems.
|
|
r.push('\n');
|
|
}
|
|
|
|
Ok(r)
|
|
}
|
|
|
|
if let Some(stripped) = s.strip_prefix("/*!") {
|
|
extract_block(stripped)
|
|
} else if s.starts_with("//!") || s.starts_with("///") {
|
|
extract_line(s)
|
|
} else {
|
|
Err("no doc comment found".into())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_comment() {
|
|
macro_rules! ec {
|
|
($s:expr) => {
|
|
extract_comment($s).map_err(|e| e.to_string())
|
|
};
|
|
}
|
|
|
|
assert_eq!(ec!(r#"fn main () {}"#), Err("no doc comment found".into()));
|
|
|
|
assert_eq!(
|
|
ec!(r#"/*!
|
|
Here is a manifest:
|
|
|
|
```cargo
|
|
[dependencies]
|
|
time = "*"
|
|
```
|
|
*/
|
|
fn main() {}
|
|
"#),
|
|
Ok(r#"
|
|
Here is a manifest:
|
|
|
|
```cargo
|
|
[dependencies]
|
|
time = "*"
|
|
```
|
|
|
|
"#
|
|
.into())
|
|
);
|
|
|
|
assert_eq!(
|
|
ec!(r#"/*!
|
|
* Here is a manifest:
|
|
*
|
|
* ```cargo
|
|
* [dependencies]
|
|
* time = "*"
|
|
* ```
|
|
*/
|
|
fn main() {}
|
|
"#),
|
|
Ok(r#"
|
|
Here is a manifest:
|
|
|
|
```cargo
|
|
[dependencies]
|
|
time = "*"
|
|
```
|
|
|
|
"#
|
|
.into())
|
|
);
|
|
|
|
assert_eq!(
|
|
ec!(r#"//! Here is a manifest:
|
|
//!
|
|
//! ```cargo
|
|
//! [dependencies]
|
|
//! time = "*"
|
|
//! ```
|
|
fn main() {}
|
|
"#),
|
|
Ok(r#"Here is a manifest:
|
|
|
|
```cargo
|
|
[dependencies]
|
|
time = "*"
|
|
```
|
|
"#
|
|
.into())
|
|
);
|
|
}
|
|
|
|
/**
|
|
Generates a default Cargo manifest for the given input.
|
|
*/
|
|
fn default_manifest(input: &Input, input_id: &OsString, bin_name: Option<String>) -> MainResult<toml::value::Table> {
|
|
let mani_str = {
|
|
let pkg_name = input.package_name();
|
|
let bin_name = bin_name.unwrap_or_else(|| format!("{}_{}", &*pkg_name, input_id.to_str().unwrap()));
|
|
let mut subs = HashMap::with_capacity(3);
|
|
subs.insert(consts::MANI_NAME_SUB, &*pkg_name);
|
|
subs.insert(consts::MANI_BIN_NAME_SUB, &*bin_name);
|
|
subs.insert(consts::MANI_FILE_SUB, input.safe_name());
|
|
templates::expand(consts::DEFAULT_MANIFEST, &subs)?
|
|
};
|
|
toml::from_str(&mani_str).map_err(|e| {
|
|
MainError::Tag(
|
|
"could not parse default manifest".into(),
|
|
Box::new(MainError::Other(Box::new(e))),
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
Generates a partial Cargo manifest containing the specified dependencies.
|
|
*/
|
|
fn deps_manifest(deps: &[(String, String)]) -> MainResult<toml::value::Table> {
|
|
let mut mani_str = String::new();
|
|
mani_str.push_str("[dependencies]\n");
|
|
|
|
for &(ref name, ref ver) in deps {
|
|
mani_str.push_str(name);
|
|
mani_str.push('=');
|
|
|
|
// We only want to quote the version if it *isn't* a table.
|
|
let quotes = match ver.starts_with('{') {
|
|
true => "",
|
|
false => "\"",
|
|
};
|
|
mani_str.push_str(quotes);
|
|
mani_str.push_str(ver);
|
|
mani_str.push_str(quotes);
|
|
mani_str.push('\n');
|
|
}
|
|
|
|
toml::from_str(&mani_str).map_err(|e| {
|
|
MainError::Tag(
|
|
"could not parse dependency manifest".into(),
|
|
Box::new(MainError::Other(Box::new(e))),
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
Given two Cargo manifests, merges the second *into* the first.
|
|
|
|
Note that the "merge" in this case is relatively simple: only *top-level* tables are actually merged; everything else is just outright replaced.
|
|
*/
|
|
fn merge_manifest(
|
|
mut into_t: toml::value::Table,
|
|
from_t: toml::value::Table,
|
|
) -> MainResult<toml::value::Table> {
|
|
for (k, v) in from_t {
|
|
match v {
|
|
toml::Value::Table(from_t) => {
|
|
// Merge.
|
|
match into_t.entry(k) {
|
|
toml::map::Entry::Vacant(e) => {
|
|
e.insert(toml::Value::Table(from_t));
|
|
}
|
|
toml::map::Entry::Occupied(e) => {
|
|
let into_t = as_table_mut(e.into_mut()).ok_or(
|
|
"cannot merge manifests: cannot merge \
|
|
table and non-table values",
|
|
)?;
|
|
into_t.extend(from_t);
|
|
}
|
|
}
|
|
}
|
|
v => {
|
|
// Just replace.
|
|
into_t.insert(k, v);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Ok(into_t);
|
|
|
|
fn as_table_mut(t: &mut toml::Value) -> Option<&mut toml::value::Table> {
|
|
match *t {
|
|
toml::Value::Table(ref mut t) => Some(t),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Given a Cargo manifest, attempts to rewrite relative file paths to absolute ones, allowing the manifest to be relocated.
|
|
*/
|
|
fn fix_manifest_paths(mani: toml::value::Table, base: &Path) -> MainResult<toml::value::Table> {
|
|
// Values that need to be rewritten:
|
|
let paths: &[&[&str]] = &[
|
|
&["build-dependencies", "*", "path"],
|
|
&["dependencies", "*", "path"],
|
|
&["dev-dependencies", "*", "path"],
|
|
&["package", "build"],
|
|
&["target", "*", "dependencies", "*", "path"],
|
|
];
|
|
|
|
let mut mani = toml::Value::Table(mani);
|
|
|
|
for path in paths {
|
|
iterate_toml_mut_path(&mut mani, path, &mut |v| {
|
|
if let toml::Value::String(ref mut s) = *v {
|
|
if Path::new(s).is_relative() {
|
|
let p = base.join(&*s);
|
|
if let Some(p) = p.to_str() {
|
|
*s = p.into()
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
})?
|
|
}
|
|
|
|
match mani {
|
|
toml::Value::Table(mani) => Ok(mani),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/**
|
|
Iterates over the specified TOML values via a path specification.
|
|
*/
|
|
fn iterate_toml_mut_path<F>(
|
|
base: &mut toml::Value,
|
|
path: &[&str],
|
|
on_each: &mut F,
|
|
) -> MainResult<()>
|
|
where
|
|
F: FnMut(&mut toml::Value) -> MainResult<()>,
|
|
{
|
|
if path.is_empty() {
|
|
return on_each(base);
|
|
}
|
|
|
|
let cur = path[0];
|
|
let tail = &path[1..];
|
|
|
|
if cur == "*" {
|
|
if let toml::Value::Table(ref mut tab) = *base {
|
|
for (_, v) in tab {
|
|
iterate_toml_mut_path(v, tail, on_each)?;
|
|
}
|
|
}
|
|
} else if let toml::Value::Table(ref mut tab) = *base {
|
|
if let Some(v) = tab.get_mut(cur) {
|
|
iterate_toml_mut_path(v, tail, on_each)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|