Merged in make-build-more-robust (pull request #42)

Improve build robustness
This commit is contained in:
Dorian 2020-03-12 19:05:47 +00:00
commit d097eeecbb
7 changed files with 313 additions and 250 deletions

View File

@ -3,9 +3,10 @@
releases_release_uri = "https://github.com/dorianpula/rookeries/releases/%s"
releases_issue_uri = "https://bitbucket.org/dorianpula/rookeries/issues/%s"
* :release:`0.18.0 <2019-02-??>`
* :release:`0.18.0 <2019-03-??>`
* :feature:`38` Add support for TOML-based frontmatter for Markdown source files.
* :feature:`38` Use new and improved site format.
* :feature:`42` Add ability to query page and build information in template.
* :feature:`39` Add support for git initialization and deployment.
* :support:`37` General code clean-up as part of using Rust 2018.
* :support:`37` Add support for migrating old versions of Rookeries site to a newer version.

View File

@ -1,22 +1,60 @@
use crate::migration::PreStable18Site;
use crate::{
cli::{caution_message, header_message, success_message},
cli::{caution_message, details_message, header_message, success_message},
errors::RookeriesError,
files::{build_path, copy_directory, copy_file, create_directory, FileType},
migration::PreStable18Site,
Page, PageHeader, Project, Rookeries, Site,
};
use chrono::prelude::*;
use colored::Colorize;
use serde_json::json;
use serde::ser::{Serialize, SerializeStruct, Serializer};
use std::{
ffi::OsString,
fs::{read_dir, read_to_string, remove_dir_all, write},
path::Path,
path::{Path, PathBuf},
};
use uuid::Uuid;
const ROOKERIES_UNKNOWN_PLACEHOLDER: &str = "???";
#[derive(Clone, Debug)]
/// Information about the current build.
pub struct BuildInfo {
git_revision_id: String,
build_time: DateTime<Local>,
content_build_id: Uuid,
}
impl BuildInfo {
pub fn new() -> Self {
Self {
git_revision_id: Rookeries::git_revision(),
build_time: Local::now(),
content_build_id: Uuid::new_v4(),
}
}
pub fn formatted_build_time(&self) -> String {
self.build_time.to_rfc3339_opts(SecondsFormat::Secs, true)
}
}
impl Serialize for BuildInfo {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("BuildInfo", 5)?;
state.serialize_field("gitRevision", &self.git_revision_id)?;
state.serialize_field("contentBuildId", &self.content_build_id)?;
state.serialize_field("buildTime", &self.formatted_build_time())?;
state.serialize_field("app", &Rookeries::name())?;
state.serialize_field("version", &Rookeries::current_version())?;
state.end()
}
}
pub fn build_site(project: &Project, activate_dev_mode: bool) -> Result<(), RookeriesError> {
header_message("Building the site...");
@ -45,229 +83,71 @@ pub fn build_site(project: &Project, activate_dev_mode: bool) -> Result<(), Rook
&project_site.title.green(),
));
// Build a list of the pages to build out.
header_message("Searching for pages...");
let mut site_source_pages: Vec<OsString> = Vec::new();
for entry in read_dir(project.root_dir())? {
// TODO: Make the search recursive through directories.
let path = entry?.path();
if path.is_file()
&& path.extension() != None
&& path.extension().unwrap_or(&path.as_os_str()) == "md"
{
site_source_pages.push(path.into_os_string());
}
}
site_source_pages.sort();
for source in &site_source_pages {
let path = Path::new(source);
success_message(&format!("Found {}", path.display()));
}
let site_source_pages = find_source_pages(project)?;
// TODO: Add in support for figuring out the index page or allowing one to be set.
header_message("Compiling page information...");
header_message("Extracting page information...");
// TODO: Add warning about a missing index page.
// TODO: Add support for aliases (e.g. such as about being an index page)
let site_source_pages: Vec<Page> = site_source_pages
.iter()
.map(|source_page| {
// TODO: Extract separate per page build.
let source_path = Path::new(source_page);
let slug = source_path
.file_stem()
.unwrap_or_else(|| source_path.as_os_str())
.to_str()
.unwrap_or(ROOKERIES_UNKNOWN_PLACEHOLDER)
.to_string();
let raw_content = read_to_string(source_path)
.map_err(|err| {
let source_page_err = source_page
.clone()
.into_string()
.unwrap_or_else(|_| ROOKERIES_UNKNOWN_PLACEHOLDER.to_string());
RookeriesError::ReadFileFailure(err, source_page_err, FileType::SourceMarkdown)
})
.unwrap_or_else(|_| ROOKERIES_UNKNOWN_PLACEHOLDER.to_string());
// Extract the page header from the content.
let parsed_content: Vec<&str> = raw_content.split("+++").collect();
let (header, content) = if parsed_content.len() > 1 {
(parsed_content[1], parsed_content[2])
} else {
("", parsed_content[parsed_content.len() - 1])
};
// TODO: Remember to check valid titles post-migration.
let older_post_title = match &older_site {
None => None,
Some(older_site_def) => Some(older_site_def.get_title_page(&slug)),
};
let header = toml::from_str(&header).unwrap_or_else(|_| match older_post_title {
None => PageHeader::new(slug.clone()),
Some(title) => PageHeader { title },
});
success_message(&format!("Compiling \"{}\" page", &slug.green()));
Page {
title: header.title,
slug,
content: content.to_string(),
created_at: None,
}
})
.map(|source_page| build_page_from_path(&older_site, source_page))
.collect();
// Create a build directory for the resulting files.
header_message("Preparing build directory...");
let build_directory = project.build_dir();
if build_directory.exists() {
caution_message("Recreating build directory...");
remove_dir_all(&build_directory)?;
create_directory(&build_directory, FileType::BuildRoot)?;
} else {
create_directory(&build_directory, FileType::BuildRoot)?;
}
// TODO: Clean this up.
header_message("Preparing for the build...");
let build_directory = create_build_directory(project)?;
let build_info = BuildInfo::new();
success_message("Build information compiled.");
header_message("Initialize page rendering system...");
let mut render_engine = tera::Tera::default();
// TODO: Add templates programmatically.
let template_path = project.template_dir().join("base_index.html");
let another_template_path = project.template_dir().join("sample.html");
render_engine
.add_template_files(vec![
(&template_path, Some("index")),
(&another_template_path, Some("sample")),
])
.map_err(RookeriesError::TemplateSetupFailure)?;
let render_engine = prepare_render_engine(project)?;
let mut ctx = prepare_render_context(activate_dev_mode, older_site, &project_site, &build_info);
success_message("Render system initialized.");
let build_timestamp: DateTime<Local> = Local::now();
let build_timestamp = build_timestamp.to_rfc3339_opts(SecondsFormat::Secs, true);
let status = json!({
"version": Rookeries::current_version(),
"app": env!("CARGO_PKG_NAME").to_string(),
"gitRevision": Rookeries::git_revision(),
"contentBuildId": Uuid::new_v4(),
"buildTime": build_timestamp,
});
header_message("Creating the index page...");
let mut ctx = tera::Context::new();
// TODO: Figure out better support for Pre 0.18.0 version sites.
if let Some(older_site) = older_site {
let deprecation_message = format!(
"{} Using deprecated values: {} and {} in the templates.",
"WARNING!".yellow(),
"site.name".yellow(),
"plugins".yellow(),
);
caution_message(&deprecation_message);
let deprecation_message = format!(
"{} Update these values to: {} and {} respectively in the templates.",
"RECOMMENDATION:".yellow(),
"site.title".yellow(),
"site.plugins".yellow(),
);
caution_message(&deprecation_message);
ctx.insert("site", &older_site);
let plugins: Vec<String> = project_site
.plugins
.iter()
.map(|plugin| plugin.name.clone())
.collect();
ctx.insert("plugins", &plugins)
} else {
ctx.insert("site", &project_site);
}
header_message("Rendering the site...");
let api_path = project.build_api_dir();
create_directory(&api_path, FileType::APIRoot)?;
build_artifact(&build_info, &api_path)?;
build_site_api_repr(&project_site, &api_path)?;
let api_pages_path = &api_path.join("pages");
create_directory(&api_pages_path, FileType::PageAPI)?;
// TODO: Add ensure index page added into the site source pages.
// TODO: Ensure getting the right page as documented in the landingPage
ctx.insert("current_page", "index");
ctx.insert("rookeries", &status);
if activate_dev_mode {
ctx.insert("dev_mode", "active");
}
// TODO: Add index pages into the site source pages.
let render_page_str = render_engine
.render("sample", &ctx)
.map_err(|err| RookeriesError::RenderTemplateFailure(err, "index".to_string()))?;
write(build_directory.join("index.html"), render_page_str).map_err(|err| {
RookeriesError::WriteFileFailure(err, "index.html".to_string(), FileType::RenderedHtml)
})?;
success_message(&format!("Created the {} HTML page.", "index".green()));
header_message("Creating the pages...");
header_message("Rendering the pages...");
for page in &site_source_pages {
create_directory(&build_directory.join(&page.slug), FileType::Page)?;
ctx.insert("current_page", &page.slug);
let render_page_str = render_engine.render("index", &ctx).map_err(|err| {
RookeriesError::RenderTemplateFailure(err, format!("{}/index", &page.slug))
let page_directory = if &page.slug == "index" {
project.build_dir()
} else {
build_directory.join(&page.slug)
};
create_directory(&page_directory, FileType::Page)?;
ctx.insert("page", &page);
let render_page_str = render_engine.render("sample", &ctx).map_err(|err| {
let template_render_path = if &page.slug == "index" {
String::from("index")
} else {
format!("{}/index", &page.slug)
};
RookeriesError::RenderTemplateFailure(err, template_render_path)
})?;
write(
build_directory.join(&page.slug).join("index.html"),
render_page_str,
)
.map_err(|err| {
write(page_directory.join("index.html"), render_page_str).map_err(|err| {
RookeriesError::WriteFileFailure(
err,
format!("{}/index.html", &page.slug),
FileType::RenderedHtml,
)
})?;
success_message(&format!("Created the {} HTML page.", &page.slug.green()));
}
details_message(&format!(
"Rendered the {} {}.",
&page.slug.yellow(),
FileType::RenderedHtml.to_string().yellow()
));
// Create the API representations.
header_message("Creating the API JSON files...");
let api_path = &build_directory.join("api");
create_directory(&api_path, FileType::APIRoot)?;
// Create a build artifact.
let status = serde_json::to_string_pretty(&status).map_err(|err| {
RookeriesError::JsonSerializationError(
err,
"_build.json".to_string(),
FileType::BuildArtifactJson,
)
})?;
write(api_path.join("_build.json"), status).map_err(|err| {
RookeriesError::WriteFileFailure(
err,
"api/_build.json".to_string(),
FileType::BuildArtifactJson,
)
})?;
success_message(&format!(
"Created the {}.",
FileType::BuildArtifactJson.to_string().green()
));
let site_json = serde_json::to_string_pretty(&project_site).map_err(|err| {
RookeriesError::JsonSerializationError(
err,
"site.json".to_string(),
FileType::RenderedSiteJson,
)
})?;
write(&api_path.join("site.json"), site_json).map_err(|err| {
RookeriesError::WriteFileFailure(
err,
"api/site.json".to_string(),
FileType::RenderedSiteJson,
)
})?;
success_message(&format!(
"Created the {}.",
FileType::RenderedSiteJson.to_string().green()
));
// Create all the page API reps.
let api_pages_path = &api_path.join("pages");
create_directory(&api_pages_path, FileType::PageAPI)?;
for page in site_source_pages {
// Create the page API reps.
let page_json_filename = format!("{}.json", &page.slug);
let page_json = serde_json::to_string_pretty(&page).map_err(|err| {
RookeriesError::JsonSerializationError(
@ -276,15 +156,15 @@ pub fn build_site(project: &Project, activate_dev_mode: bool) -> Result<(), Rook
FileType::RenderedPageJson,
)
})?;
write(api_pages_path.join(&page_json_filename), page_json).map_err(|err| {
RookeriesError::WriteFileFailure(err, page_json_filename, FileType::RenderedPageJson)
})?;
success_message(&format!(
"Created the {} for the {} page.",
FileType::RenderedPageJson.to_string().green(),
&page.slug.green(),
details_message(&format!(
"Rendered the {} {}.",
&page.slug.yellow(),
FileType::RenderedPageJson.to_string().yellow()
));
success_message(&format!("Rendered the {} page.", &page.slug.green(),));
}
// Bring in the static file directory.
@ -328,3 +208,188 @@ pub fn build_site(project: &Project, activate_dev_mode: bool) -> Result<(), Rook
Ok(())
}
fn build_site_api_repr(project_site: &Site, api_path: &PathBuf) -> Result<(), RookeriesError> {
let site_json = serde_json::to_string_pretty(&project_site).map_err(|err| {
RookeriesError::JsonSerializationError(
err,
"site.json".to_string(),
FileType::RenderedSiteJson,
)
})?;
write(&api_path.join("site.json"), site_json).map_err(|err| {
RookeriesError::WriteFileFailure(
err,
"api/site.json".to_string(),
FileType::RenderedSiteJson,
)
})?;
success_message(&format!(
"Rendered the {}.",
FileType::RenderedSiteJson.to_string().green()
));
Ok(())
}
fn build_artifact(build_info: &BuildInfo, api_path: &PathBuf) -> Result<(), RookeriesError> {
// Create a build artifact.
let status = serde_json::to_string_pretty(&build_info).map_err(|err| {
RookeriesError::JsonSerializationError(
err,
"_build.json".to_string(),
FileType::BuildArtifactJson,
)
})?;
write(api_path.join("_build.json"), status).map_err(|err| {
RookeriesError::WriteFileFailure(
err,
"api/_build.json".to_string(),
FileType::BuildArtifactJson,
)
})?;
success_message(&format!(
"Rendered the {}.",
FileType::BuildArtifactJson.to_string().green()
));
Ok(())
}
fn prepare_render_context(
activate_dev_mode: bool,
older_site: Option<PreStable18Site>,
project_site: &Site,
build_info: &BuildInfo,
) -> tera::Context {
let mut ctx = tera::Context::new();
// TODO: Figure out better support for Pre 0.18.0 version sites.
if let Some(older_site) = older_site {
let deprecation_message = format!(
"{} Using deprecated values: {} and {} in the templates.",
"WARNING!".yellow(),
"site.name".yellow(),
"plugins".yellow(),
);
caution_message(&deprecation_message);
let deprecation_message = format!(
"{} Update these values to: {} and {} respectively in the templates.",
"RECOMMENDATION:".yellow(),
"site.title".yellow(),
"site.plugins".yellow(),
);
caution_message(&deprecation_message);
ctx.insert("site", &older_site);
let plugins: Vec<String> = project_site
.plugins
.iter()
.map(|plugin| plugin.name.clone())
.collect();
ctx.insert("plugins", &plugins)
} else {
ctx.insert("site", &project_site);
}
ctx.insert("rookeries", &build_info);
if activate_dev_mode {
ctx.insert("dev_mode", "active");
}
ctx
}
fn prepare_render_engine(project: &Project) -> Result<tera::Tera, RookeriesError> {
let mut render_engine = tera::Tera::default();
// TODO: Add templates programmatically.
let template_path = project.template_dir().join("base_index.html");
let another_template_path = project.template_dir().join("sample.html");
render_engine
.add_template_files(vec![
(&template_path, Some("index")),
(&another_template_path, Some("sample")),
])
.map_err(RookeriesError::TemplateSetupFailure)?;
Ok(render_engine)
}
fn create_build_directory(project: &Project) -> Result<PathBuf, RookeriesError> {
// Create a build directory for the resulting files.
let build_directory = project.build_dir();
if build_directory.exists() {
caution_message("Recreating build directory...");
remove_dir_all(&build_directory)?;
create_directory(&build_directory, FileType::BuildRoot)?;
} else {
create_directory(&build_directory, FileType::BuildRoot)?;
}
success_message("Build directory ready.");
Ok(build_directory)
}
fn build_page_from_path(older_site: &Option<PreStable18Site>, source_page: &OsString) -> Page {
// TODO: Extract separate per page build.
let source_path = Path::new(source_page);
let slug = source_path
.file_stem()
.unwrap_or_else(|| source_path.as_os_str())
.to_str()
.unwrap_or(ROOKERIES_UNKNOWN_PLACEHOLDER)
.to_string();
let raw_content = read_to_string(source_path)
.map_err(|err| {
let source_page_err = source_page
.clone()
.into_string()
.unwrap_or_else(|_| ROOKERIES_UNKNOWN_PLACEHOLDER.to_string());
RookeriesError::ReadFileFailure(err, source_page_err, FileType::SourceMarkdown)
})
.unwrap_or_else(|_| ROOKERIES_UNKNOWN_PLACEHOLDER.to_string());
// Extract the page header from the content.
let parsed_content: Vec<&str> = raw_content.split("+++").collect();
let (header, content) = if parsed_content.len() > 1 {
(parsed_content[1], parsed_content[2])
} else {
("", parsed_content[parsed_content.len() - 1])
};
// TODO: Remember to check valid titles post-migration.
let older_post_title = match &older_site {
None => None,
Some(older_site_def) => Some(older_site_def.get_title_page(&slug)),
};
let header = toml::from_str(&header).unwrap_or_else(|_| match older_post_title {
None => PageHeader::new(slug.clone()),
Some(title) => PageHeader { title },
});
success_message(&format!("Extracted \"{}\" page", &slug.green()));
Page {
title: header.title,
slug,
content: content.to_string(),
created_at: None,
}
}
fn find_source_pages(project: &Project) -> Result<Vec<OsString>, RookeriesError> {
// Build a list of the pages to build out.
header_message("Searching for pages...");
let mut site_source_pages: Vec<OsString> = Vec::new();
for entry in read_dir(project.root_dir())? {
// TODO: Make the search recursive through directories.
let path = entry?.path();
if path.is_file()
&& path.extension() != None
&& path.extension().unwrap_or(&path.as_os_str()) == "md"
{
site_source_pages.push(path.into_os_string());
}
}
site_source_pages.sort();
for source in &site_source_pages {
let path = Path::new(source);
success_message(&format!("Found {}", path.display()));
}
Ok(site_source_pages)
}

View File

@ -19,7 +19,6 @@ pub fn initialize_site(project: Project) -> Result<(), RookeriesError> {
header_message("Initializing a new site...");
// TODO: Add support for interactive wizard to populate values.
// TODO: Add support for .gitignore + git initialization. (Maybe post initialization plugins).
let project_initial_details: Site = Default::default();
prepare_project_directory(&project)?;
initialize_git(&project)?;

View File

@ -1,5 +1,5 @@
use crate::{
cli::{caution_message, details_message, success_message},
cli::{caution_message, details_message},
errors::RookeriesError,
};
use colored::Colorize;
@ -182,12 +182,9 @@ pub fn create_directory(
) -> Result<(), RookeriesError> {
fs::create_dir_all(target_directory)
.map_err(|err| RookeriesError::CreateDirectoryFailure(err, directory_type))?;
success_message(&format!(
"Created the {} directory.",
directory_type.to_string().green(),
));
details_message(&format!(
"Directory created at: {}",
"Created {} directory at: {}",
directory_type.to_string().yellow(),
target_directory.display().to_string().yellow(),
));
Ok(())

View File

@ -38,6 +38,10 @@ impl Project {
pub fn build_dir(&self) -> PathBuf {
self.root_dir().join("build")
}
/// Gets a path to the project's build API directory. This contains the built site.
pub fn build_api_dir(&self) -> PathBuf {
self.build_dir().join("api")
}
/// Gets a path to the project's directory of static assets.
pub fn static_assets_dir(&self) -> PathBuf {
self.root_dir().join("static")
@ -148,6 +152,11 @@ impl Default for Site {
pub struct Rookeries {}
impl Rookeries {
/// Gets the name of the app.
pub fn name() -> String {
env!("CARGO_PKG_NAME").to_string()
}
/// Gets the current version of the app.
pub fn current_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
@ -161,7 +170,8 @@ impl Rookeries {
/// Gets a pretty variant of the app version for the `--version` flag.
pub fn print_app_version() -> String {
format!(
"rookeries v{version} © 2013-2020 {authors}",
"{name} v{version} © 2013-2020 {authors}",
name = Rookeries::name(),
version = Rookeries::current_version(),
authors = env!("CARGO_PKG_AUTHORS"),
)

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/html">
<head>
<title>{{ site.title }} :: {{ current_page }}</title>
<title>{{ page.title }} - {{ site.title }}</title>
<link rel="icon" type="image/icon" href="/static/favicon.ico" />
<link rel="stylesheet"
type="text/css"
@ -14,7 +14,7 @@
<div class="rookeries-layout">
<dark-mode-switch theme="dark"></dark-mode-switch>
<header>
<h1>Sample Header</h1>
<h1>{{ page.title }}</h1>
</header>
<nav>

View File

@ -61,7 +61,6 @@ fn test_init_creates_site() {
let expected_output = format!(
"
Initializing a new site...
Created the project root directory.
Created a new project in {test_directory}
Initialized git support for project.
Created a site.toml!
@ -78,7 +77,6 @@ fn test_init_creates_site() {
Copied over sample Markdown: license.md
Adding plugins to site...
Created the plugin directory.
Copied over the dark-mode-switch plugin!
Site initialized. You can now start building your new site. Happy hacking!
@ -176,7 +174,6 @@ fn assert_valid_directory(path: &Path) {
assert!(path.is_dir());
}
// TODO: Add a test with plugins and extras.
#[test]
fn test_build_builds_site() {
let mut rng = thread_rng();
@ -197,35 +194,24 @@ fn test_build_builds_site() {
Found {test_directory}/index.md
Found {test_directory}/license.md
Compiling page information...
Compiling \"demo\" page
Compiling \"index\" page
Compiling \"license\" page
Extracting page information...
Extracted \"demo\" page
Extracted \"index\" page
Extracted \"license\" page
Preparing build directory...
Created the build directory.
Preparing for the build...
Build directory ready.
Build information compiled.
Render system initialized.
Initialize page rendering system...
Rendering the site...
Rendered the build artifact JSON.
Rendered the site JSON.
Creating the index page...
Created the index HTML page.
Creating the pages...
Created the page directory.
Created the demo HTML page.
Created the page directory.
Created the index HTML page.
Created the page directory.
Created the license HTML page.
Creating the API JSON files...
Created the API root directory.
Created the build artifact JSON.
Created the site JSON.
Created the API page root directory.
Created the page JSON for the demo page.
Created the page JSON for the index page.
Created the page JSON for the license page.
Rendering the pages...
Rendered the demo page.
Rendered the index page.
Rendered the license page.
Copying static assets to built site...
Copied over static asset directory!
@ -286,7 +272,12 @@ fn test_build_builds_site() {
// Check if pages are present and rendered as expected.
for test_page_name in expected_pages() {
let actual_page_path = build_dir.join(format!("{}/index.html", &test_page_name));
let actual_page_path = if test_page_name == "index" {
String::from("index.html")
} else {
format!("{}/index.html", &test_page_name)
};
let actual_page_path = build_dir.join(actual_page_path);
assert_file_readable_into_string(&actual_page_path);
}
}