Skip to content

Commit

Permalink
Auto merge of #7375 - ehuss:extract-platform, r=alexcrichton
Browse files Browse the repository at this point in the history
Extract Platform to a separate crate.

This moves the `Platform`, `Cfg`, `CfgExpr` types to a new crate named "cargo-platform".  The intent here is to give users of `cargo_metadata` a way of parsing and inspecting cargo's platform values.

Along the way, I rewrote the error handling to remove `failure`, and to slightly improve the output.

I'm having doubts whether or not this is a good idea.  As you can see from the `examples/matches.rs` example, it is nontrivial to use this (which also misses cargo's config values and environment variables).  I don't know if anyone will actually use this.  If this doesn't seem to have value, I would suggest closing it.

I've also included a sample script, `publish.py`, for publishing cargo itself.  I suspect it will need tweaking, but I figure it would be a start and open for feedback.
  • Loading branch information
bors committed Sep 20, 2019
2 parents b6c6f68 + 57c96c1 commit 7ab4778
Show file tree
Hide file tree
Showing 18 changed files with 589 additions and 265 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ path = "src/cargo/lib.rs"
[dependencies]
atty = "0.2"
bytesize = "1.0"
cargo-platform = { path = "crates/cargo-platform", version = "0.1" }
crates-io = { path = "crates/crates-io", version = "0.28" }
crossbeam-utils = "0.6"
crypto-hash = "0.3.1"
Expand Down
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
displayName: "Check rustfmt (crates-io)"
- bash: cd crates/resolver-tests && cargo fmt --all -- --check
displayName: "Check rustfmt (resolver-tests)"
- bash: cd crates/cargo-platform && cargo fmt --all -- --check
displayName: "Check rustfmt (cargo-platform)"
variables:
TOOLCHAIN: stable

Expand Down
3 changes: 3 additions & 0 deletions ci/azure-test-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@ steps:

- bash: cargo test -p cargo-test-support
displayName: "cargo test -p cargo-test-support"

- bash: cargo test -p cargo-platform
displayName: "cargo test -p cargo-platform"
13 changes: 13 additions & 0 deletions crates/cargo-platform/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "cargo-platform"
version = "0.1.0"
authors = ["The Cargo Project Developers"]
edition = "2018"
license = "MIT OR Apache-2.0"
homepage = "https://github.com/rust-lang/cargo"
repository = "https://github.com/rust-lang/cargo"
documentation = "https://docs.rs/cargo-platform"
description = "Cargo's representation of a target platform."

[dependencies]
serde = { version = "1.0.82", features = ['derive'] }
55 changes: 55 additions & 0 deletions crates/cargo-platform/examples/matches.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//! This example demonstrates how to filter a Platform based on the current
//! host target.

use cargo_platform::{Cfg, Platform};
use std::process::Command;
use std::str::FromStr;

static EXAMPLES: &[&str] = &[
"cfg(windows)",
"cfg(unix)",
"cfg(target_os=\"macos\")",
"cfg(target_os=\"linux\")",
"cfg(any(target_arch=\"x86\", target_arch=\"x86_64\"))",
];

fn main() {
let target = get_target();
let cfgs = get_cfgs();
println!("host target={} cfgs:", target);
for cfg in &cfgs {
println!(" {}", cfg);
}
let mut examples: Vec<&str> = EXAMPLES.iter().copied().collect();
examples.push(target.as_str());
for example in examples {
let p = Platform::from_str(example).unwrap();
println!("{:?} matches: {:?}", example, p.matches(&target, &cfgs));
}
}

fn get_target() -> String {
let output = Command::new("rustc")
.arg("-Vv")
.output()
.expect("rustc failed to run");
let stdout = String::from_utf8(output.stdout).unwrap();
for line in stdout.lines() {
if line.starts_with("host: ") {
return String::from(&line[6..]);
}
}
panic!("Failed to find host: {}", stdout);
}

fn get_cfgs() -> Vec<Cfg> {
let output = Command::new("rustc")
.arg("--print=cfg")
.output()
.expect("rustc failed to run");
let stdout = String::from_utf8(output.stdout).unwrap();
stdout
.lines()
.map(|line| Cfg::from_str(line).unwrap())
.collect()
}
147 changes: 94 additions & 53 deletions src/cargo/util/cfg.rs → crates/cargo-platform/src/cfg.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
use crate::error::{ParseError, ParseErrorKind::*};
use std::fmt;
use std::iter;
use std::str::{self, FromStr};

use crate::util::CargoResult;

#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Clone, Debug)]
pub enum Cfg {
Name(String),
KeyPair(String, String),
}

/// A cfg expression.
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Clone, Debug)]
pub enum CfgExpr {
Not(Box<CfgExpr>),
Expand All @@ -18,6 +12,15 @@ pub enum CfgExpr {
Value(Cfg),
}

/// A cfg value.
#[derive(Eq, PartialEq, Hash, Ord, PartialOrd, Clone, Debug)]
pub enum Cfg {
/// A named cfg value, like `unix`.
Name(String),
/// A key/value cfg pair, like `target_os = "linux"`.
KeyPair(String, String),
}

#[derive(PartialEq)]
enum Token<'a> {
LeftParen,
Expand All @@ -28,23 +31,27 @@ enum Token<'a> {
String(&'a str),
}

#[derive(Clone)]
struct Tokenizer<'a> {
s: iter::Peekable<str::CharIndices<'a>>,
orig: &'a str,
}

struct Parser<'a> {
t: iter::Peekable<Tokenizer<'a>>,
t: Tokenizer<'a>,
}

impl FromStr for Cfg {
type Err = failure::Error;
type Err = ParseError;

fn from_str(s: &str) -> CargoResult<Cfg> {
fn from_str(s: &str) -> Result<Cfg, Self::Err> {
let mut p = Parser::new(s);
let e = p.cfg()?;
if p.t.next().is_some() {
failure::bail!("malformed cfg value or key/value pair: `{}`", s)
if let Some(rest) = p.rest() {
return Err(ParseError::new(
p.t.orig,
UnterminatedExpression(rest.to_string()),
));
}
Ok(e)
}
Expand Down Expand Up @@ -85,16 +92,16 @@ impl CfgExpr {
}

impl FromStr for CfgExpr {
type Err = failure::Error;
type Err = ParseError;

fn from_str(s: &str) -> CargoResult<CfgExpr> {
fn from_str(s: &str) -> Result<CfgExpr, Self::Err> {
let mut p = Parser::new(s);
let e = p.expr()?;
if p.t.next().is_some() {
failure::bail!(
"can only have one cfg-expression, consider using all() or \
any() explicitly"
)
if let Some(rest) = p.rest() {
return Err(ParseError::new(
p.t.orig,
UnterminatedExpression(rest.to_string()),
));
}
Ok(e)
}
Expand Down Expand Up @@ -131,14 +138,13 @@ impl<'a> Parser<'a> {
t: Tokenizer {
s: s.char_indices().peekable(),
orig: s,
}
.peekable(),
},
}
}

fn expr(&mut self) -> CargoResult<CfgExpr> {
match self.t.peek() {
Some(&Ok(Token::Ident(op @ "all"))) | Some(&Ok(Token::Ident(op @ "any"))) => {
fn expr(&mut self) -> Result<CfgExpr, ParseError> {
match self.peek() {
Some(Ok(Token::Ident(op @ "all"))) | Some(Ok(Token::Ident(op @ "any"))) => {
self.t.next();
let mut e = Vec::new();
self.eat(&Token::LeftParen)?;
Expand All @@ -155,67 +161,108 @@ impl<'a> Parser<'a> {
Ok(CfgExpr::Any(e))
}
}
Some(&Ok(Token::Ident("not"))) => {
Some(Ok(Token::Ident("not"))) => {
self.t.next();
self.eat(&Token::LeftParen)?;
let e = self.expr()?;
self.eat(&Token::RightParen)?;
Ok(CfgExpr::Not(Box::new(e)))
}
Some(&Ok(..)) => self.cfg().map(CfgExpr::Value),
Some(&Err(..)) => Err(self.t.next().unwrap().err().unwrap()),
None => failure::bail!(
"expected start of a cfg expression, \
found nothing"
),
Some(Ok(..)) => self.cfg().map(CfgExpr::Value),
Some(Err(..)) => Err(self.t.next().unwrap().err().unwrap()),
None => Err(ParseError::new(
self.t.orig,
IncompleteExpr("start of a cfg expression"),
)),
}
}

fn cfg(&mut self) -> CargoResult<Cfg> {
fn cfg(&mut self) -> Result<Cfg, ParseError> {
match self.t.next() {
Some(Ok(Token::Ident(name))) => {
let e = if self.r#try(&Token::Equals) {
let val = match self.t.next() {
Some(Ok(Token::String(s))) => s,
Some(Ok(t)) => failure::bail!("expected a string, found {}", t.classify()),
Some(Ok(t)) => {
return Err(ParseError::new(
self.t.orig,
UnexpectedToken {
expected: "a string",
found: t.classify(),
},
))
}
Some(Err(e)) => return Err(e),
None => failure::bail!("expected a string, found nothing"),
None => {
return Err(ParseError::new(self.t.orig, IncompleteExpr("a string")))
}
};
Cfg::KeyPair(name.to_string(), val.to_string())
} else {
Cfg::Name(name.to_string())
};
Ok(e)
}
Some(Ok(t)) => failure::bail!("expected identifier, found {}", t.classify()),
Some(Ok(t)) => Err(ParseError::new(
self.t.orig,
UnexpectedToken {
expected: "identifier",
found: t.classify(),
},
)),
Some(Err(e)) => Err(e),
None => failure::bail!("expected identifier, found nothing"),
None => Err(ParseError::new(self.t.orig, IncompleteExpr("identifier"))),
}
}

fn peek(&mut self) -> Option<Result<Token<'a>, ParseError>> {
self.t.clone().next()
}

fn r#try(&mut self, token: &Token<'a>) -> bool {
match self.t.peek() {
Some(&Ok(ref t)) if token == t => {}
match self.peek() {
Some(Ok(ref t)) if token == t => {}
_ => return false,
}
self.t.next();
true
}

fn eat(&mut self, token: &Token<'a>) -> CargoResult<()> {
fn eat(&mut self, token: &Token<'a>) -> Result<(), ParseError> {
match self.t.next() {
Some(Ok(ref t)) if token == t => Ok(()),
Some(Ok(t)) => failure::bail!("expected {}, found {}", token.classify(), t.classify()),
Some(Ok(t)) => Err(ParseError::new(
self.t.orig,
UnexpectedToken {
expected: token.classify(),
found: t.classify(),
},
)),
Some(Err(e)) => Err(e),
None => failure::bail!("expected {}, but cfg expr ended", token.classify()),
None => Err(ParseError::new(
self.t.orig,
IncompleteExpr(token.classify()),
)),
}
}

/// Returns the rest of the input from the current location.
fn rest(&self) -> Option<&str> {
let mut s = self.t.s.clone();
loop {
match s.next() {
Some((_, ' ')) => {}
Some((start, _ch)) => return Some(&self.t.orig[start..]),
None => return None,
}
}
}
}

impl<'a> Iterator for Tokenizer<'a> {
type Item = CargoResult<Token<'a>>;
type Item = Result<Token<'a>, ParseError>;

fn next(&mut self) -> Option<CargoResult<Token<'a>>> {
fn next(&mut self) -> Option<Result<Token<'a>, ParseError>> {
loop {
match self.s.next() {
Some((_, ' ')) => {}
Expand All @@ -229,7 +276,7 @@ impl<'a> Iterator for Tokenizer<'a> {
return Some(Ok(Token::String(&self.orig[start + 1..end])));
}
}
return Some(Err(failure::format_err!("unterminated string in cfg")));
return Some(Err(ParseError::new(self.orig, UnterminatedString)));
}
Some((start, ch)) if is_ident_start(ch) => {
while let Some(&(end, ch)) = self.s.peek() {
Expand All @@ -242,13 +289,7 @@ impl<'a> Iterator for Tokenizer<'a> {
return Some(Ok(Token::Ident(&self.orig[start..])));
}
Some((_, ch)) => {
return Some(Err(failure::format_err!(
"unexpected character in \
cfg `{}`, expected parens, \
a comma, an identifier, or \
a string",
ch
)));
return Some(Err(ParseError::new(self.orig, UnexpectedChar(ch))));
}
None => return None,
}
Expand All @@ -265,7 +306,7 @@ fn is_ident_rest(ch: char) -> bool {
}

impl<'a> Token<'a> {
fn classify(&self) -> &str {
fn classify(&self) -> &'static str {
match *self {
Token::LeftParen => "`(`",
Token::RightParen => "`)`",
Expand Down
Loading

0 comments on commit 7ab4778

Please sign in to comment.