diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..0e9cb90 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[build] +#rustflags = ["-C", "link-arg=-fuse-ld=lld"] +target-dir = "/ws/target" \ No newline at end of file diff --git a/.gitignore b/.gitignore index bc805cd..6229c73 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ Cargo.lock # Prototype directory sample_data/**/build/prototype/i386 +.vagrant \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cd6550b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "oi-userland"] + path = oi-userland + url = git@github.com:Toasterson/oi-userland.git diff --git a/.vscode/dryrun.log b/.vscode/dryrun.log index 528b4a6..09fa7ab 100644 --- a/.vscode/dryrun.log +++ b/.vscode/dryrun.log @@ -1,9 +1,8 @@ make --dry-run --always-make --keep-going --print-directory -make: Entering directory '/home/toast/workspace/illumos/aurora/ips' +make: Entering directory '/ws/toast/illumos/rust/ips' rm -rf target artifacts - cargo build --release mkdir -p artifacts cp target/release/pkg6dev artifacts/ -make: Leaving directory '/home/toast/workspace/illumos/aurora/ips' +make: Leaving directory '/ws/toast/illumos/rust/ips' diff --git a/.vscode/targets.log b/.vscode/targets.log index 8dfcc28..57c4c25 100644 --- a/.vscode/targets.log +++ b/.vscode/targets.log @@ -5,30 +5,19 @@ make all --print-data-base --no-builtin-variables --no-builtin-rules --question # License GPLv3+: GNU GPL version 3 or later # This is free software: you are free to change and redistribute it. # There is NO WARRANTY, to the extent permitted by law. - -# Make data base, printed on Thu Sep 1 15:11:07 2022 +# Make data base, printed on Fri Sep 2 20:28:38 2022 # Variables -# environment -GO111MODULE = on # environment LC_ALL = C # environment -LC_NAME = de_CH.UTF-8 -# environment -LC_NUMERIC = de_CH.UTF-8 -# environment -VSCODE_CWD = /home/toast -# environment -LC_ADDRESS = de_CH.UTF-8 +VSCODE_CWD = /ws/toast # default MAKE_COMMAND := make # environment VSCODE_HANDLES_SIGPIPE = true -# environment -GOPATH = /home/toast/workspace/go # automatic @D = $(patsubst %/,%,$(dir $@)) # environment @@ -36,13 +25,11 @@ VSCODE_HANDLES_UNCAUGHT_ERRORS = true # default .VARIABLES := # environment -PWD = /home/toast/workspace/illumos/aurora/ips +PWD = /ws/toast/illumos/rust/ips # automatic %D = $(patsubst %/,%,$(dir $%)) # environment -LSCOLORS = Gxfxcxdxbxegedabagacad -# environment -OLDPWD = /home/toast +MAIL = /var/spool/mail/toast # automatic ^D = $(patsubst %/,%,$(dir $^)) # automatic @@ -52,19 +39,19 @@ LANG = C # default .LOADED := # default -.INCLUDE_DIRS = /usr/local/include /usr/include /usr/include +.INCLUDE_DIRS = /usr/include /usr/local/include /usr/include # makefile MAKEFLAGS = pqrR # makefile -CURDIR := /home/toast/workspace/illumos/aurora/ips +CURDIR := /ws/toast/illumos/rust/ips # environment -APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL = true +APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL = 1 # automatic *D = $(patsubst %/,%,$(dir $*)) # environment MFLAGS = -pqrR # environment -SSH_AUTH_SOCK = /run/user/1000/vscode-ssh-auth-sock-316701701 +SSH_AUTH_SOCK = /run/user/1000/vscode-ssh-auth-sock-643226622 # default .SHELLFLAGS := -c # automatic @@ -74,8 +61,6 @@ MAKEFILE_LIST := Makefile # automatic @F = $(notdir $@) # environment -ZSH = /home/toast/.oh-my-zsh -# environment XDG_SESSION_TYPE = tty # automatic ?D = $(patsubst %/,%,$(dir $?)) @@ -87,92 +72,62 @@ DBUS_SESSION_BUS_ADDRESS = unix:path=/run/user/1000/bus = StdResult; + +#[derive(Debug, Error)] +pub enum ActionError { + #[error("payload error: {0}")] + PayloadError(#[from] PayloadError), + + #[error("file action error: {0}")] + FileError(#[from] FileError), + + #[error("value {0} is not a boolean")] + NotBooleanValue(String), + + #[error("io error: {0}")] + IOError(#[from] std::io::Error), + + #[error("parser error: {0}")] + ParserError(#[from] pest::error::Error) +} pub trait FacetedAction { // Add a facet to the action if the facet is already present the function returns false. @@ -615,6 +636,6 @@ fn string_to_bool(orig: &str) -> Result { "false" => Ok(false), "t" => Ok(true), "f" => Ok(false), - _ => Err(anyhow!("not a boolean like value")) + _ => Err(ActionError::NotBooleanValue(orig.clone().to_owned())) } } diff --git a/libips/src/digest/mod.rs b/libips/src/digest/mod.rs index 04f6525..c300e72 100644 --- a/libips/src/digest/mod.rs +++ b/libips/src/digest/mod.rs @@ -4,12 +4,14 @@ // obtain one at https://mozilla.org/MPL/2.0/. use thiserror::Error; -use anyhow::Result; +use std::result::Result as StdResult; use std::str::FromStr; use sha2::{Digest as Sha2Digest}; #[allow(unused_imports)] use sha3::{Digest as Sha3Digest}; +type Result = StdResult; + #[allow(dead_code)] static DEFAULT_ALGORITHM: DigestAlgorithm = DigestAlgorithm::SHA512; @@ -52,7 +54,7 @@ pub struct Digest { impl FromStr for Digest { type Err = DigestError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> StdResult { let str = String::from(s); if !s.contains(":") { return Ok(Digest{ diff --git a/libips/src/lib.rs b/libips/src/lib.rs index 328cd90..cefd505 100644 --- a/libips/src/lib.rs +++ b/libips/src/lib.rs @@ -7,10 +7,6 @@ pub mod actions; pub mod digest; pub mod payload; -extern crate pest; -#[macro_use] extern crate pest_derive; -extern crate maplit; - #[cfg(test)] mod tests { diff --git a/libips/src/payload/mod.rs b/libips/src/payload/mod.rs index ac9c5fe..b9fb3fe 100644 --- a/libips/src/payload/mod.rs +++ b/libips/src/payload/mod.rs @@ -3,10 +3,22 @@ // MPL was not distributed with this file, You can // obtain one at https://mozilla.org/MPL/2.0/. -use crate::digest::{Digest, DigestAlgorithm, DigestSource}; -use anyhow::Error; +use crate::digest::{Digest, DigestAlgorithm, DigestSource, DigestError}; use object::Object; use std::path::Path; +use thiserror::Error; +use std::result::Result as StdResult; +use std::io::Error as IOError; + +type Result = StdResult; + +#[derive(Debug, Error)] +pub enum PayloadError { + #[error("io error: {0}")] + IOError(#[from] IOError), + #[error("digest error: {0}")] + DigestError(#[from] DigestError), +} #[derive(Debug, PartialEq, Clone)] pub enum PayloadCompressionAlgorithm { @@ -56,7 +68,7 @@ impl Payload { self.architecture == PayloadArchitecture::NOARCH && self.bitness == PayloadBits::Independent } - pub fn compute_payload(path: &Path) -> Result { + pub fn compute_payload(path: &Path) -> Result { let f = std::fs::read(path)?; let (bitness, architecture) = match object::File::parse(f.as_slice()) { diff --git a/oi-userland b/oi-userland new file mode 160000 index 0000000..ca81f7d --- /dev/null +++ b/oi-userland @@ -0,0 +1 @@ +Subproject commit ca81f7dcc5ea1029defff41dbd9548940e8ba29a diff --git a/pkg6dev/src/main.rs b/pkg6dev/src/main.rs index 2279280..8dfb835 100644 --- a/pkg6dev/src/main.rs +++ b/pkg6dev/src/main.rs @@ -1,5 +1,5 @@ use clap::{Parser, Subcommand}; -use libips::actions::{File, Manifest}; +use libips::actions::{File, Manifest, ActionError}; use anyhow::{Result}; use std::collections::HashMap; @@ -93,7 +93,7 @@ fn diff_component( .as_ref() .join("manifests/sample-manifest.p5m"); - let manifests_res: Result> = manifest_files + let manifests_res: Result, ActionError> = manifest_files .iter() .map(|f| Manifest::parse_file(f.to_string())) .collect(); diff --git a/ports.spec b/ports.spec new file mode 100644 index 0000000..95a569c --- /dev/null +++ b/ports.spec @@ -0,0 +1,27 @@ +Name: ports +Version: 0.1.0 +Release: 0 +Summary: Portable Software compilation System +License: MPL-2.0 +VCS: https://github.com/OpenFlowLabs/ports.git + +%description +The ports Postable Software compilation system is a parser and interpreter for +build instructions made in the Specfile format. It is inspired but not exactly +equal to RPM. Mainly because it implements additional features such as git support +and add more convinience commands usefull for packaging. It is mainly based on knowledge +gained working with oi-userland and designed to make the proces of that system more +automated/better. + +%prep +# Cargo or git will be automatially prepared + +%build +cargo install --path . --target-dir %{source_dir}/cargo --root %{proto_dir}/usr --bins + +%files +/usr/bin/ports + +%changelog +* Sat Apr 03 2020 Till Wegmueller 0.1.0-0 +- Initial RPM release diff --git a/ports/Cargo.toml b/ports/Cargo.toml new file mode 100644 index 0000000..56b42c8 --- /dev/null +++ b/ports/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ports" +version = "0.1.0" +authors = ["Till Wegmueller "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.59" +clap = {version = "3.2.16", features = [ "derive", "env" ] } +specfile = {path = "../specfile"} +shellexpand = "2.1.2" +url = { version = "2.2.2", features = ["serde"]} +reqwest = { version = "0.11", features = ["blocking"] } +which = "4.3.0" +libips = {path = "../libips"} +thiserror = "*" \ No newline at end of file diff --git a/ports/src/main.rs b/ports/src/main.rs new file mode 100644 index 0000000..044e50f --- /dev/null +++ b/ports/src/main.rs @@ -0,0 +1,93 @@ +mod workspace; +mod sources; + +use anyhow::anyhow; +use clap::{Parser, Subcommand}; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use specfile::parse; +use specfile::macros; +use std::collections::HashMap; +use crate::workspace::Workspace; +use anyhow::Result; + +enum Verbose{ + Off, + Some, + On, + Debug +} + +#[derive(Debug, Parser)] +#[clap(version)] +struct Cli { + #[clap(subcommand)] + pub command: Commands, + + #[clap(short, long, env)] + pub config: Option, + + #[clap(short, parse(from_occurrences))] + pub verbose: i8, +} + +#[derive(Debug, Subcommand)] +enum Commands { + Package { + #[clap(short, long)] + target: PathBuf, + + #[clap(value_parser)] + specfile: PathBuf, + } +} + +fn main() -> Result<()> { + let cli: Cli = Cli::parse(); + + if let Some(c) = cli.config { + println!("Value for config: {}", c.display()); + } + + let _verbose = match cli.verbose { + 0 => Verbose::Off, + 1 => Verbose::Some, + 2 => Verbose::On, + 3 | _ => Verbose::Debug, + }; + + match cli.command { + Commands::Package { target, specfile } => { + run_package_command(specfile, target)?; + } + } + + Ok(()) +} + +fn run_package_command>(spec_file: P, _target: P) -> Result<()> { + let content_string = fs::read_to_string(spec_file)?; + let spec = parse(content_string)?; + let mut ws = Workspace::new("")?; + let downloaded = ws.get_sources(spec.sources)?; + ws.unpack_all_sources(downloaded)?; + + let mut macro_map= HashMap::::new(); + for ws_macro in ws.get_macros() { + macro_map.insert( + ws_macro.0, + ws_macro.1.to_str().ok_or(anyhow!("not string path {}", ws_macro.1.display()))?.to_owned() + ); + } + + let mp = macros::MacroParser { + macros: macro_map + }; + + let build_script = mp.parse(spec.build_script)?; + ws.build(build_script)?; + ws.package(spec.files)?; + + Ok(()) +} diff --git a/ports/src/sources.rs b/ports/src/sources.rs new file mode 100644 index 0000000..73c45e0 --- /dev/null +++ b/ports/src/sources.rs @@ -0,0 +1,43 @@ +use url::{Url, ParseError}; +use thiserror::Error; +use std::{result::Result as StdResult, path::{Path, PathBuf}, fmt::Display}; + +type Result = StdResult; + +#[derive(Debug, Error)] +pub enum SourceError { + #[error("can't create source from url: {0}")] + CantCreateSource(String), + #[error("can not parse source url: {0}")] + UrlParseError(#[from]ParseError) +} + +#[derive(Debug, Clone)] +pub struct Source { + pub url: Url, + pub local_name: PathBuf, +} + +impl Display for Source { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.local_name.display()) + } +} + +impl Source { + pub fn new>(url_string: &str, local_base: P) -> Result { + let url = Url::parse(url_string)?; + let path = url.path().to_owned(); + let path_vec: Vec<_> = path.split("/").collect(); + match path_vec.last() { + Some(str) => { + let local_name = str.clone(); + Ok(Source { + url, + local_name: local_base.as_ref().join(local_name), + }) + } + None => Err(SourceError::CantCreateSource(url.into()))? + } + } +} \ No newline at end of file diff --git a/ports/src/workspace.rs b/ports/src/workspace.rs new file mode 100644 index 0000000..f18d5e5 --- /dev/null +++ b/ports/src/workspace.rs @@ -0,0 +1,215 @@ +use std::fs::{create_dir_all, File}; +use crate::sources::{Source, SourceError}; +use std::process::{Command, Stdio}; +use std::path::{Path, PathBuf}; +use std::io::copy; +use std::collections::HashMap; +use std::io::prelude::*; +use std::env; +use libips::actions::{Manifest, File as FileAction, ActionError}; +use std::env::{set_current_dir, current_dir}; +use thiserror::Error; +use std::result::Result as StdResult; +use std::io::Error as IOError; + +type Result = StdResult; + +static DEFAULTWORKSPACEROOT: &str = "~/.ports/wks"; +static DEFAULTARCH: &str = "i386"; +static DEFAULTTAR: &str = "gtar"; +static DEFAULTSHEBANG: &'static [u8; 19usize] = b"#!/usr/bin/env bash"; + +#[derive(Debug, Error)] +pub enum WorkspaceError { + #[error("command returned {command} exit code: {code}")] + NonZeroCommandExitCode { + command: String, + code: i32, + }, + #[error("source {0} cannot be extracted")] + UnextractableSource(Source), + #[error("status code invalid")] + InvalidStatusCode, + #[error("source {0} has no extension")] + SourceHasNoExtension(Source), + #[error("io error: {0}")] + IOError(#[from] IOError), + #[error("could not lookup variable {0}")] + VariableLookupError(String), + #[error("source error: {0}")] + SourceError(#[from] SourceError), + #[error("reqwest error: {0}")] + ReqwestError(#[from] reqwest::Error), + #[error("unrunable script {0}")] + UnrunableScript(String), + #[error("path lookup error: {0}")] + PathLookupError(#[from] which::Error), + #[error("ips action error: {0}")] + IpsActionError(#[from] ActionError), +} + + +pub struct Workspace { + root: PathBuf, + source_dir: PathBuf, + build_dir: PathBuf, + proto_dir: PathBuf, +} + +fn init_root(ws: &Workspace) -> Result<()> { + create_dir_all(&ws.root)?; + create_dir_all(&ws.build_dir)?; + create_dir_all(&ws.source_dir)?; + create_dir_all(&ws.proto_dir)?; + + Ok(()) +} + +impl Workspace { + pub fn new(root: &str) -> Result { + + let root_dir = if root == "" { + DEFAULTWORKSPACEROOT + } else { + root + }; + + let expanded_root_dir = shellexpand::full(root_dir).map_err(|e|{ + WorkspaceError::VariableLookupError(format!("{}", e.cause)) + })?.to_string(); + + let ws = Workspace{ + root: Path::new(&expanded_root_dir).to_path_buf(), + build_dir: Path::new(&expanded_root_dir).join("build").join(DEFAULTARCH), + source_dir: Path::new(&expanded_root_dir).join("sources"), + proto_dir: Path::new(&expanded_root_dir).join("build").join("proto"), + }; + + init_root(&ws)?; + + Ok(ws) + } + + pub fn expand_source_path(&self, fname: &str) -> PathBuf { + self.source_dir.join(fname) + } + + pub fn get_proto_dir(&self) -> PathBuf { + self.proto_dir.clone() + } + + pub fn get_build_dir(&self) -> PathBuf { + self.build_dir.clone() + } + + pub fn get_macros(&self) -> HashMap { + [ + ("proto_dir".to_owned(), self.proto_dir.clone()), + ("build_dir".to_owned(), self.build_dir.clone()), + ("source_dir".to_owned(), self.source_dir.clone()), + ].into() + } + + pub fn get_sources(&self, sources: Vec) -> Result> { + let mut src_vec: Vec = vec![]; + for src in sources { + let src_struct = Source::new(&src, &self.source_dir)?; + let bytes = reqwest::blocking::get(src_struct.url.as_str())?.bytes()?; + let mut out = File::create(&src_struct.local_name)?; + copy(&mut bytes.as_ref(), &mut out)?; + + src_vec.push(src_struct); + } + + Ok(src_vec) + } + + pub fn unpack_all_sources(&self, sources: Vec) -> Result<()> { + for src in sources { + self.unpack_source(&src)?; + } + + Ok(()) + } + + pub fn unpack_source(&self, src: &Source) -> Result<()> { + match Path::new(&src.local_name).extension() { + Some(ext) => { + if !ext.to_str().ok_or(WorkspaceError::SourceHasNoExtension(src.clone()))?.contains("tar") { + return Err(WorkspaceError::UnextractableSource(src.clone())); + } + //TODO support inspecting the tar file to see if we have a top level directory or not + let mut tar_cmd = Command::new(DEFAULTTAR) + .args([ + "-C", + &self.build_dir.to_str().ok_or(WorkspaceError::UnextractableSource(src.clone()))?, + "-xaf", &src.local_name.to_str().ok_or(WorkspaceError::UnextractableSource(src.clone()))?, + "--strip-components=1" + ]) + .spawn()?; + + let status = tar_cmd.wait()?; + if !status.success() { + return Err(WorkspaceError::NonZeroCommandExitCode {command: "tar".to_owned(), code: status.code().ok_or(WorkspaceError::InvalidStatusCode)?})?; + } + } + None => { + return Err(WorkspaceError::UnextractableSource(src.clone())); + } + } + + Ok(()) + } + + pub fn build(&self, build_script: String) -> Result<()> { + let build_script_path = self.build_dir.join("build_script.sh"); + let mut file = File::create(&build_script_path)?; + file.write_all(DEFAULTSHEBANG)?; + file.write_all(b"\n")?; + file.write_all(build_script.as_bytes())?; + file.write_all(b"\n")?; + let bash = which::which("bash")?; + let filtered_env : HashMap = + env::vars().filter(|&(ref k, _)| + k == "TERM" || k == "TZ" || k == "LANG" || k == "PATH" + ).collect(); + + let mut shell = Command::new(bash) + .args([ + "-ex", + &build_script_path.to_str().ok_or(WorkspaceError::UnrunableScript("build_script".into()))? + ]) + .env_clear() + .envs(&filtered_env) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()?; + + let status = shell.wait()?; + if !status.success() { + return Err(WorkspaceError::NonZeroCommandExitCode {command: "build_script".to_owned(), code: status.code().ok_or(WorkspaceError::InvalidStatusCode)?})?; + } + + Ok(()) + } + + pub fn package(&self, file_list: Vec) -> Result<()> { + let mut manifest = Manifest::default(); + let cwd = current_dir()?; + set_current_dir(Path::new(&self.proto_dir))?; + for f in file_list { + if f.starts_with("/") { + let mut f_mut = f.clone(); + f_mut.remove(0); + manifest.add_file(FileAction::read_from_path(Path::new(&f_mut))?) + } else { + manifest.add_file(FileAction::read_from_path(Path::new(&f))?) + } + } + set_current_dir(cwd)?; + + println!("{:?}", manifest); + + Ok(()) + } +} \ No newline at end of file diff --git a/specfile/Cargo.toml b/specfile/Cargo.toml new file mode 100644 index 0000000..9bc27b8 --- /dev/null +++ b/specfile/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "specfile" +version = "0.1.0" +authors = ["Till Wegmueller "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pest = "2.3.0" +pest_derive = "2.3.0" +anyhow = "1.0.59" +thiserror = "*" \ No newline at end of file diff --git a/specfile/src/lib.rs b/specfile/src/lib.rs new file mode 100644 index 0000000..9936626 --- /dev/null +++ b/specfile/src/lib.rs @@ -0,0 +1,164 @@ +pub mod macros; + +use pest::Parser; +use pest_derive::Parser; +use std::collections::HashMap; +use anyhow::{Result}; + +#[derive(Parser)] +#[grammar = "specfile.pest"] +struct SpecFileParser; + +#[derive(Default, Debug)] +pub struct SpecFile { + pub name: String, + pub version: String, + pub release: String, + pub summary: String, + pub license: String, + pub sources: Vec, + pub variables: HashMap, + pub description: String, + pub prep_script: String, + pub build_script: String, + pub install_script: String, + pub files: Vec, + pub changelog: String, +} + +enum KnownVariableControl { + Name, + Version, + Release, + Summary, + License, + None, +} + +fn append_newline_string(s: &str, section_line: i32) -> String { + if section_line == 0 { + return s.to_owned(); + } + return "\n".to_owned() + s; +} + +pub fn parse(file_contents: String) -> Result { + let pairs = SpecFileParser::parse(Rule::file, &file_contents)?; + let mut spec = SpecFile::default(); + + + for pair in pairs { + // A pair can be converted to an iterator of the tokens which make it up: + match pair.as_rule() { + Rule::variable => { + let mut var_control = KnownVariableControl::None; + let mut var_name_tmp = String::new(); + for variable_rule in pair.clone().into_inner() { + match variable_rule.as_rule() { + Rule::variable_name => { + match variable_rule.as_str() { + "Name" => var_control = KnownVariableControl::Name, + "Version" => var_control = KnownVariableControl::Version, + "Release" => var_control = KnownVariableControl::Release, + "Summary" => var_control = KnownVariableControl::Summary, + "License" => var_control = KnownVariableControl::License, + _ => var_control = { + var_name_tmp = variable_rule.as_str().to_string(); + KnownVariableControl::None + }, + } + } + Rule::variable_text => { + match var_control { + KnownVariableControl::Name => spec.name = variable_rule.as_str().to_string(), + KnownVariableControl::Version => spec.version = variable_rule.as_str().to_string(), + KnownVariableControl::Release => spec.release = variable_rule.as_str().to_string(), + KnownVariableControl::Summary =>spec.summary = variable_rule.as_str().to_string(), + KnownVariableControl::License => spec.license = variable_rule.as_str().to_string(), + KnownVariableControl::None => { + spec.variables.insert(var_name_tmp.clone(), variable_rule.as_str().to_string()); + } + } + } + _ => () + } + } + } + Rule::section => { + let mut section_name_tmp = String::new(); + let mut section_line = 0; + for section_rule in pair.clone().into_inner() { + match section_rule.as_rule() { + Rule::section_name => { + section_name_tmp = section_rule.as_str().to_string() + } + Rule::section_line => { + for line_or_comment in section_rule.into_inner() { + match line_or_comment.as_rule() { + Rule::section_text => { + match section_name_tmp.as_str() { + "description" => { + spec.description.push_str(append_newline_string(line_or_comment.as_str(), section_line).as_str()); + section_line = section_line + 1 + }, + "prep" => { + spec.prep_script.push_str(append_newline_string(line_or_comment.as_str(), section_line).as_str()); + section_line = section_line + 1 + }, + "build" => { + spec.build_script.push_str(append_newline_string(line_or_comment.as_str(), section_line).as_str()); + section_line = section_line + 1 + }, + "files" => spec.files.push(line_or_comment.as_str().trim_end().to_string()), + "install" => { + spec.install_script.push_str(append_newline_string(line_or_comment.as_str(), section_line).as_str()); + section_line = section_line + 1 + }, + "changelog" => { + spec.changelog.push_str(append_newline_string(line_or_comment.as_str(), section_line).as_str()); + section_line = section_line + 1 + }, + _ => panic!( + "Unknown Section: {:?}", + line_or_comment.as_rule() + ), + } + } + _ => () + } + } + } + _ => panic!("Rule not known please update the code: {:?}", section_rule.as_rule()), + } + } + } + Rule::EOI => (), + _ => panic!("Rule not known please update the code: {:?}", pair.as_rule()), + } + } + + Ok(spec) +} + +#[cfg(test)] +mod tests { + use std::fs; + use crate::parse; + + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } + + #[test] + fn test_parse() { + let contents = fs::read_to_string("src/test_data/simple.spec"); + match contents { + Ok(file) => { + let spec = parse(file); + assert!(spec.is_ok(), "parsing error {:?}", spec) + }, + Err(e) => panic!("io error: {:}", e) + } + } +} diff --git a/specfile/src/macro.pest b/specfile/src/macro.pest new file mode 100644 index 0000000..e440cf2 --- /dev/null +++ b/specfile/src/macro.pest @@ -0,0 +1,19 @@ +WHITESPACE = _{ " " | "\t" } +char = { ASCII_ALPHANUMERIC | "." | "_" | "/" | "-" | "=" | ">" | "<" | "!" | "'" | "#" | "\"" } +text = @{ char+ | WHITESPACE | NEWLINE } +env_variable_char = { 'A'..'Z' | '0'..'9' } + +macro_parameter = @{char+} +macro_name = @{char+} +macro_parameters = _{ ("," | macro_parameter)* } +spec_macro_without_parameters = _{"%{" ~ macro_name ~ "}" | "%" ~ macro_name} +spec_macro_with_parameters = _{"%{" ~ macro_name ~ "("~macro_parameters~")" ~ "}" | "%" ~ macro_name ~ "("~macro_parameters~")"} +spec_macro = { (spec_macro_with_parameters|spec_macro_without_parameters) } +spec_optional_macro = @{ "%{?" ~ char+ ~ "}" } +function = { "%(" ~ (spec_macro | spec_optional_macro | text)* ~ ")" } +text_with_macros = { function | spec_macro | spec_optional_macro } + +file = {SOI ~ text* ~ (text | text_with_macros)+ ~ text* ~ EOI} + +env_variable = @{"$" ~ (env_variable_char | "_")+} + diff --git a/specfile/src/macros.rs b/specfile/src/macros.rs new file mode 100644 index 0000000..7240004 --- /dev/null +++ b/specfile/src/macros.rs @@ -0,0 +1,99 @@ +use pest::Parser; +use pest_derive::Parser; +use std::collections::HashMap; +use thiserror::Error; +use anyhow::Result; + +#[derive(Debug, Error)] +pub enum MacroParserError { + #[error("macro does not exist: {macro_name}")] + DoesNotExist { + macro_name: String, + } +} + +#[derive(Parser)] +#[grammar = "macro.pest"] +struct InternalMacroParser; + +#[derive(Default, Debug)] +pub struct MacroParser { + pub macros: HashMap +} + +#[derive(Default, Debug)] +pub struct Macro { + pub name: String, + pub parameters: Vec +} + +impl MacroParser { + pub fn parse(&self ,raw_string: String) -> Result { + let mut return_string = String::new(); + + for (i, line) in raw_string.lines().enumerate() { + let mut replaced_line = String::new(); + let pairs = InternalMacroParser::parse(Rule::file, &line)?; + + for pair in pairs { + for test_pair in pair.into_inner() { + match test_pair.as_rule() { + Rule::text_with_macros => { + for inner in test_pair.into_inner() { + match inner.as_rule() { + Rule::spec_macro => { + for macro_pair in inner.clone().into_inner() { + match macro_pair.as_rule() { + Rule::macro_name => { + replaced_line += self.get_variable(macro_pair.as_str())?; + }, + Rule::macro_parameter => { + println!("macro parameter: {}", macro_pair.as_str()) + }, + _ => panic!( + "Unexpected macro match please update the code together with the peg grammar: {:?}", + macro_pair.as_rule() + ) + } + } + } + _ => panic!( + "Unexpected inner match please update the code together with the peg grammar: {:?}", + inner.as_rule() + ) + } + } + }, + Rule::EOI => (), + Rule::text => { + replaced_line += test_pair.as_str(); + replaced_line += " "; + }, + _ => panic!( + "Unexpected match please update the code together with the peg grammar: {:?}", + test_pair.as_rule() + ) + } + } + } + replaced_line = replaced_line.trim_end().to_owned(); + + if i == 0 { + return_string += &replaced_line; + } else { + return_string += "\n"; + return_string += &replaced_line; + } + } + + Ok(return_string) + } + + fn get_variable(&self, macro_name: &str) -> Result<&str> { + if self.macros.contains_key(macro_name) { + return Ok(self.macros[macro_name].as_str()) + } + Err(MacroParserError::DoesNotExist {macro_name: macro_name.into()})? + } +} + diff --git a/specfile/src/specfile.pest b/specfile/src/specfile.pest new file mode 100644 index 0000000..6516223 --- /dev/null +++ b/specfile/src/specfile.pest @@ -0,0 +1,22 @@ +alpha = { 'a'..'z' | 'A'..'Z' } +digit = { '0'..'9' } +uppercase = { 'A'..'Z' } +char = { ASCII_ALPHANUMERIC | "," | "." | "_" | "/" | "-" | "=" | ">" | "<" | "!" | "'" | "#" | ":" | "{" | "}" | "%" | "*" | "@" | "\"" } +WHITESPACE = _{ " " | "\t" } +text = @{ char+ | WHITESPACE } +variable_name = @{uppercase ~ alpha+ ~ digit*} +variable_text = @{ text+ } +variable = {variable_name ~ ":" ~ variable_text } +empty_variable = {variable_name ~ ":" } +multiline_variable = {variable_name ~ ":" ~ NEWLINE? ~ (text ~ NEWLINE)+ } +section_text = @{ text+ } +comment_line = @{ "#" ~ section_text } +section_line = { comment_line ~ NEWLINE | section_text ~ NEWLINE } +section_name = @{ (ASCII_ALPHA_LOWER | ASCII_DIGIT)+ } +section = {"%" ~ section_name ~ NEWLINE ~ section_line+ } + +file = _{ + SOI ~ + (variable ~ NEWLINE+ | multiline_variable ~ NEWLINE+ | empty_variable ~ NEWLINE+ | section | NEWLINE )+ ~ + EOI +} \ No newline at end of file diff --git a/specfile/src/test_data/simple.spec b/specfile/src/test_data/simple.spec new file mode 100644 index 0000000..1f03ca2 --- /dev/null +++ b/specfile/src/test_data/simple.spec @@ -0,0 +1,29 @@ +Name: hello-world +Version: 1 +Release: 1 +Summary: Most simple RPM package +License: FIXME + +%description +This is my first RPM package, which does nothing. +This is a second description line. + +%prep +# we have no source, so nothing here + +%build +cat > hello-world.sh <