mirror of
https://gitlab.redox-os.org/redox-os/redox.git
synced 2026-06-21 20:34:17 +08:00
Implement web generation for packages
This commit is contained in:
parent
9d672df778
commit
69fa5f1bc0
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,6 +12,7 @@
|
||||
.devcontainer/
|
||||
# Cookbook
|
||||
/repo
|
||||
/web
|
||||
/cookbook.toml
|
||||
source
|
||||
source.tmp
|
||||
|
||||
@ -63,15 +63,17 @@ const REPO_HELP_STR: &str = r#"
|
||||
--repo-binary override recipes config to use repo_binary
|
||||
|
||||
cook env and their defaults:
|
||||
CI= set to any value to disable TUI
|
||||
COOKBOOK_LOGS= whether to capture build logs (default is !CI)
|
||||
COOKBOOK_OFFLINE=false prevent internet access if possible
|
||||
CI= set to any value to disable TUI
|
||||
COOKBOOK_LOGS= whether to capture build logs (default is !CI)
|
||||
COOKBOOK_OFFLINE=false prevent internet access if possible
|
||||
ignored when command "fetch" is used
|
||||
COOKBOOK_NONSTOP=false keep running even a recipe build failed
|
||||
COOKBOOK_VERBOSE=true print success/error on each recipe
|
||||
COOKBOOK_CLEAN_BUILD=false remove build directory before building
|
||||
COOKBOOK_CLEAN_TARGET=false remove target directory after building
|
||||
COOKBOOK_MAKE_JOBS= override build jobs count from nproc
|
||||
COOKBOOK_NONSTOP=false keep running even a recipe build failed
|
||||
COOKBOOK_VERBOSE=true print success/error on each recipe
|
||||
COOKBOOK_CLEAN_BUILD=false remove build directory before building
|
||||
COOKBOOK_CLEAN_TARGET=false remove target directory after building
|
||||
COOKBOOK_WRITE_FILETREE=false whether to write stage files tree
|
||||
COOKBOOK_MAKE_JOBS= override build jobs count from nproc
|
||||
COOKBOOK_WEB=false whether to generate package web files
|
||||
"#;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -698,16 +700,20 @@ fn handle_cook(
|
||||
package(&recipe, &stage_dirs, &auto_deps, logger)
|
||||
.map_err(|err| anyhow!("failed to package: {:?}", err))?;
|
||||
|
||||
if config.cook.clean_target {
|
||||
if config.cook.clean_target || config.cook.write_filetree {
|
||||
let stage_dirs = get_stage_dirs(&recipe.recipe.optional_packages, &target_dir);
|
||||
for stage_dir in stage_dirs {
|
||||
if stage_dir.is_dir() {
|
||||
let mut stage_files_buf = String::new();
|
||||
tree::walk_file_tree(&stage_dir, "", &mut stage_files_buf)
|
||||
.context("failed to walk stage files tree")?;
|
||||
fs::write(stage_dir.with_added_extension("files"), stage_files_buf)
|
||||
.context("unable to write stage files")?;
|
||||
fs::remove_dir_all(&stage_dir).context("failed to remove stage dir")?;
|
||||
if config.cook.write_filetree {
|
||||
let mut stage_files_buf = String::new();
|
||||
tree::walk_file_tree(&stage_dir, "", &mut stage_files_buf)
|
||||
.context("failed to walk stage files tree")?;
|
||||
fs::write(stage_dir.with_added_extension("files"), stage_files_buf)
|
||||
.context("unable to write stage files")?;
|
||||
}
|
||||
if config.cook.clean_target {
|
||||
fs::remove_dir_all(&stage_dir).context("failed to remove stage dir")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ use cookbook::WALK_DEPTH;
|
||||
use cookbook::cook::ident::{get_ident, init_ident};
|
||||
use cookbook::cook::{fetch, package as cook_package};
|
||||
use cookbook::recipe::CookRecipe;
|
||||
use cookbook::web::{CliWebConfig, generate_web};
|
||||
use pkg::package::{Repository, SourceIdentifier};
|
||||
use pkg::{Package, PackageName, recipes};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
@ -29,18 +30,22 @@ struct CliConfig {
|
||||
repo_dir: PathBuf,
|
||||
appstream: bool,
|
||||
recipe_list: Vec<String>,
|
||||
web: Option<CliWebConfig>,
|
||||
}
|
||||
|
||||
impl CliConfig {
|
||||
fn parse_args() -> Result<Self, std::io::Error> {
|
||||
let mut args = env::args().skip(1);
|
||||
let repo_dir = args
|
||||
.next()
|
||||
.expect("Usage: repo_builder <REPO_DIR> <recipe1> <recipe2> ...");
|
||||
let repo_dir = PathBuf::from(
|
||||
args.next()
|
||||
.expect("Usage: repo_builder <REPO_DIR> <recipe1> <recipe2> ..."),
|
||||
);
|
||||
let web = CliWebConfig::parse_args();
|
||||
Ok(CliConfig {
|
||||
repo_dir: PathBuf::from(repo_dir),
|
||||
repo_dir,
|
||||
appstream: env::var("COOKBOOK_APPSTREAM").ok().as_deref() == Some("true"),
|
||||
recipe_list: args.collect(),
|
||||
web,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -268,12 +273,17 @@ fn publish_packages(config: &CliConfig) -> anyhow::Result<()> {
|
||||
packages.insert(package_name, version_str.to_string());
|
||||
}
|
||||
|
||||
let output = toml::to_string(&Repository {
|
||||
let repository = Repository {
|
||||
packages,
|
||||
outdated_packages,
|
||||
})?;
|
||||
};
|
||||
|
||||
let output = toml::to_string(&repository)?;
|
||||
let mut output_file = File::create(&repo_toml_path)?;
|
||||
output_file.write_all(output.as_bytes())?;
|
||||
|
||||
if let Some(conf) = &config.web {
|
||||
generate_web(&repository.packages.keys().cloned().collect(), conf);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -24,6 +24,8 @@ pub struct CookConfigOpt {
|
||||
/// whether to always clean the target directory after building
|
||||
/// (deletes everything except pkgar files)
|
||||
pub clean_target: Option<bool>,
|
||||
/// whether to always write stage.files metadata
|
||||
pub write_filetree: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)]
|
||||
@ -36,6 +38,7 @@ pub struct CookConfig {
|
||||
pub verbose: bool,
|
||||
pub clean_build: bool,
|
||||
pub clean_target: bool,
|
||||
pub write_filetree: bool,
|
||||
}
|
||||
|
||||
impl From<CookConfigOpt> for CookConfig {
|
||||
@ -49,6 +52,7 @@ impl From<CookConfigOpt> for CookConfig {
|
||||
verbose: value.verbose.unwrap(),
|
||||
clean_build: value.clean_build.unwrap(),
|
||||
clean_target: value.clean_target.unwrap(),
|
||||
write_filetree: value.write_filetree.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,6 +110,12 @@ pub fn init_config() {
|
||||
if config.cook_opt.clean_target.is_none() {
|
||||
config.cook_opt.clean_target = Some(extract_env("COOKBOOK_CLEAN_TARGET", false));
|
||||
}
|
||||
if config.cook_opt.write_filetree.is_none() {
|
||||
config.cook_opt.write_filetree = Some(extract_env(
|
||||
"COOKBOOK_WRITE_FILETREE",
|
||||
config.cook_opt.clean_target.unwrap_or(false) || extract_env("COOKBOOK_WEB", false),
|
||||
));
|
||||
}
|
||||
if config.mirrors.len() == 0 {
|
||||
// The GNU FTP mirror below is automatically inserted for convenience
|
||||
// You can choose other mirrors by setting it on cookbook.toml
|
||||
|
||||
@ -2,6 +2,7 @@ pub mod blake3;
|
||||
pub mod config;
|
||||
pub mod cook;
|
||||
pub mod recipe;
|
||||
pub mod web;
|
||||
|
||||
mod progress_bar;
|
||||
|
||||
|
||||
@ -148,6 +148,7 @@ pub struct BuildRecipe {
|
||||
pub struct PackageRecipe {
|
||||
pub dependencies: Vec<PackageName>,
|
||||
pub version: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)]
|
||||
|
||||
122
src/web.rs
Normal file
122
src/web.rs
Normal file
@ -0,0 +1,122 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use pkg::{Package, PackageName};
|
||||
|
||||
use crate::{
|
||||
recipe::CookRecipe,
|
||||
web::html::{generate_html_index, generate_html_pkg},
|
||||
};
|
||||
|
||||
pub mod html;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CliWebConfig {
|
||||
/// path relative to cwd dir to generate web files
|
||||
out_dir: PathBuf,
|
||||
/// absolute url to repo (not the web) instead of "/repo"
|
||||
repo_url: String,
|
||||
/// this repository build url
|
||||
this_repo: String,
|
||||
}
|
||||
|
||||
impl CliWebConfig {
|
||||
pub fn parse_args() -> Option<CliWebConfig> {
|
||||
if env::var("COOKBOOK_WEB").ok().as_deref() != Some("true") {
|
||||
return None;
|
||||
}
|
||||
let Ok(pwd) = env::current_dir() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(CliWebConfig {
|
||||
repo_url: env::var("COOKBOOK_WEB_REPO_URL")
|
||||
.ok()
|
||||
.unwrap_or("/repo".to_string()),
|
||||
out_dir: pwd.join(
|
||||
env::var("COOKBOOK_WEB_OUT_DIR")
|
||||
.ok()
|
||||
.unwrap_or("web".to_string()),
|
||||
),
|
||||
// TODO: Hardcoded URL, maybe get this remote-url next time
|
||||
this_repo: "https://gitlab.redox-os.org/redox-os/redox".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const CSS: &str = include_str!("./web/style.css");
|
||||
|
||||
pub fn generate_web(all_packages: &Vec<String>, config: &CliWebConfig) {
|
||||
let repo_path = &config.out_dir.join(redoxer::target());
|
||||
if !repo_path.is_dir() {
|
||||
fs::create_dir_all(repo_path).unwrap();
|
||||
}
|
||||
|
||||
let mut valid_packages = Vec::new();
|
||||
let mut dependents_map: HashMap<String, BTreeSet<String>> = HashMap::new();
|
||||
|
||||
for package_name in all_packages {
|
||||
let Some(recipe_path) = pkg::recipes::find(package_name) else {
|
||||
continue;
|
||||
};
|
||||
// TODO: Package::from_path
|
||||
let Ok(package) = Package::new(&PackageName::new(package_name).unwrap()) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(recipe) = CookRecipe::from_path(&recipe_path, true, false) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for dep in &package.depends {
|
||||
dependents_map
|
||||
.entry(dep.to_string())
|
||||
.or_default()
|
||||
.insert(package.name.to_string());
|
||||
}
|
||||
|
||||
valid_packages.push((package, recipe));
|
||||
}
|
||||
|
||||
for (package, recipe) in &valid_packages {
|
||||
let dependents = dependents_map
|
||||
.get(package.name.as_str())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let stage_files_path = recipe.stage_paths().0.with_added_extension("files");
|
||||
let stage_files = fs::read_to_string(stage_files_path).ok();
|
||||
|
||||
let html_path = repo_path.join(format!("{}.html", package.name.as_str()));
|
||||
|
||||
generate_html_pkg(
|
||||
&package,
|
||||
&recipe,
|
||||
&dependents.into_iter().collect(),
|
||||
&stage_files,
|
||||
&html_path,
|
||||
&config,
|
||||
);
|
||||
}
|
||||
|
||||
let mut grouped_packages: BTreeMap<String, Vec<&(Package, CookRecipe)>> = BTreeMap::new();
|
||||
|
||||
for item in &valid_packages {
|
||||
let category = get_category(&item.1.dir);
|
||||
grouped_packages.entry(category).or_default().push(item);
|
||||
}
|
||||
|
||||
let index_path = repo_path.join("index.html");
|
||||
let style_path = repo_path.join("style.css");
|
||||
generate_html_index(grouped_packages, &index_path, &config);
|
||||
fs::write(style_path, CSS).expect("Failed to write CSS file");
|
||||
}
|
||||
|
||||
pub(crate) fn get_category(dir: &Path) -> String {
|
||||
let Some(category) = dir.parent().map(|p| p.display().to_string()) else {
|
||||
return "uncategorized".to_string();
|
||||
};
|
||||
category["recipes/".len()..].to_string()
|
||||
}
|
||||
326
src/web/html.rs
Normal file
326
src/web/html.rs
Normal file
@ -0,0 +1,326 @@
|
||||
use crate::cook::ident;
|
||||
use crate::recipe::SourceRecipe;
|
||||
use crate::web::get_category;
|
||||
use crate::{cook::tree::format_size, recipe::CookRecipe};
|
||||
use pkg::Package;
|
||||
use std::collections::BTreeMap;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
pub fn generate_html_pkg(
|
||||
package: &Package,
|
||||
recipe: &CookRecipe,
|
||||
dependents: &Vec<String>,
|
||||
stage_files: &Option<String>,
|
||||
html_path: &Path,
|
||||
config: &crate::web::CliWebConfig,
|
||||
) {
|
||||
let name = &package.name;
|
||||
let version = &package.version;
|
||||
let target = &package.target;
|
||||
let category = &get_category(&recipe.dir);
|
||||
let description = recipe
|
||||
.recipe
|
||||
.package
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|p| p.as_str())
|
||||
.unwrap_or("-");
|
||||
|
||||
let desc_html = recipe
|
||||
.recipe
|
||||
.package
|
||||
.description
|
||||
.as_ref()
|
||||
.map(|desc| format!(r#"<p class="description">{}</p>"#, desc))
|
||||
.unwrap_or_default();
|
||||
|
||||
let repo_url = &config.repo_url;
|
||||
|
||||
let deps_html = if package.depends.is_empty() {
|
||||
String::from("<p>None</p>")
|
||||
} else {
|
||||
let items: Vec<String> = package
|
||||
.depends
|
||||
.iter()
|
||||
.map(|dep| format!(r#"<li><a href="{dep}.html">{dep}</a></li>"#))
|
||||
.collect();
|
||||
format!("<ul>\n{}\n</ul>", items.join("\n"))
|
||||
};
|
||||
|
||||
let dependents_html = if dependents.is_empty() {
|
||||
String::from("<p>None</p>")
|
||||
} else {
|
||||
let items: Vec<String> = dependents
|
||||
.iter()
|
||||
.map(|dep| format!(r#"<li><a href="{dep}.html">{dep}</a></li>"#))
|
||||
.collect();
|
||||
format!("<ul>\n{}\n</ul>", items.join("\n"))
|
||||
};
|
||||
|
||||
let mut source_html = match &recipe.recipe.source {
|
||||
Some(SourceRecipe::Git { git, .. }) => {
|
||||
let host = get_hostname(git);
|
||||
let tree_link = get_tree_url(git, host, &package.source_identifier, None);
|
||||
let short_commit = &package.source_identifier[0..7];
|
||||
format!(
|
||||
r#"
|
||||
<table>
|
||||
<tr><th>Git:</th><td><a href="{git}" target="_blank">{host}</a></td></tr>
|
||||
<tr><th>Commit:</th><td><a href="{tree_link}" target="_blank">{short_commit}</a></td></tr>
|
||||
</table>"#
|
||||
)
|
||||
}
|
||||
Some(SourceRecipe::Tar { tar, .. }) => {
|
||||
let host = get_hostname(tar);
|
||||
format!(
|
||||
r#"<table>
|
||||
<tr><th>Tarball:</th><td><a href="{tar}" target="_blank">{host}</a></td></tr>
|
||||
</table>"#
|
||||
)
|
||||
}
|
||||
Some(SourceRecipe::SameAs { same_as }) => {
|
||||
let r = Path::new(same_as).file_name().unwrap().to_string_lossy();
|
||||
format!(
|
||||
r#"<table>
|
||||
<tr><th>Same as:</th><td><a href="{r}.html">{r}</a></td></tr>
|
||||
</table>"#
|
||||
)
|
||||
}
|
||||
_ => String::from(r#"<p>No source specified.</p>"#),
|
||||
};
|
||||
|
||||
let (files_html, files_count) = if let Some(stage_files) = stage_files {
|
||||
let count = stage_files
|
||||
.split('\n')
|
||||
.filter(|p| !p.ends_with('/') && !p.is_empty())
|
||||
.count();
|
||||
(format!("<pre>{stage_files}</pre>"), format!("{}", count))
|
||||
} else {
|
||||
(
|
||||
String::from(r#"<p>No package files defined.</p>"#),
|
||||
String::from("?"),
|
||||
)
|
||||
};
|
||||
|
||||
{
|
||||
let host = get_hostname(&config.this_repo);
|
||||
let tree_link = get_tree_url(
|
||||
&config.this_repo,
|
||||
host,
|
||||
&package.commit_identifier,
|
||||
Some(&format!("recipes/{category}/{name}/recipe.toml")),
|
||||
);
|
||||
let short_commit = &package.commit_identifier[0..7];
|
||||
source_html += &format!(
|
||||
r#"
|
||||
<table>
|
||||
<tr><th>Build script:</th><td><a href="{tree_link}" target="_blank">{short_commit}</a></td></tr>
|
||||
</table>
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
let (arch, os) = {
|
||||
let target_split: Vec<&str> = package.target.split('-').collect();
|
||||
(
|
||||
target_split
|
||||
.get(0)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("-".into()),
|
||||
target_split
|
||||
.get(2)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("-".into()),
|
||||
)
|
||||
};
|
||||
|
||||
let html = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{name} - Redox OS Package</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="pkg-header">
|
||||
<div class="container">
|
||||
<a href="index.html" class="back-link">← Back to packages</a>
|
||||
<h1>{name} <span class="version">{version}</span></h1>
|
||||
{desc_html}
|
||||
<p class="description">{description}</p>
|
||||
<div class="install-action">
|
||||
<span class="prompt">$</span>
|
||||
<code>pkg install {name}</code>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="pkg-content container">
|
||||
<div class="pkg-main">
|
||||
<section class="pkg-deps card">
|
||||
<h2>Dependencies</h2>
|
||||
{deps_html}
|
||||
</section>
|
||||
<section class="pkg-dependents card">
|
||||
<h2>Dependents</h2>
|
||||
{dependents_html}
|
||||
</section>
|
||||
<section class="pkg-recipe card">
|
||||
<h2>Package Files</h2>
|
||||
{files_html}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="pkg-meta card">
|
||||
<table>
|
||||
<tr><th></th><td><a href="{repo_url}/{target}/{name}.pkgar" target="_blank">Download</a></td></tr>
|
||||
</table>
|
||||
<h2>Package Info</h2>
|
||||
<table>
|
||||
<tr><th>OS</th><td>{os}</td></tr>
|
||||
<tr><th>Architecture</th><td>{arch}</td></tr>
|
||||
<tr><th>Category</th><td><a href="index.html#cat-{category}">{category}</a></td></tr>
|
||||
<tr><th>Network Size</th><td>{network_size}</td></tr>
|
||||
<tr><th>Storage Size</th><td>{storage_size}</td></tr>
|
||||
<tr><th>File count</th><td>{files_count}</td></tr>
|
||||
<tr><th>Published</th><td title="{published}">{published_short}</td></tr>
|
||||
<tr><th>Hash</th><td><code class="hash meta-box">{blake3}</code></td></tr>
|
||||
</table>
|
||||
<h2>Package Source</h2>
|
||||
{source_html}
|
||||
<div style="height:100px"></div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>"#,
|
||||
network_size = format_size(package.network_size),
|
||||
storage_size = format_size(package.storage_size),
|
||||
published_short = &package.time_identifier[0..10],
|
||||
published = package.time_identifier,
|
||||
blake3 = package.blake3,
|
||||
);
|
||||
|
||||
fs::write(html_path, html).expect("Failed to write package HTML file");
|
||||
}
|
||||
|
||||
pub fn generate_html_index(
|
||||
grouped_packages: BTreeMap<String, Vec<&(Package, CookRecipe)>>,
|
||||
index_path: &Path,
|
||||
config: &crate::web::CliWebConfig,
|
||||
) {
|
||||
let mut categories_html = Vec::new();
|
||||
|
||||
for (category, pkgs) in grouped_packages {
|
||||
let cards_html: Vec<String> = pkgs
|
||||
.iter()
|
||||
.map(|(pkg, _recipe)| {
|
||||
let name = &pkg.name;
|
||||
format!(
|
||||
r#"
|
||||
<div class="package-card">
|
||||
<h3 class="pkg-name"><a href="{name}.html">{name}</a></h3>
|
||||
<div class="pkg-stats">
|
||||
<span class="pkg-version">{version}</span>
|
||||
<span class="pkg-size">{size}</span>
|
||||
</div>
|
||||
</div>"#,
|
||||
name = name,
|
||||
version = pkg.version,
|
||||
size = format_size(pkg.network_size)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let category_block = format!(
|
||||
r#"
|
||||
<section class="category-section">
|
||||
<h2 class="category-title" id="cat-{category}">{category}</h2>
|
||||
<div class="package-grid">
|
||||
{cards}
|
||||
</div>
|
||||
</section>"#,
|
||||
category = category,
|
||||
cards = cards_html.join("\n")
|
||||
);
|
||||
|
||||
categories_html.push(category_block);
|
||||
}
|
||||
|
||||
let html = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Redox Package Repository</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="index-header">
|
||||
<h1>Redox OS Package Repository</h1>
|
||||
<p class="description">Repository for <code>{target}</code></p>
|
||||
</header>
|
||||
|
||||
<main class="index-content container">
|
||||
{category_sections}
|
||||
|
||||
<footer>
|
||||
<p>Generated at <code>{commit_time}</code> with build tree <a href="{commit_tree}" target="_blank">{commit_hash}</a></p>
|
||||
<div style="height:100px"></div>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>"#,
|
||||
target = redoxer::target(),
|
||||
category_sections = categories_html.join("\n\n"),
|
||||
commit_time = &ident::get_ident().time,
|
||||
commit_hash = &ident::get_ident().commit[0..7],
|
||||
commit_tree = get_tree_url(
|
||||
&config.this_repo,
|
||||
get_hostname(&config.this_repo),
|
||||
&ident::get_ident().commit,
|
||||
None
|
||||
),
|
||||
);
|
||||
|
||||
println!("Generating web content to {}", index_path.display());
|
||||
fs::write(index_path, html).expect("Failed to write index HTML file");
|
||||
}
|
||||
|
||||
fn get_hostname(url: &str) -> &str {
|
||||
url.split("://")
|
||||
.nth(1)
|
||||
.unwrap_or(url)
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or(url)
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or(url)
|
||||
}
|
||||
|
||||
pub fn get_tree_url(git_url: &str, host: &str, commit: &str, folder: Option<&str>) -> String {
|
||||
let mut base_url = git_url.trim_end_matches(".git").to_string();
|
||||
|
||||
if let Some(ssh_path) = base_url.strip_prefix("git@") {
|
||||
// "git@github.com:user/repo" -> "https://github.com/user/repo"
|
||||
base_url = format!("https://{}", ssh_path.replace(':', "/"));
|
||||
} else if base_url.starts_with("git://") {
|
||||
// "git://github.com/user/repo" -> "https://github.com/user/repo"
|
||||
base_url = base_url.replacen("git://", "https://", 1);
|
||||
}
|
||||
|
||||
let base_url = if host == "github.com" {
|
||||
format!("{}/tree/{}", base_url, commit)
|
||||
} else if host.contains("gitlab") {
|
||||
format!("{}/-/tree/{}", base_url, commit)
|
||||
} else {
|
||||
return format!("{}?commit={}", base_url, commit);
|
||||
};
|
||||
|
||||
match folder {
|
||||
Some(f) => format!("{base_url}/{f}"),
|
||||
None => base_url,
|
||||
}
|
||||
}
|
||||
246
src/web/style.css
Normal file
246
src/web/style.css
Normal file
@ -0,0 +1,246 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #f9f9fb;
|
||||
color: #24292e;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 1.5rem;
|
||||
color: #24292e;
|
||||
border-bottom: 2px solid #e1e4e8;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.package-grid {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: -10px;
|
||||
}
|
||||
|
||||
.package-card {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 10px;
|
||||
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
vertical-align: top;
|
||||
|
||||
display: flex;
|
||||
flex: 0 1 280px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.package-card .pkg-name {
|
||||
margin-bottom: 15px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.package-card .pkg-name a {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.package-card .pkg-name a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.package-card .pkg-stats {
|
||||
display: block;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #6a737d;
|
||||
font-size: 0.9rem;
|
||||
border-top: 1px solid #e1e4e8;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.package-card .pkg-version {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
background-color: #f3f4f6;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.package-card .pkg-size {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #24292e;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #000000;
|
||||
border-bottom: 1px solid #24292e;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-weight: 600;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.2em 0.4em;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pkg-header, .index-header {
|
||||
background-color: #ffffff;
|
||||
border-bottom: 1px solid #d1d5da;
|
||||
padding: 40px 0;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pkg-header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pkg-header .version {
|
||||
color: #6a737d;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pkg-header .description {
|
||||
font-size: 1.2rem;
|
||||
color: #586069;
|
||||
max-width: 600px;
|
||||
margin: 0 auto 1.5rem auto;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #6a737d;
|
||||
border: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: #24292e;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.install-action {
|
||||
display: inline-block;
|
||||
background-color: #f3f4f6;
|
||||
border: 1px solid #d1d5da;
|
||||
border-radius: 6px;
|
||||
padding: 12px 20px;
|
||||
font-family: ui-monospace, SFMono-Regular, monospace;
|
||||
font-size: 1.1rem;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.install-action .prompt {
|
||||
color: #6a737d;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.install-action code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
font-size: 1.1rem;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.pkg-content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pkg-main, .pkg-meta {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.pkg-main {
|
||||
width: 60%;
|
||||
}
|
||||
.pkg-meta {
|
||||
width: 35%;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-box {
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
max-width: 150px;
|
||||
user-select: all;
|
||||
padding: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 10px 0;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
th {
|
||||
color: #6a737d;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pkg-meta table th {
|
||||
width: 40%;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.pkg-deps ul, .pkg-dependents ul {
|
||||
list-style-type: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pkg-deps li, .pkg-dependents li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
width: 50%;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user