Skip to content
This repository was archived by the owner on Dec 25, 2021. It is now read-only.

Commit a9ed228

Browse files
authored
Merge pull request #1 from whisklabs/develop
Tool for sanitizing Pants dependencies
2 parents 449710c + 61c9a3e commit a9ed228

File tree

7 files changed

+519
-3
lines changed

7 files changed

+519
-3
lines changed

.dockerignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
target/
2+
.idea
3+
4+
/Dockerfile
5+
.gitignore
6+
README.MD

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
2-
name = "pants-cleaner"
3-
version = "0.1.0"
2+
name = "pants-dependency-sanitizer"
3+
version = "0.2.0"
44
authors = ["C.Solovev <[email protected]>"]
55
edition = "2018"
66

@@ -11,4 +11,11 @@ structopt = "0.2.15"
1111
# Serialization
1212
serde = "1.0.103"
1313
serde_derive = "1.0.103"
14-
serde_json = "1.0.42"
14+
serde_json = "1.0.42"
15+
16+
[profile.release]
17+
lto = true
18+
19+
[[bin]]
20+
name = "dep-sanitizer"
21+
path = "src/main.rs"

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Create a container with statically linked binary (container size ~ 2 MB)
2+
3+
FROM ekidd/rust-musl-builder:stable AS builder
4+
5+
COPY . .
6+
RUN sudo chown -R rust:rust .
7+
RUN cargo build --release
8+
9+
FROM scratch
10+
11+
COPY --from=builder /home/rust/src/target/x86_64-unknown-linux-musl/release/pants-cleaner /app
12+
WORKDIR /project
13+
ENTRYPOINT ["/app"]

README.MD

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
# Tool for sanitize Pants dependencies.
3+
4+
This tool works with a report from the followed command (run from the root folder of an inspected project):
5+
6+
./pants -q dep-usage.jvm --no-summary src/scala/:: > deps.json
7+
8+
Make this report before using this tool, it may take a few minutes!
9+
10+
## How to build
11+
12+
Binary (cargo required - [how to install rust](https://www.rust-lang.org/tools/install))
13+
14+
cargo build --release
15+
16+
Docker (docker required)(may take a few minutes)
17+
18+
docker build -t dep-sanitizer.
19+
20+
21+
## How to use
22+
23+
Run this tool from the root of your Pants project. If report file isn't in project root,
24+
specified report file via `--report_file` e.g.
25+
26+
dep-sanitizer --report_file=/tmp/deps.json unused show
27+
28+
For getting help
29+
30+
dep-sanitizer help
31+
32+
For showing all unused dependencies
33+
34+
dep-sanitizer --prefix=src/ unused show
35+
36+
For remove all unused dependencies
37+
38+
dep-sanitizer unused fix
39+
40+
For showing all undeclared but used transitively modules dependencies
41+
42+
dep-sanitizer undeclared show
43+
44+
For adding all undeclared dependencies to corresponded BUILD files
45+
46+
dep-sanitizer undeclared fix
47+
48+
Use with docker
49+
50+
docker run -v ${PWD}/../atlas:/project/ dep-sanitizer unused show
51+
52+
Note that, we mount 'atlas' project into container as 'project' folder. This folder
53+
should contain 'deps.json' file as well.
54+
55+
56+

src/main.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#[macro_use]
2+
extern crate serde_derive;
3+
4+
use std::path::PathBuf;
5+
use structopt::StructOpt;
6+
7+
mod sanitizer;
8+
9+
#[derive(StructOpt, Debug)]
10+
#[structopt(
11+
name = "pants-cleaner",
12+
about = "A tool for optimize pants jvm dependencies"
13+
)]
14+
pub struct Config {
15+
/// Full path to Pants 'dep-usage.jvm' report file in Json format.
16+
/// You should create it before using this tool like this
17+
/// `./pants -q dep-usage.jvm --no-summary src/:: > deps.json`
18+
/// and provide full path to this file.
19+
#[structopt(short, long, parse(from_os_str), default_value = "deps.json")]
20+
report_file: PathBuf,
21+
22+
/// Applies any action only for modules that start with this include_prefix.
23+
#[structopt(short, long, default_value = "src/scala/")]
24+
prefix: String,
25+
26+
/// If dependency was annotated with this marker tath it will be skipped to sanitize(removing)
27+
#[structopt(short, long, default_value = "#skip-sanitize")]
28+
skip_marker: String,
29+
30+
#[structopt(subcommand)]
31+
cmd: Command,
32+
}
33+
34+
#[derive(StructOpt, Debug)]
35+
pub enum Command {
36+
/// Manage unused but declared modules dependencies
37+
#[structopt(name = "unused")]
38+
Unused {
39+
#[structopt(subcommand)]
40+
cmd: UnusedSubCommand,
41+
},
42+
/// Manage undeclared but used transitively modules dependencies
43+
#[structopt(name = "undeclared")]
44+
Undeclared {
45+
#[structopt(subcommand)]
46+
cmd: UndeclaredSubCommand,
47+
},
48+
}
49+
50+
#[derive(StructOpt, Debug)]
51+
pub enum UnusedSubCommand {
52+
/// Shows all unused dependencies
53+
#[structopt(name = "show")]
54+
Show,
55+
/// Removes all unused dependencies
56+
#[structopt(name = "fix")]
57+
Fix,
58+
}
59+
60+
#[derive(StructOpt, Debug)]
61+
pub enum UndeclaredSubCommand {
62+
/// Shows all undeclared dependencies
63+
#[structopt(name = "show")]
64+
Show,
65+
/// Add all undeclared dependencies to corresponded BUILD files
66+
#[structopt(name = "fix")]
67+
Fix,
68+
}
69+
70+
fn main() {
71+
let config: Config = dbg!(Config::from_args());
72+
sanitizer::perform(config);
73+
}

src/sanitizer/deps_manager.rs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
//! This module provides functionality for read from and write to Pants BUILD file.
2+
3+
use std::collections::BTreeSet;
4+
use std::error::Error;
5+
use std::fmt;
6+
use std::fmt::{Debug, Formatter};
7+
use std::fs;
8+
use std::fs::File;
9+
use std::io::{BufRead, BufReader, BufWriter, Write};
10+
use std::path::PathBuf;
11+
use std::string::ToString;
12+
13+
/// Representation fof Pants address.
14+
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
15+
pub struct Address {
16+
pub folder: String,
17+
pub module_name: String,
18+
}
19+
20+
impl Address {
21+
pub fn from_str(str: &str) -> Self {
22+
let split = str.split(':').collect::<Vec<_>>();
23+
let folder = split[0].to_string();
24+
let module_name = split[1].to_string();
25+
Address {
26+
folder,
27+
module_name,
28+
}
29+
}
30+
/// In the case when 1 folder == 1 module return true.
31+
pub fn is_simple(&self) -> bool {
32+
self.folder.ends_with(&self.module_name)
33+
}
34+
35+
/// Line corresponds to this address.
36+
pub fn match_line(&self, line: &str) -> bool {
37+
line.contains(&format!("'{}:{}'", self.folder, self.module_name)) // full address
38+
|| (self.is_simple() && line.contains(&format!("'{}'", &self.folder))) // only folder
39+
|| line.contains(&format!("':{}'", self.module_name)) // only module name
40+
}
41+
42+
pub fn as_str(&self) -> String {
43+
if self.is_simple() {
44+
self.folder.to_string()
45+
} else {
46+
format!("{}:{}", self.folder, self.module_name)
47+
}
48+
}
49+
}
50+
51+
impl Debug for Address {
52+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53+
write!(f, "{:?}:{:?}", self.folder, self.module_name)
54+
}
55+
}
56+
57+
/// Finds BUILD file and removes lines with unused dependencies, returns number of removed lines.
58+
pub fn remove_deps(
59+
module: &Address,
60+
deps: Vec<Address>,
61+
skip_marker: &str,
62+
) -> Result<usize, Box<dyn Error>> {
63+
let mut counter = 0;
64+
65+
for entry in fs::read_dir(&module.folder)? {
66+
let entry = entry?;
67+
if entry.file_name() == "BUILD" {
68+
// read and filter unused dependencies
69+
let cleaned = {
70+
let file = BufReader::new(File::open(entry.path())?);
71+
72+
let mut inside_module_section = module.is_simple();
73+
let mut inside_module_dep_section = false;
74+
75+
file.lines()
76+
.filter_map(|line| {
77+
let line = line.unwrap_or_else(|_| {
78+
panic!("Couldn't read line from {}/BUILD ", module.folder)
79+
});
80+
81+
if line.contains("name=") && line.contains(&module.module_name) {
82+
inside_module_section = true;
83+
}
84+
85+
if inside_module_section && line.contains("dependencies") {
86+
inside_module_dep_section = true;
87+
}
88+
89+
if inside_module_dep_section && line.contains(']') {
90+
inside_module_dep_section = false;
91+
inside_module_section = false; // actually no, but it's ok so simplifying
92+
}
93+
94+
if inside_module_dep_section
95+
&& !line.contains(skip_marker)
96+
&& deps.iter().any(|target| target.match_line(&line))
97+
{
98+
// we are in dependency block of required module
99+
// if line contents unused dep remove it from result
100+
counter += 1;
101+
None
102+
} else {
103+
Some(line)
104+
}
105+
})
106+
.collect::<Vec<String>>()
107+
};
108+
109+
// write filtered dependencies back in BUILD file
110+
let mut file = BufWriter::new(File::create(entry.path())?);
111+
for line in cleaned {
112+
writeln!(file, "{}", line)?;
113+
}
114+
file.flush()?;
115+
break;
116+
}
117+
}
118+
Ok(counter)
119+
}
120+
121+
/// Finds a BUILD file and inserts lines with undeclared dependencies, returns number of inserted lines.
122+
pub fn add_deps(
123+
module: &Address,
124+
deps: Vec<Address>,
125+
skip_marker: &str,
126+
) -> Result<usize, Box<dyn Error>> {
127+
let mut counter = 0;
128+
for entry in fs::read_dir(&module.folder)? {
129+
let entry = entry?;
130+
if entry.file_name() == "BUILD" {
131+
// read existed, add undeclared and sort
132+
133+
let updated_deps =
134+
add_deps_to_file(entry.path(), &module, deps, &mut counter, skip_marker)?;
135+
136+
// write filtered dependencies back in BUILD file
137+
let mut file = BufWriter::new(File::create(entry.path())?);
138+
for line in updated_deps {
139+
writeln!(file, "{}", line)?;
140+
}
141+
file.flush()?;
142+
break;
143+
} else {
144+
}
145+
}
146+
Ok(counter as usize)
147+
}
148+
149+
/// Adds new deps to dependency block of the BUILD file.
150+
fn add_deps_to_file(
151+
file: PathBuf,
152+
module: &Address,
153+
deps: Vec<Address>,
154+
counter: &mut isize,
155+
skip_marker: &str,
156+
) -> Result<Vec<String>, Box<dyn Error>> {
157+
let file = BufReader::new(File::open(file)?);
158+
159+
let deps_iter = deps
160+
.into_iter()
161+
.map(|dep| format!(" '{}',", dep.as_str()));
162+
163+
let mut result: Vec<String> = Vec::new();
164+
// we use BTreeSet because deps should be sorted and unique
165+
let mut updated_deps = BTreeSet::new();
166+
let mut inside_module_section = module.is_simple();
167+
let mut inside_module_dep_section = false;
168+
169+
for line in file.lines() {
170+
let line = line?;
171+
172+
if line.contains("name=") && line.contains(&module.module_name) {
173+
inside_module_section = true;
174+
}
175+
176+
if line.contains(']') && inside_module_dep_section {
177+
// add undeclared to deps
178+
let before = updated_deps.len() as isize;
179+
updated_deps.extend(deps_iter.clone());
180+
*counter += updated_deps.len() as isize - before;
181+
// add deps to file
182+
result.extend(updated_deps.clone());
183+
result.push(line);
184+
inside_module_dep_section = false;
185+
inside_module_section = false; // actually no, but it's ok so simplifying
186+
continue;
187+
}
188+
189+
if inside_module_dep_section {
190+
// we are into dep block just add new line into deps set
191+
if line.ends_with(',') || line.contains(skip_marker) {
192+
updated_deps.insert(line.replace('"', "'"));
193+
} else if !line.is_empty() {
194+
updated_deps.insert(line.replace('"', "'") + ",");
195+
};
196+
continue;
197+
}
198+
199+
if inside_module_section && line.contains("dependencies") {
200+
inside_module_dep_section = true;
201+
result.push(line);
202+
continue;
203+
}
204+
205+
result.push(line);
206+
}
207+
208+
Ok(result)
209+
}

0 commit comments

Comments
 (0)