stackage/etc/commenter/src/lib.rs
2022-01-15 00:20:58 +01:00

419 lines
12 KiB
Rust

use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader, LineWriter, Lines, Write};
use std::path::Path;
use std::process::Command;
use lazy_regex::regex;
use serde::{Deserialize, Deserializer};
pub fn clear() {
handle(true, |loc, _lines| match loc {
// Add empty array to keep yaml valid
Location::Lib => vec![" []".to_owned()],
Location::Test | Location::Bench => vec![],
});
}
pub fn add(lib: Vec<String>, test: Vec<String>, bench: Vec<String>) {
handle(true, |loc, mut lines| {
lines.extend(match loc {
Location::Lib => lib.clone(),
Location::Test => test.clone(),
Location::Bench => bench.clone(),
});
lines.sort();
lines
});
}
enum VersionTag {
Manual(String),
Auto(String),
}
impl VersionTag {
fn tag(&self) -> &'static str {
match self {
VersionTag::Manual(_) => "manual",
VersionTag::Auto(_) => "auto",
}
}
fn version(&self) -> &str {
match self {
VersionTag::Manual(s) => s,
VersionTag::Auto(s) => s,
}
}
}
pub fn outdated() {
let mut all: Vec<String> = vec![];
let versioned = handle(false, |_loc, lines| {
all.extend(lines);
vec![]
});
let mut map: BTreeMap<String, VersionTag> = BTreeMap::new();
for VersionedPackage { package, version } in versioned {
map.insert(package, VersionTag::Manual(version));
}
let mut support: BTreeMap<(String, String), BTreeSet<(String, String)>> = BTreeMap::new();
for v in all.into_iter() {
let caps = regex!("tried ([^ ]+)-([^,-]+),").captures(&v).unwrap();
let package = caps.get(1).unwrap().as_str().to_owned();
let version = caps.get(2).unwrap().as_str().to_owned();
map.insert(package.clone(), VersionTag::Auto(version.clone()));
if let Some(caps) = regex!("does not support: ([^ ]+)-([^-]+)").captures(&v) {
let dep_package = caps.get(1).unwrap().as_str().to_owned();
let dep_version = caps.get(2).unwrap().as_str().to_owned();
let entry = support.entry((dep_package, dep_version)).or_default();
entry.insert((package, version));
}
}
let entries = map.len() + support.len();
let mut i = 0;
for (package, version) in map {
if is_boot(&package) {
continue;
}
if i % 100 == 0 {
println!("{:02}%", ((i as f64 / entries as f64) * 100.0).floor());
}
i += 1;
let latest = latest_version(&package);
if version.version() != latest {
println!(
"{package} mismatch, {tag}: {version}, hackage: {latest}",
tag = version.tag(),
version = version.version(),
);
}
}
for ((package, version), dependents) in support {
if is_boot(&package) {
continue;
}
if i % 100 == 0 {
println!("{:02}%", ((i as f64 / entries as f64) * 100.0).floor());
}
i += 1;
let latest = latest_version(&package);
if version != latest {
let max = 3;
let dependents_stripped = dependents.len().saturating_sub(max);
let dependents = dependents
.into_iter()
.take(max)
.map(|(p, v)| format!("{p}-{v}"))
.collect::<Vec<String>>()
.join(", ");
let dependents = if dependents_stripped > 0 {
format!("{dependents} and {dependents_stripped} more")
} else {
dependents
};
println!(
"{package} mismatch, snapshot: {version}, hackage: {latest}, dependents: {dependents}"
);
}
}
}
fn is_boot(package: &str) -> bool {
[
"Cabal",
"base",
"bytestring",
"containers",
"containers",
"directory",
"filepath",
"deepseq",
"ghc",
"ghc-bignum",
"ghc-boot",
"ghc-boot-th",
"ghc-prim",
"ghc-lib-parser", // not a boot lib, but tied to the GHC version.
"integer-gmp",
"process",
"stm",
"template-haskell",
"text",
"time",
]
.contains(&package)
}
fn latest_version(pkg: &str) -> String {
String::from_utf8(
Command::new("latest-version")
.args([pkg])
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim()
.to_owned()
}
enum State {
LookingForLibBounds,
ProcessingLibBounds,
LookingForTestBounds,
ProcessingTestBounds,
LookingForBenchBounds,
ProcessingBenchBounds,
Done,
}
struct VersionedPackage {
package: String,
version: String,
}
fn parse_versioned_package(s: &str) -> Option<VersionedPackage> {
if let Some(caps) = regex!(r#"- *([^ ]+) < *0 *# *([\d.]+)"#).captures(s) {
let package = caps.get(1).unwrap().as_str().to_owned();
let version = caps.get(2).unwrap().as_str().to_owned();
Some(VersionedPackage { package, version })
} else if let Some(caps) = regex!(r#"- *([^ ]+) *# *([\d.]+)"#).captures(s) {
let package = caps.get(1).unwrap().as_str().to_owned();
let version = caps.get(2).unwrap().as_str().to_owned();
Some(VersionedPackage { package, version })
} else {
None
}
}
fn handle<F>(write: bool, mut f: F) -> Vec<VersionedPackage>
where
F: FnMut(Location, Vec<String>) -> Vec<String>,
{
let path = "build-constraints.yaml";
let mut new_lines: Vec<String> = vec![];
let mut versioned_packages: Vec<VersionedPackage> = vec![];
let mut state = State::LookingForLibBounds;
let mut buf = vec![];
for line in read_lines(path).map(|s| s.unwrap()) {
if let Some(versioned_package) = parse_versioned_package(&line) {
versioned_packages.push(versioned_package);
}
match state {
State::LookingForLibBounds => {
if line == r#" "Library and exe bounds failures":"# {
state = State::ProcessingLibBounds;
}
new_lines.push(line);
}
State::ProcessingLibBounds => {
if line == r#" # End of Library and exe bounds failures"# {
new_lines.extend(f(Location::Lib, buf).into_iter());
buf = vec![];
new_lines.push(line);
state = State::LookingForTestBounds;
} else {
// Remove empty section
if line != " []" {
buf.push(line);
}
}
}
State::LookingForTestBounds => {
if line == r#" # Test bounds issues"# {
state = State::ProcessingTestBounds;
}
new_lines.push(line);
}
State::ProcessingTestBounds => {
if line == r#" # End of Test bounds issues"# {
new_lines.extend(f(Location::Test, buf).into_iter());
buf = vec![];
new_lines.push(line);
state = State::LookingForBenchBounds;
} else {
buf.push(line);
}
}
State::LookingForBenchBounds => {
if line == r#" # Benchmark bounds issues"# {
state = State::ProcessingBenchBounds;
}
new_lines.push(line);
}
State::ProcessingBenchBounds => {
if line == r#" # End of Benchmark bounds issues"# {
new_lines.extend(f(Location::Bench, buf).into_iter());
buf = vec![];
new_lines.push(line);
state = State::Done;
} else {
buf.push(line);
}
}
State::Done => {
new_lines.push(line);
}
}
}
if write {
let file = File::create(path).unwrap();
let mut file = LineWriter::new(file);
for line in new_lines {
file.write_all((line + "\n").as_bytes()).unwrap();
}
file.flush().unwrap();
}
versioned_packages
}
enum Location {
Lib,
Test,
Bench,
}
fn read_lines<P>(filename: P) -> Lines<BufReader<File>>
where
P: AsRef<Path>,
{
let file = File::open(filename).unwrap();
BufReader::new(file).lines()
}
#[derive(Deserialize)]
struct SnapshotYaml {
// flags: BTreeMap<PackageName, BTreeMap<PackageFlag, bool>>,
// publish_time
packages: Vec<SnapshotPackage>,
// hidden
// resolver
}
#[derive(Deserialize)]
struct SnapshotPackage {
hackage: PackageWithVersionAndSha,
// pantry-tree
}
#[derive(PartialOrd, Ord, PartialEq, Eq, Clone)]
struct PackageName(String);
impl fmt::Display for PackageName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
struct Version(String);
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.fmt(f)
}
}
// zstd-0.1.3.0@sha256:4c0a372251068eb6086b8c3a0a9f347488f08b570a7705844ffeb2c720c97223,3723
struct PackageWithVersionAndSha {
name: PackageName,
version: Version,
}
impl<'de> serde::Deserialize<'de> for PackageWithVersionAndSha {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = String::deserialize(deserializer)?;
let r = regex!(r#"^(.+?)-([.\d]+)@sha256:[\da-z]+,\d+$"#);
if let Some(caps) = r.captures(&s) {
let name = PackageName(caps.get(1).unwrap().as_str().to_owned());
let version = Version(caps.get(2).unwrap().as_str().to_owned());
Ok(Self { name, version })
} else {
Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Other(&s),
&"Invalid PackageVersionWithSha",
))
}
}
}
fn yaml_from_file<A, P: AsRef<Path>>(path: P) -> Result<A, Box<dyn Error>>
where
A: for<'de> Deserialize<'de>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
let u = serde_yaml::from_reader(reader)?;
Ok(u)
}
struct Snapshot {
packages: BTreeMap<PackageName, Diff<Version>>,
}
#[derive(Clone, Copy)]
enum Diff<A> {
Left(A),
Right(A),
Both(A, A),
}
fn to_diff(a: SnapshotYaml, b: SnapshotYaml) -> Snapshot {
let mut packages = BTreeMap::new();
for s in a.packages {
let package = s.hackage;
packages.insert(package.name, Diff::Left(package.version));
}
for s in b.packages {
let package = s.hackage;
let name = package.name;
let version = package.version;
if let Some(a) = packages.remove(&name) {
match a {
Diff::Left(a) => {
if a == version {
packages.remove(&name);
} else {
packages.insert(name, Diff::Both(a, version));
}
}
_ => unreachable!(),
}
} else {
packages.insert(name, Diff::Right(version));
}
}
Snapshot { packages }
}
pub fn diff_snapshot(a: String, b: String) {
let diff = to_diff(yaml_from_file(a).unwrap(), yaml_from_file(b).unwrap());
for (name, diff) in diff.packages {
let s = match diff {
Diff::Left(a) => format!("- {name}-{a}"),
Diff::Right(b) => format!("+ {name}-{b}"),
Diff::Both(a, b) => format!("~ {name}-{a} -> {b}"),
};
println!("{s}");
}
}