diff --git a/Cargo.toml b/Cargo.toml index f5ab526..b5efad1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ cargo-features = ["codegen-backend"] [workspace] -members = ["emulator", "common", "assembler", "dsa_editor", "compiler", "dsx-build"] +members = ["emulator", "common", "assembler", "dsa_editor", "compiler", "dsx_server"] resolver = "3" [workspace.package] diff --git a/dsx-build/Cargo.toml b/dsx-build/Cargo.toml deleted file mode 100644 index 3bce473..0000000 --- a/dsx-build/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "dsx-build" -version.workspace = true -edition.workspace = true -authors.workspace = true - -[dependencies] -compiler = { path = "../compiler" } -assembler = { path = "../assembler" } -chrono = "0.4.43" diff --git a/dsx-build/src/main.rs b/dsx-build/src/main.rs deleted file mode 100644 index 0f3fcdf..0000000 --- a/dsx-build/src/main.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::process::{Command, Stdio}; -use std::{ - env, fs, - path::{Path, PathBuf}, -}; - -use crate::templates::{Dsa, Dsc, Template}; - -mod templates; - -/// Run a command and exit on failure. -fn run(cmd: &mut Command) { - let status = cmd.status().expect("failed to execute command"); - if !status.success() { - std::process::exit(1); - } -} - -fn main() { - // Very small CLI – only three sub‑commands. - let args: Vec = env::args().collect(); - if args.len() < 2 { - eprintln!("Usage: dsx-build [options]"); - std::process::exit(1); - } - match args[1].as_str() { - "new" => cmd_new(&args[2..]), - "build" => cmd_build(), - "package" => todo!("Package manager stub – not implemented yet."), - _ => { - eprintln!("Unknown command: {}", args[1]); - std::process::exit(1); - } - } -} - -// ---------- new project ---------------------------------------------------- -fn cmd_new(args: &[String]) { - let mut lang = "dsa"; - for i in 0..args.len() { - if args[i] == "--lang" && i + 1 < args.len() { - lang = &args[i + 1]; - } - } - - let lib = args.contains(&"--lib".to_string()); - - // Determine project root: a subdirectory named after the supplied --name argument. - let mut name_opt = None; - for i in 0..args.len() { - if args[i] == "--name" && i + 1 < args.len() { - name_opt = Some(&args[i + 1]); - break; - } - } - - let project_name = match name_opt { - Some(name) => name.to_string(), - None => { - eprintln!("Error: --name argument required"); - std::process::exit(1); - } - }; - - let cwd = env::current_dir().unwrap(); - let src_path = cwd.join(&project_name).join("src"); - fs::create_dir_all(&src_path).expect("Failed to create project directory"); - - match lang { - "dsa" => { - // Minimal DSA binary template. - let path = src_path.join(format!("main.dsa")); - - let template = Dsa::create(&project_name, lib); - - fs::write(path, template).expect("Unable to write DSA file"); - } - "dsc" => { - let path = src_path.join(format!("main.dsc")); - - let template = Dsc::create(&project_name, lib); - - fs::write(path, template).expect("Unable to write DSC file"); - } - _ => { - eprintln!("Unsupported language: {}", lang); - std::process::exit(1); - } - } - - fs::create_dir_all(src_path.join("lib")).expect("Failed to create lib directory"); - fs::write( - src_path.join("lib/print.dsa"), - templates::create_print_lib(), - ) - .expect("Failed to create print.dsa"); - fs::write( - src_path.join("lib/maths.dsa"), - templates::create_maths_lib(), - ) - .expect("Failed to create maths.dsa"); - - println!( - "Created new {} project in {}.", - lang, - src_path.parent().unwrap().display() - ); -} - -// ---------- build ---------------------------------------------------------- -fn cmd_build() { - let cwd = env::current_dir().unwrap(); - - // Detect .dsc or .dsa files in current directory. - let mut has_dsc = false; - let mut has_dsa = false; - for entry in fs::read_dir(&cwd.join("src")).expect("unable to read dir") { - if let Ok(entry) = entry { - let path = entry.path(); - if path.extension().and_then(|s| s.to_str()) == Some("dsc") { - has_dsc = true; - } else if path.extension().and_then(|s| s.to_str()) == Some("dsa") { - has_dsa = true; - } - } - } - - if !has_dsc && !has_dsa { - eprintln!("No .dsc or .dsa source found in src directory."); - std::process::exit(1); - } - - // Assemble main.dsa to a dsb binary. - println!("Assembling Project to a DSB binary..."); - let build_dir = cwd.join("build"); - fs::create_dir_all(&build_dir).expect("Failed to create build directory"); - - // Copy everything from `cwd/src` to the build directory. - fn copy_recursively(src: &Path, dst: &Path) { - if src.is_file() { - fs::create_dir_all(dst.parent().unwrap()) - .expect("Failed to create parent directory"); - fs::copy(src, dst).expect("Failed to copy file"); - } else if src.is_dir() { - for entry in fs::read_dir(src).expect("Unable to read source dir") { - let entry = entry.expect("Failed to read entry"); - let child_src = entry.path(); - let child_dst = dst.join(entry.file_name()); - copy_recursively(&child_src, &child_dst); - } - } - } - - let src_dir = cwd.join("src"); - if src_dir.exists() { - copy_recursively(&src_dir, &build_dir); - } - - // Change current working directory to the build directory. - env::set_current_dir(&build_dir).expect("Failed to change to build directory"); - - if has_dsc { - println!("Compiling DSC to DSA..."); - fn compile_recursive(path: &Path) { - if path.is_dir() { - for entry in fs::read_dir(path).expect("unable to read dir") { - let entry = entry.expect("failed to read entry"); - compile_recursive(&entry.path()); - } - } else if path.extension().and_then(|s| s.to_str()) == Some("dsc") { - let input_path = path; - let output_path = path.with_extension("dsa"); - compiler::compile_file(&input_path, &output_path).unwrap_or_else(|e| { - eprintln!("Failed to compile {:?}: {}", input_path, e); - std::process::exit(1); - }); - } - } - compile_recursive(&build_dir); - } - - // Replace .dsc with .dsa only in include statements, recursively for each file. - let mut sed_cmd = Command::new("bash"); - sed_cmd.args(&[ - "-c", - &format!( - "find \"{}\" -type f -name '*.dsa' -exec sed -i '/^include/ s/\\.dsc/.dsa/g' {{}} +", - build_dir.display() - ), - ]); - run(&mut sed_cmd); - - fs::create_dir_all(&cwd.join("artifacts")).expect("Failed to create build directory"); - assembler::assemble_file("./main.dsa", "../artifacts/out.dsb").unwrap_or_else(|e| { - eprintln!("Failed to assemble {:?}: {}", "./main.dsa", e); - std::process::exit(1); - }); - - println!("Build finished. Binary at {}/main.dsb", build_dir.display()); -} diff --git a/dsx_server/Cargo.toml b/dsx_server/Cargo.toml new file mode 100644 index 0000000..535509f --- /dev/null +++ b/dsx_server/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "dsx" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[[bin]] +name = "dsx_server" +path = "src/server/server.rs" + +[[bin]] +name = "dsx" +path = "src/client/client.rs" + +[dependencies] +compiler = { path = "../compiler" } +assembler = { path = "../assembler" } +common = { path = "../common" } + +anyhow = "1.0.102" +dotenv = "0.15.0" +rocket = { version = "0.5.1", features = ["json"] } +rocket_dyn_templates = { version = "0.2.0", features = ["tera"] } +serde = { version = "1.0.228", features = ["derive"] } +toml = "1.0.3" +tracing = "0.1.44" +tracing-subscriber = { version = "0.3.22", features = ["env-filter"] } +chrono = "0.4.43" +tar = "0.4.44" +flate2 = "1.1.9" +walkdir = "2.5.0" diff --git a/dsx_server/bacon.toml b/dsx_server/bacon.toml new file mode 100644 index 0000000..66d1030 --- /dev/null +++ b/dsx_server/bacon.toml @@ -0,0 +1,106 @@ +default_job = "check" +env.CARGO_TERM_COLOR = "always" + +[jobs.check] +command = ["cargo", "check"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets"] +need_stdout = false + +# Run clippy on the default target +[jobs.clippy] +command = ["cargo", "clippy"] +need_stdout = false + +[jobs.clippy-all] +command = ["cargo", "clippy", "--all-targets"] +need_stdout = false + +# Run clippy in pedantic mode +# The 'dismiss' feature may come handy +[jobs.pedantic] +command = [ + "cargo", "clippy", + "--", + "-W", "clippy::pedantic", +] +need_stdout = false + +# This job lets you run +# - all tests: bacon test +# - a specific test: bacon test -- config::test_default_files +# - the tests of a package: bacon test -- -- -p config +[jobs.test] +command = ["cargo", "test"] +need_stdout = true + +[jobs.nextest] +command = [ + "cargo", "nextest", "run", + "--hide-progress-bar", "--failure-output", "final" +] +need_stdout = true +analyzer = "nextest" + +[jobs.doc] +command = ["cargo", "doc", "--no-deps"] +need_stdout = false + +# If the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# if it makes sense for this crate. +[jobs.run] +command = [ + "cargo", "run", "--bin", "dsx_server" + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = true + +# Run your long-running application (eg server) and have the result displayed in bacon. +# For programs that never stop (eg a server), `background` is set to false +# to have the cargo run output immediately displayed instead of waiting for +# program's end. +# 'on_change_strategy' is set to `kill_then_restart` to have your program restart +# on every change (an alternative would be to use the 'F5' key manually in bacon). +# If you often use this job, it makes sense to override the 'r' key by adding +# a binding `r = job:run-long` at the end of this file . +# A custom kill command such as the one suggested below is frequently needed to kill +# long running programs (uncomment it if you need it) +[jobs.run-long] +command = [ + "cargo", "run", + # put launch parameters for your program behind a `--` separator +] +need_stdout = true +allow_warnings = true +background = false +on_change_strategy = "kill_then_restart" +# kill = ["pkill", "-TERM", "-P"] + +# This parameterized job runs the example of your choice, as soon +# as the code compiles. +# Call it as +# bacon ex -- my-example +[jobs.ex] +command = ["cargo", "run", "--example"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal global prefs.toml file instead. +[keybindings] +# alt-m = "job:my-job" +c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target +p = "job:pedantic" diff --git a/dsx_server/data/repos/example/Package.toml b/dsx_server/data/repos/example/Package.toml new file mode 100644 index 0000000..a2727f9 --- /dev/null +++ b/dsx_server/data/repos/example/Package.toml @@ -0,0 +1 @@ +id="example" diff --git a/dsx_server/data/repos/example/repo/Dsx.toml b/dsx_server/data/repos/example/repo/Dsx.toml new file mode 100644 index 0000000..da5c772 --- /dev/null +++ b/dsx_server/data/repos/example/repo/Dsx.toml @@ -0,0 +1 @@ +name = "example" diff --git a/dsx_server/data/repos/example/repo/src/lib/maths.dsa b/dsx_server/data/repos/example/repo/src/lib/maths.dsa new file mode 100644 index 0000000..687ad5d --- /dev/null +++ b/dsx_server/data/repos/example/repo/src/lib/maths.dsa @@ -0,0 +1,105 @@ + +// multiply.dsa +// usage: +// +// include multiply "" +// +// usage for multiply: +// push (arg1) +// push (arg0) +// call multiply::multiply +// pop (arg0) +// pop (arg1) + +multiply: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 // load op 2 + ldw bpr, rg1, 12 // load op 1 + lwi 0, rg2 // initialise rg2 to zero + +_multiply_loop: + add rg2, rg0, rg2 + dec rg1 + + cmp rg1, zero + jgt _multiply_loop + +_multiply_end: + stw rg2, bpr, 8 + + mov bpr, spr + pop bpr + return + +divmod: + push bpr + mov spr, bpr + + ldw bpr, rg1, 8 // load op 2 + ldw bpr, rg0, 12 // load op 1 + + lli 0, rg3 + +_divmod_loop: + cmp rg0, rg1 + jlt _divmod_end + + sub rg0, rg1, rg0 + inc rg3 + + jmp _divmod_loop + +_divmod_end: + // store div in first arg + // store mod in second arg + stw rg3, bpr, 8 + stw rg0, bpr, 12 + + mov bpr, spr + pop bpr + return + +// multiply.dsa - improved version +// Multiplies two 32-bit numbers using shift-and-add +// +// Usage: +// push operand2 (multiplier) +// push operand1 (multiplicand) +// call multiply::multiply +// pop result +// pop zero (discard second argument) + +new_multiply: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 // rg0 = multiplicand + ldw bpr, rg1, 12 // rg1 = multiplier + + lli 0, rg2 // rg2 = result (accumulator) + lli 32, rg3 // rg3 = bit counter + +mult_loop: + // Check if lowest bit of multiplier is 1 + lli 1, acc + and rg1, acc, acc // acc = rg1 & 1 + cmp acc, zero + jeq skip_add // if (rg1 & 1) == 0, skip addition + + // Add multiplicand to result + add rg2, rg0, rg2 + +skip_add: + shl rg0, 1 // shift multiplicand left + shr rg1, 1 // shift multiplier right + + dec rg3 + cmp rg3, zero + jgt mult_loop + + stw rg2, bpr, 8 // store result + mov bpr, spr + pop bpr + return diff --git a/dsx_server/data/repos/example/repo/src/lib/print.dsa b/dsx_server/data/repos/example/repo/src/lib/print.dsa new file mode 100644 index 0000000..ead1d5c --- /dev/null +++ b/dsx_server/data/repos/example/repo/src/lib/print.dsa @@ -0,0 +1,332 @@ + +// lib: +// print.dsa + +// usage: +// +// include print """ +// +// usage for print: +// push (register containing address of string) +// push pcx +// jmp print::print +// +// usage for reset: +// push pcx +// jmp print::reset +// +// usage for clear: +// push pcx +// jmp print::clear +// +// usage for print_byte: +// push (register containing byte) +// push pcx +// jmp print::print_byte +// +// usage for print_word: +// push (register containing word) +// push pcx +// jmp print::print_word +// +// usage for print_num: +// push (register containing number to print in decimal) +// push pcx +// jmp print::print_num +// + +include maths "./maths.dsa" + +dw display: 0x20000 +dw current: 0x20000 + +// ------------------------------------------ +// prints the string at addr(arg[0]) to the screen. (no trailing whitespace unless explicitly provided) +print: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + ldw current, rg1 + +_print_loop: + ldb rg0, acc + cmp acc, zero + jeq _end + stb acc, rg1 + + addi rg0, 1 + addi rg1, 1 + + jmp _print_loop + +// ------------------------------------------ +println: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + ldw current, rg1 + +_println_loop: + ldb rg0, acc + cmp acc, zero + jeq _println_end + stb acc, rg1 + + addi rg0, 1 + addi rg1, 1 + + jmp _println_loop + +_println_end: + call print_newline + jmp _end + +// ------------------------------------------ +// prints the value of arg[0] to the screen. +print_word: + // initialise + push bpr + mov spr, bpr + + // load byte into acc + ldw bpr, rg0, 8 + ldw current, rg1 + + addi rg1, 3 + + stb rg0, rg1 + subi rg1, 1 + shr rg0, 8 + stb rg0, rg1 + subi rg1, 1 + shr rg0, 8 + stb rg0, rg1 + subi rg1, 1 + shr rg0, 8 + stb rg0, rg1 + + addi rg1, 4 + jmp _end + +// ------------------------------------------ +// prints the last byte of arg[0] to the screen. +print_byte: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + ldw current, rg1 + + stb rg0, rg1 + addi rg1, 1 + jmp _end + +// ------------------------------------------ +// prints the value of arg[0] to the screen in hex. +print_hex_word: + push bpr + mov spr, bpr + + ldw current, rg1 + + ldb bpr, rg0, 8 + push rg0 + call _print_hex_byte + addi spr, 4 + + ldb bpr, rg0, 9 + push rg0 + call _print_hex_byte + addi spr, 4 + + ldb bpr, rg0, 10 + push rg0 + call _print_hex_byte + addi spr, 4 + + ldb bpr, rg0, 11 + push rg0 + call _print_hex_byte + addi spr, 4 + + jmp _end + +// ------------------------------------------ +// prints the last byte of arg[0] to the screen in hex. +print_hex_byte: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 + ldw current, rg1 + + call _print_hex_byte + jmp _end + +// function body +_print_hex_byte: + // mask to get lower nibble + lli 0xF, rg2 + // save rg0 state + push rg0 + + shr rg0, 4 + and rg0, rg2, rg0 + call _print_hex_nibble + pop rg0 + + and rg0, rg2, rg0 + call _print_hex_nibble + return + +// print a hex digit +_print_hex_nibble: + lli 10, rg3 + cmp rg0, rg3 + jlt _print_hex_nibble_number + addi rg0, 0x37, rg0 + stb rg0, rg1 + addi rg1, 1 + return + +// helper function. +_print_hex_nibble_number: + addi rg0, 0x30, rg0 + stb rg0, rg1 + addi rg1, 1 + return + +// ------------------------------------------ +// print whitespace +print_whitespace: + push bpr + mov spr, bpr + + ldw current, rg1 + lli 0x20, rg0 + stb rg0, rg1 + addi rg1, 1 + jmp _end + +// ------------------------------------------ +// print newline +print_newline: + push bpr + mov spr, bpr + + // load variables into registers + ldw display, rg0 + ldw current, rg1 + + // get the offset from the display base + sub rg1, rg0, rg0 + + lwi 80, rg2 + pusha 3 + push rg0 + push rg2 + call maths::divmod + pop zero // result + pop rg3 // remainder + popa 3 + + sub rg1, rg3, rg2 + addi rg2, 80, rg1 + + // _end saves the display state + jmp _end + +// ------------------------------------------ +// prints arg[0] as a decimal number to the screen. +print_num: + push bpr + mov spr, bpr + + ldw bpr, rg0, 8 // load number to print + lli 0, rg5 // rg5 = digit counter + + // check if number is zero + cmp rg0, zero + jne _print_num_extract_digits + + // special case: print '0' for zero + lli 0x30, rg6 + push rg6 // push digit to stack buffer + lli 1, rg5 // we have 1 digit + jmp _print_num_output + +_print_num_extract_digits: + // divide by 10 repeatedly to get digits + cmp rg0, zero + jeq _print_num_output + + // call divmod(rg0, 10) + push rg0 // dividend + lli 10, rg1 + push rg1 // divisor (10) + call maths::divmod + pop rg0 // quotient (continue dividing this) + pop rg1 // remainder (the digit) + + // convert digit to ASCII and push to stack buffer + addi rg1, 0x30, rg6 // convert to ASCII + push rg6 // push digit to stack + inc rg5 // increment digit counter + + jmp _print_num_extract_digits + +_print_num_output: + // now print digits (pop them off in reverse order) + ldw current, rg1 // get display pointer + +_print_num_output_loop: + // check if we've printed all digits + cmp rg5, zero + jeq _print_num_done + + // pop digit and print it + pop rg6 + stb rg6, rg1 + addi rg1, 1 + dec rg5 + + jmp _print_num_output_loop + +_print_num_done: + jmp _end + +// ------------------------------------------ +// resets the cursor position on the screen to 0x20000. (0,0) +reset: + push bpr + mov spr, bpr + ldw display, rg1 + jmp _end + +// ------------------------------------------ +// clears the screen +clear: + push bpr + mov spr, bpr + // display size = 2000 bytes / 500 words + lli 500 rg0 + ldw display, rg1 + +_clear_loop: + dec rg0 + stw zero, rg1 + addi rg1, 4 + cmp rg0, zero + jgt _clear_loop + jmp _end + +// ------------------------------------------ +// return +_end: + stw rg1, current + + mov bpr, spr + pop bpr + return diff --git a/dsx_server/data/repos/example/repo/src/main.dsa b/dsx_server/data/repos/example/repo/src/main.dsa new file mode 100644 index 0000000..9b60951 --- /dev/null +++ b/dsx_server/data/repos/example/repo/src/main.dsa @@ -0,0 +1,39 @@ + +// GENERATED BY DSX-BUILD +// Generated at: 2026-02-21 02:50:14 +// Project name: example + +// Imports +include print: "./lib/print.dsa" + +// Globals & Reserved Memory +dw stack: 0x10000 +db message: "Process Exited with code:" + +// Entry Point +_init: + ldw stack, bpr + mov bpr, spr + push zero + call main + call print::print_newline + lwi message, rg0 + push rg0 + call print::print + pop zero + call print::print_hex_word + pop zero + hlt + +main: + push bpr + mov spr, bpr + + // Your code goes here + + // Return zero + stw zero, bpr, 8 + + mov bpr, spr + pop bpr + return \ No newline at end of file diff --git a/dsx_server/doc/endpoints.md b/dsx_server/doc/endpoints.md new file mode 100644 index 0000000..efa1cbc --- /dev/null +++ b/dsx_server/doc/endpoints.md @@ -0,0 +1,19 @@ + +# Endpoints + +let n be the repo name. + +## Web view +GET /packages/ # home page listing packages - simple search bar. +GET /packages?q= # search for a package +GET /packages/ # main page for a repository, shows status, files, name etc. +GET /packages//~repo/ # path for a file within a repo +GET /packages//~repo?q= # search within a package's files +GET /packages//~artifact/ # page listing repo artifacts by date +GET /packages//~artifact/ # page for a specific artifact and status/logs + +POST /api/pkg # create repo +GET /api/pkg/ # repo status/metadata +POST /api/pkg//push # upload source tarball +GET /api/pkg//pull # download source tarball +GET /api/pkg//artifact # download compiled binary diff --git a/dsx_server/doc/structure.md b/dsx_server/doc/structure.md new file mode 100644 index 0000000..c6b9971 --- /dev/null +++ b/dsx_server/doc/structure.md @@ -0,0 +1,15 @@ +# Folder structure + +data/ + repos/ + / + repo/ + Dsx.toml + README.md + src/ + artifacts/ + .dsb + .dsb + docs/ + .md + index/ diff --git a/dsx_server/src/client/client.rs b/dsx_server/src/client/client.rs new file mode 100644 index 0000000..6dfdf7b --- /dev/null +++ b/dsx_server/src/client/client.rs @@ -0,0 +1,53 @@ +use std::process::{Command, Stdio}; +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +use dsx::common::builder; + +pub mod new; + +fn main() { + // Very small CLI – only three sub‑commands. + let args: Vec = env::args().collect(); + if args.len() < 2 { + eprintln!("Usage: dsx-build [options]"); + std::process::exit(1); + } + match args[1].as_str() { + "new" => new::new_project(&args[2..]), + "build" => { + if let Some(dir) = find_project_root() { + builder::build_project(&dir).expect("Build failed!"); + } else { + eprintln!("No Dsx.toml found"); + std::process::exit(1); + } + } + "package" => todo!("Package manager stub – not implemented yet."), + _ => { + eprintln!("Unknown command: {}", args[1]); + std::process::exit(1); + } + } +} + +fn find_project_root() -> Option { + // check if current dir has Dsx.toml otherwise check parent dir recursively + let mut cwd = env::current_dir().unwrap(); + loop { + let dsx_toml = cwd.join("Dsx.toml"); + if dsx_toml.exists() { + return Some(cwd); + } + + if let Some(parent) = cwd.parent() { + cwd = parent.to_path_buf(); + } else { + break; + } + } + + None +} diff --git a/dsx_server/src/client/new.rs b/dsx_server/src/client/new.rs new file mode 100644 index 0000000..abe265a --- /dev/null +++ b/dsx_server/src/client/new.rs @@ -0,0 +1,103 @@ +use std::{env, fmt, fs, path::PathBuf}; + +use dsx::common::{ + config::DsxConfig, + templates::{self, Dsa, Dsc, Template}, +}; + +// ---------- new project ---------------------------------------------------- +pub fn new_project(args: &[String]) { + // get project details from args. + let lang = Language::from_args(args); + let lib = args.contains(&"--lib".to_string()); + let name = project_name(args).unwrap_or_else(|| { + eprintln!("Error: --name argument required"); + std::process::exit(1); + }); + + let project_path = env::current_dir().unwrap().join(name); + let src_path = project_path.join("src"); + + fs::create_dir_all(&src_path).expect("Failed to create project directory"); + + let config_template = DsxConfig::new(name); + fs::write( + project_path.join("Dsx.toml"), + toml::to_string(&config_template).unwrap(), + ) + .expect("Unable to write default config"); + + let (path, template) = match lang { + Language::Unknown | Language::Dsa => { + (src_path.join("main.dsa"), Dsa::create(name, lib)) + } + Language::Dsc => (src_path.join("main.dsc"), Dsc::create(name, lib)), + }; + + fs::write(path, template).expect("Unable to write DSA file"); + + fs::create_dir_all(src_path.join("lib")).expect("Failed to create lib directory"); + fs::write( + src_path.join("lib/print.dsa"), + templates::create_print_lib(), + ) + .expect("Failed to create print.dsa"); + fs::write( + src_path.join("lib/maths.dsa"), + templates::create_maths_lib(), + ) + .expect("Failed to create maths.dsa"); + + println!( + "Created new {} project in {}.", + lang, + src_path.parent().unwrap().display() + ); +} + +// helpers + +enum Language { + Unknown, + Dsa, + Dsc, +} + +impl Language { + fn from_args(args: &[String]) -> Self { + let mut lang = Language::Unknown; + for i in 0..args.len() { + if args[i] == "--lang" && i + 1 < args.len() { + match args[i + 1].as_str() { + "dsa" => lang = Language::Dsa, + "dsc" => lang = Language::Dsc, + _ => { + eprintln!("Error: Invalid language argument"); + std::process::exit(1); + } + } + } + } + lang + } +} + +impl fmt::Display for Language { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Unknown => write!(f, "Unknown"), + Self::Dsa => write!(f, "Dsa"), + Self::Dsc => write!(f, "Dsc"), + } + } +} + +pub fn project_name(args: &[String]) -> Option<&str> { + for i in 0..args.len() { + if args[i] == "--name" && i + 1 < args.len() { + return Some(&args[i + 1]); + } + } + + None +} diff --git a/dsx_server/src/common/builder/mod.rs b/dsx_server/src/common/builder/mod.rs new file mode 100644 index 0000000..236b189 --- /dev/null +++ b/dsx_server/src/common/builder/mod.rs @@ -0,0 +1,168 @@ +use std::{ + env, fs, io, + path::{Path, PathBuf}, + process::Command, +}; + +use crate::common::config::DsxConfig; + +use assembler::prelude::Assembler; +use common::build::{BuildError, Builder}; +use compiler::Compiler; + +// ---------- build ---------------------------------------------------------- +pub fn build_project(cwd: &Path) -> Result<(), BuildError> { + let config: DsxConfig = toml::from_str(&fs::read_to_string(cwd.join("Dsx.toml"))?) + .map_err(|deser_err| { + io::Error::new(io::ErrorKind::InvalidData, deser_err.to_string()) + })?; + + let src_dir = cwd.join("src"); + if !src_dir.exists() { + return Err(BuildError::Generic(String::from( + "Source Directory does not exist", + ))); + } + + // make sure there's a main file to assemble later. + if !main_exists(&src_dir)? { + return Err(BuildError::Generic(String::from( + "No main.dsa or main.dsc file found in top level of src directory.", + ))); + } + + // check is redundant as we're already checking for main files. + // if !has_dsc && !has_dsa { + // return Err(io::Error::new( + // io::ErrorKind::NotFound, + // "No .dsc or .dsa source found in src directory.", + // )); + // } + + // detect src. + let (has_dsa, has_dsc) = detect_source_language(&src_dir); + + // create a build dir and copy all files across + let build_dir = cwd.join("build"); + fs::create_dir_all(&build_dir)?; + env::set_current_dir(&build_dir)?; + + copy_recursively(&src_dir, &build_dir)?; + + if has_dsc { + build_all_dsc(&build_dir)?; + } + + // Replace .dsc with .dsa only in include statements, recursively for each file. + let mut sed_cmd = Command::new("bash"); + sed_cmd.args([ + "-c", + &format!( + "find \"{}\" -type f -name '*.dsa' -exec sed -i '/^include/ s/\\.dsc/.dsa/g' {{}} +", + build_dir.display() + ), + ]); + run(&mut sed_cmd); + + // assemble result + { + fs::create_dir_all(cwd.join("artifacts"))?; + let mut asm = Assembler::new("./main.dsa"); + asm.start(); + asm.write_result("../artifacts/out.dsb")?; + } + + println!("Build finished. Binary at {}/main.dsb", build_dir.display()); + Ok(()) +} + +// ----- Helpers ------------------------------- + +struct BuildStep; +impl BuildStep { + pub fn compiling(path: &Path) { + println!("Compiling {}", path.display()); + } + + pub fn assembling(path: &Path) { + println!("Assembling {}", path.display()); + } +} + +/// Checks what source languages are used in the project. +fn detect_source_language(src_dir: &Path) -> (bool, bool) { + let mut contains_dsc = false; + let mut contains_dsa = false; + + for entry in walkdir::WalkDir::new(src_dir).into_iter().flatten() { + match entry.path().extension().and_then(|s| s.to_str()) { + Some("dsc") => contains_dsc = true, + Some("dsa") => contains_dsa = true, + _ => {} + } + } + (contains_dsa, contains_dsc) +} + +// Checks if either main.dsa or main.dsc exist in the source directory +fn main_exists(src_dir: &Path) -> Result { + for entry in fs::read_dir(src_dir).into_iter().flatten() { + match entry?.path().file_name().and_then(|s| s.to_str()) { + Some("main.dsc") => return Ok(true), + Some("main.dsa") => return Ok(true), + _ => {} + } + } + + Ok(false) +} + +// Copy contents of one directory to another +fn copy_recursively(src: &Path, dst: &Path) -> Result<(), std::io::Error> { + if src.is_file() { + fs::create_dir_all(dst.parent().unwrap())?; + fs::copy(src, dst)?; + return Ok(()); + } + + if src.is_dir() { + for entry in fs::read_dir(src)? { + let entry = entry?; + let child_src = entry.path(); + let child_dst = dst.join(entry.file_name()); + copy_recursively(&child_src, &child_dst)?; + } + } + + Ok(()) +} + +fn build_all_dsc(path: &Path) -> Result<(), BuildError> { + if path.is_dir() { + for entry in fs::read_dir(path)? { + let entry = entry?; + build_all_dsc(&entry.path())?; + } + + return Ok(()); + } + + if path.extension().and_then(|s| s.to_str()) == Some("dsc") { + let input_path = path; + let output_path = path.with_extension("dsa"); + + let mut compiler = Compiler::new(input_path); + compiler.start(); + compiler.write_result(output_path.clone())?; + } + + Ok(()) +} + +/// Run a command and exit on failure. +fn run(cmd: &mut Command) { + let status = cmd.status().expect("failed to execute command"); + if !status.success() { + std::process::exit(1); + } +} diff --git a/dsx_server/src/common/config.rs b/dsx_server/src/common/config.rs new file mode 100644 index 0000000..be5da62 --- /dev/null +++ b/dsx_server/src/common/config.rs @@ -0,0 +1,45 @@ +use rocket::serde; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DsxConfig { + pub name: String, + + #[serde(default)] + pub description: Option, + + #[serde(default)] + pub remote_url: Option, + + #[serde(default)] + pub binaries: Vec, + // todo! + // #[serde(default)] + // pub libraries: Vec, +} + +impl DsxConfig { + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + description: None, + remote_url: None, + binaries: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Binary { + pub name: String, + pub path: String, +} + +impl Binary { + pub fn new(name: &str, path: &str) -> Self { + Self { + name: name.to_string(), + path: path.to_string(), + } + } +} diff --git a/dsx_server/src/common/mod.rs b/dsx_server/src/common/mod.rs new file mode 100644 index 0000000..d3d7265 --- /dev/null +++ b/dsx_server/src/common/mod.rs @@ -0,0 +1,3 @@ +pub mod builder; +pub mod config; +pub mod templates; diff --git a/dsx-build/src/templates.rs b/dsx_server/src/common/templates.rs similarity index 100% rename from dsx-build/src/templates.rs rename to dsx_server/src/common/templates.rs diff --git a/dsx_server/src/lib.rs b/dsx_server/src/lib.rs new file mode 100644 index 0000000..34994bf --- /dev/null +++ b/dsx_server/src/lib.rs @@ -0,0 +1 @@ +pub mod common; diff --git a/dsx_server/src/server/error.rs b/dsx_server/src/server/error.rs new file mode 100644 index 0000000..356fcdd --- /dev/null +++ b/dsx_server/src/server/error.rs @@ -0,0 +1,38 @@ +use common::build::BuildError; +use rocket::{Response, http::Status, response::Responder}; +use serde::Serialize; + +#[derive(Debug, Serialize, Responder)] +pub enum ApiError { + #[response(status = 404)] + NotFound(String), + + #[response(status = 500)] + InternalServerError(()), + + #[response(status = 500)] + ServerError(String), + + #[response(status = 401)] + Unauthorized(String), + #[response(status = 403)] + Forbidden(String), + + #[response(status = 400)] + BadRequest(String), +} + +impl From for ApiError { + fn from(err: std::io::Error) -> Self { + ApiError::InternalServerError(()) + } +} + +impl From for ApiError { + fn from(err: BuildError) -> Self { + match err { + BuildError::IoError(err) => ApiError::ServerError(err.to_string()), + BuildError::Generic(err) => ApiError::ServerError(err), + } + } +} diff --git a/dsx_server/src/server/model.rs b/dsx_server/src/server/model.rs new file mode 100644 index 0000000..f152d14 --- /dev/null +++ b/dsx_server/src/server/model.rs @@ -0,0 +1,144 @@ +use std::fs; +use std::path::PathBuf; + +use dsx::common::config::DsxConfig; +use serde::{Deserialize, Serialize}; + +use crate::{DATA_DIR, error::ApiError}; + +// stored as a Config.toml above the repository root. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PackageMeta { + pub id: String, + + #[serde(default)] + pub latest_build_date: Option, + #[serde(default)] + pub latest_build_status: Option, + #[serde(default)] + pub latest_build_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Package { + pub config: DsxConfig, + pub meta: PackageMeta, + pub files: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FileObj { + pub name: String, + pub path: String, + pub is_dir: bool, + pub size: u64, + pub extension: String, +} + +impl Package { + pub fn load(name: &str) -> Result { + let repo_path = DATA_DIR.join("repos").join(name); + + let config_contents = fs::read_to_string(repo_path.join("repo/Dsx.toml")) + .map_err(|e| { + warn!("unable to read config for repo, {e}"); + ApiError::InternalServerError(()) + })?; + + let config: DsxConfig = toml::from_str(&config_contents).map_err(|e| { + warn!("Invalid config file for repo! {e}"); + ApiError::InternalServerError(()) + })?; + + let meta_contents = + fs::read_to_string(repo_path.join("Package.toml")).map_err(|e| { + warn!("unable to read config for repo, {e}"); + ApiError::InternalServerError(()) + })?; + + let meta: PackageMeta = toml::from_str(&meta_contents).map_err(|e| { + warn!("Invalid meta file for repo! {e}"); + ApiError::InternalServerError(()) + })?; + + let dir = fs::read_dir(repo_path.join("repo")).map_err(|e| { + warn!("unable to read files for repo, {e}"); + ApiError::InternalServerError(()) + })?; + + let mut files = Vec::new(); + for entry in dir { + let entry = entry.map_err(|e| { + warn!("unable to read file entry for repo, {e}"); + ApiError::InternalServerError(()) + })?; + let path = entry.path(); + let metadata = fs::metadata(&path).map_err(|e| { + warn!("unable to read file metadata for repo, {e}"); + ApiError::InternalServerError(()) + })?; + let is_dir = metadata.is_dir(); + let size = metadata.len(); + let extension = path + .extension() + .map_or(String::new(), |ext| ext.to_string_lossy().to_string()); + files.push(FileObj { + path: path.to_string_lossy().to_string(), + name: path.file_name().unwrap().to_string_lossy().to_string(), + is_dir, + size, + extension, + }); + } + + Ok(Self { + config, + meta, + files, + }) + } + + pub fn unpack(archive: &std::path::Path) -> Result { + let repo_name = archive.file_name().unwrap().to_str().unwrap(); + let dest = DATA_DIR.join("repos").join(repo_name).join("repo"); + unpack_tarball(archive, &dest)?; + let package = Self::load(repo_name)?; + Ok(package) + } + + pub fn tarball(&self) -> Result, ApiError> { + let src_dir = self.path().join("repo"); + pack_tarball(&src_dir) + } + + pub fn path(&self) -> PathBuf { + DATA_DIR.join("repos").join(&self.meta.id) + } +} + +// ── Tar helpers ─────────────────────────────────────────────────────────────── + +use flate2::read::GzDecoder; +use flate2::{Compression, write::GzEncoder}; +use std::fs::File; +use tar::Builder; + +fn unpack_tarball( + archive: &std::path::Path, + dest: &std::path::Path, +) -> Result<(), ApiError> { + let file = File::open(archive)?; + let gz = GzDecoder::new(file); + let mut tar = tar::Archive::new(gz); + tar.unpack(dest)?; + Ok(()) +} + +fn pack_tarball(src_dir: &std::path::Path) -> Result, ApiError> { + let buf = Vec::new(); + let gz = GzEncoder::new(buf, Compression::default()); + let mut tar = Builder::new(gz); + tar.append_dir_all(".", src_dir)?; + let gz = tar.into_inner()?; + Ok(gz.finish()?) +} diff --git a/dsx_server/src/server/server.rs b/dsx_server/src/server/server.rs new file mode 100644 index 0000000..a42f35a --- /dev/null +++ b/dsx_server/src/server/server.rs @@ -0,0 +1,270 @@ +#[macro_use] +extern crate rocket; + +use std::fs; +use std::path::PathBuf; +use std::sync::LazyLock; + +use anyhow::anyhow; +use rocket::Data; +use rocket::data::ToByteUnit; +use rocket::serde::json::Json; +use rocket::{fs::FileServer, serde::Deserialize}; + +use rocket_dyn_templates::{Template, context}; + +use dotenv::dotenv; +use serde::Serialize; +use tracing::{info, warn}; + +use crate::error::ApiError; +use crate::model::{Package, PackageMeta}; +use dsx::common::config::DsxConfig; + +mod error; +mod model; + +static DATA_DIR: LazyLock = LazyLock::new(|| { + PathBuf::from(std::env::var("$DATA_DIR").unwrap_or("./data".to_string())) +}); + +// Search for a package +#[get("/?")] +fn search_packages(q: Option) -> Result { + #[derive(Serialize)] + struct Package { + name: String, + description: String, + updated_at: String, + } + + let mut packages = Vec::new(); + + let dir = match fs::read_dir(DATA_DIR.join("repos")) { + Ok(dir) => dir, + Err(e) => { + warn!("failed to read repos directory: {}", e); + return Err(ApiError::InternalServerError(())); + } + }; + + for entry in dir { + let entry = entry.map_err(|e| { + warn!("failed to read entry: {}", e); + ApiError::InternalServerError(()) + })?; + + let config_path = entry.path().join("repo").join("Dsx.toml"); + if config_path.exists() { + let text = fs::read_to_string(&config_path).map_err(|e| { + warn!("failed to read config file: {}", e); + ApiError::InternalServerError(()) + })?; + let config: DsxConfig = toml::from_str(&text).map_err(|e| { + warn!("failed to parse config file: {}", e); + ApiError::InternalServerError(()) + })?; + + error!("{}", config.description.clone().unwrap_or_default()); + + // skip repo if it doesnt match query params + if let Some(query) = &q + && !(config.name.contains(query) + || config + .description + .clone() + .unwrap_or_default() + .contains(query)) + { + continue; + } + + packages.push(Package { + name: config.name, + description: config.description.unwrap_or_default(), + updated_at: String::from("0:00"), + }) + } + } + + Ok(Template::render( + "packages", + context! { + packages, + query: q.clone().unwrap_or_default() + }, + )) +} + +// Main page for a repository, shows status, files, name etc. +#[get("/")] +fn package_main(name: &str) -> Result { + // get package info + let package = Package::load(name)?; + + println!("{}", package.config.name); + + Ok(Template::render( + "package_home", + context! { + parent_path: String::new(), + current_path: String::from("/"), + package: package, + }, + )) +} + +// Path for a file within a repo +#[get("//~repo/")] +fn repo_file(name: &str, path: std::path::PathBuf) -> Result { + let package = Package::load(name)?; + + Ok(Template::render( + "file", + context! { + package: package, + }, + )) +} + +// Search within a package's files +#[get("//~repo?")] +fn search_repo_files(name: &str, q: Option) -> String { + format!("Search within {} for {:?}", name, q) +} + +// Page listing repo artifacts by date +#[get("//artifacts")] +fn list_artifacts(name: &str) -> String { + format!("Artifacts for package {}", name) +} + +// Page for a specific artifact and status/logs +#[get("//artifacts/")] +fn artifact_detail(name: &str, id: u64) -> String { + format!("Artifact {} details for package {}", id, name) +} + +#[derive(Deserialize)] +#[serde(crate = "rocket::serde")] +struct NewRepo<'r> { + name: &'r str, +} + +// Create repo +#[post("/pkg", data = "")] +fn create_repo(repo: Json>) -> Result<(), &'static str> { + let path = DATA_DIR.join("repos").join(repo.name); + + if repo.name.is_empty() { + return Err("Repository name cannot be empty!"); + } + + if path.exists() { + tracing::info!( + "Attempt to create repository '{}' which already exists.", + repo.name + ); + return Err("This repository already exists!"); + } + + if let Err(e) = fs::create_dir_all(path) { + tracing::error!( + "Attempted to create package with name {} - Error: {e},", + repo.name + ); + return Err("Internal server error"); + } + + Ok(()) +} + +// Repo status/metadata +#[get("/pkg/")] +fn get_pkg(name: &str) -> Result, ApiError> { + let package = Package::load(name).map_err(|e| { + ApiError::NotFound(String::from("repo with name {name} does not exist")) + })?; + + Ok(Json((package.meta, package.config))) +} + +// Upload source tarball +#[post("/pkg//push", data = "")] +async fn push_tarball(name: &str, data: Data<'_>) -> Result<(), ApiError> { + let repo_dir = DATA_DIR.join("repos").join(name); + let tmp_path = repo_dir.join("upload.tar.gz"); + let stream = data + .open(256.mebibytes()) + .into_file(&tmp_path) + .await + .map_err(|e| ApiError::InternalServerError(()))?; + + if !stream.is_complete() { + return Err(ApiError::BadRequest("Incomplete upload".to_string())); + } + + // Unpack over the existing repo dir. + if repo_dir.exists() { + fs::remove_dir_all(&repo_dir).map_err(|e| ApiError::InternalServerError(()))?; + } + fs::create_dir_all(&repo_dir).map_err(|e| ApiError::InternalServerError(()))?; + + let _ = Package::unpack(&tmp_path)?; + fs::remove_file(&tmp_path).ok(); + Ok(()) +} + +// Download source tarball +#[get("/pkg//pull")] +fn pull_tarball( + name: &str, +) -> Result<(rocket::http::ContentType, Vec), Json> { + if let Ok(package) = Package::load(name) { + let tarball = package.tarball()?; + Ok(( + rocket::http::ContentType::new("application", "octet-stream"), + tarball, + )) + } else { + Err(Json(ApiError::NotFound(format!( + "repo with name {name} does not exist" + )))) + } +} + +// Download compiled binary +#[get("/pkg//artifact")] +fn download_artifact(name: &str) -> &'static str { + "Download artifact" +} + +#[launch] +fn rocket() -> _ { + dotenv().unwrap(); + + rocket::build() + .mount( + "/packages", + routes![ + search_packages, + package_main, + repo_file, + search_repo_files, + list_artifacts, + artifact_detail, + ], + ) + .mount( + "/api", + routes![ + create_repo, + get_pkg, + push_tarball, + pull_tarball, + download_artifact + ], + ) + .attach(Template::fairing()) + .mount("/static", FileServer::from("./static")) +} diff --git a/dsx_server/static/placeholder.jpg b/dsx_server/static/placeholder.jpg new file mode 100644 index 0000000..e562c5b Binary files /dev/null and b/dsx_server/static/placeholder.jpg differ diff --git a/dsx_server/templates/artifact_detail.html.tera b/dsx_server/templates/artifact_detail.html.tera new file mode 100644 index 0000000..d82a53c --- /dev/null +++ b/dsx_server/templates/artifact_detail.html.tera @@ -0,0 +1,360 @@ +{% extends "base" %} + +{% block title %}#{{ artifact.id }} · {{ package.name }}{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+ + + +
+

+ #{{ artifact.id }} + {% if artifact.status == "success" %} + passed + {% elif artifact.status == "failure" %} + failed + {% elif artifact.status == "running" %} + running + {% elif artifact.status == "pending" %} + queued + {% else %} + {{ artifact.status }} + {% endif %} +

+

+ {{ artifact.trigger | default(value="Triggered manually") }} + {% if artifact.commit_sha %} · {{ artifact.commit_sha }}{% endif %} + {% if artifact.branch %} on {{ artifact.branch }}{% endif %} +

+
+ +
+ + +
+ {% if artifact.steps and artifact.steps | length > 0 %} +

Steps

+
+ {% for step in artifact.steps %} +
+
+ + {% if step.status == "success" %} + + + + {% elif step.status == "failure" %} + + + + {% elif step.status == "running" %} + + + + {% elif step.status == "skipped" %} + + + + {% else %} + + + + {% endif %} + + {{ step.name }} + {% if step.duration %}{{ step.duration }}{% endif %} + +
+
+ {% if step.log_lines and step.log_lines | length > 0 %} + {% for line in step.log_lines %} +
+ {{ loop.index }} + {{ line.text | default(value=line) | escape }} +
+ {% endfor %} + {% elif step.log %} + {% for raw_line in step.log | split(pat="\n") %} +
+ {{ loop.index }} + {{ raw_line | escape }} +
+ {% endfor %} + {% else %} +
+ 1 + No output. +
+ {% endif %} +
+
+ {% endfor %} +
+ {% else %} +
+ No steps recorded for this artifact. +
+ {% endif %} + +
+ {% if artifact.prev_id %} + ← #{{ artifact.prev_id }} + {% endif %} + {% if artifact.next_id %} + #{{ artifact.next_id }} → + {% endif %} +
+
+ + + +
+ +
+ + +{% endblock %} diff --git a/dsx_server/templates/artifact_list.html.tera b/dsx_server/templates/artifact_list.html.tera new file mode 100644 index 0000000..e5666a2 --- /dev/null +++ b/dsx_server/templates/artifact_list.html.tera @@ -0,0 +1,189 @@ +{% extends "base" %} + +{% block title %}Artifacts · {{ package.name }}{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+ + + +
+

{{ package.name }} / artifacts

+
+ + {% if stats is defined %} +
+
+ {{ stats.total | default(value=0) }} + Total +
+
+ {{ stats.success | default(value=0) }} + Passed +
+
+ {{ stats.failure | default(value=0) }} + Failed +
+ {% if stats.avg_duration is defined %} +
+ {{ stats.avg_duration }} + Avg duration +
+ {% endif %} +
+ {% endif %} + + {% if artifacts and artifacts | length > 0 %} + + {% else %} +
+ +

No artifacts yet.

+
+ {% endif %} + +
+{% endblock %} diff --git a/dsx_server/templates/base.html.tera b/dsx_server/templates/base.html.tera new file mode 100644 index 0000000..4ef91f0 --- /dev/null +++ b/dsx_server/templates/base.html.tera @@ -0,0 +1,198 @@ + + + + + + {% block title %}Packages{% endblock %} · depot + + + + {% block extra_head %}{% endblock %} + + +
+ + + +
+ {% block content %}{% endblock %} +
+ +
+ + diff --git a/dsx_server/templates/components/file.html.tera b/dsx_server/templates/components/file.html.tera new file mode 100644 index 0000000..5af8741 --- /dev/null +++ b/dsx_server/templates/components/file.html.tera @@ -0,0 +1,16 @@ +{% macro file_view(package, file) %} +
+ + {% if file.is_dir %} + + {% else %} + + {% endif %} + + + {{ file.name }} + + +
+{% endmacro %} diff --git a/dsx_server/templates/components/file_tree.html.tera b/dsx_server/templates/components/file_tree.html.tera new file mode 100644 index 0000000..1757b06 --- /dev/null +++ b/dsx_server/templates/components/file_tree.html.tera @@ -0,0 +1,35 @@ + +
+
+
+ + {{ current_path | default(value="/") }} +
+ + {% if package.files and package.files | length > 0 %} +
+ {% if current_path %} + + + + + .. + + {% endif %} + + + + {% for file in package.files %} + {{ files::file_view(package = package.config.name, file = file) }} + {% endfor %} +
+ {% else %} +
No files found.
+ {% endif %} +
+
diff --git a/dsx_server/templates/components/sidebar.html.tera b/dsx_server/templates/components/sidebar.html.tera new file mode 100644 index 0000000..21e300c --- /dev/null +++ b/dsx_server/templates/components/sidebar.html.tera @@ -0,0 +1,81 @@ + + diff --git a/dsx_server/templates/package_home.html.tera b/dsx_server/templates/package_home.html.tera new file mode 100644 index 0000000..af982c6 --- /dev/null +++ b/dsx_server/templates/package_home.html.tera @@ -0,0 +1,204 @@ +{% extends "base" %} +{% import 'components/file' as files %} + +{% block title %}{{ package.config.name }}{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+ + + +
+

+ {{ package.config.name }} + {% if package.meta.latest_build_status == "success" %} + passing + {% elif package.meta.latest_build_status == "failure" %} + failing + {% elif package.meta.latest_build_status == "running" %} + running + {% elif package.meta.latest_build_status == "pending" %} + pending + {% endif %} +

+ {% if package.config.description %} +

{{ package.config.description }}

+ {% endif %} +
+ +
+ + + {% include 'components/file_tree' %} + + + {% include 'components/sidebar' %} +
+
+{% endblock %} diff --git a/dsx_server/templates/packages.html.tera b/dsx_server/templates/packages.html.tera new file mode 100644 index 0000000..83522d5 --- /dev/null +++ b/dsx_server/templates/packages.html.tera @@ -0,0 +1,174 @@ +{% extends "base" %} + +{% block title %}{% if query %}Search: {{ query }}{% else %}All Packages{% endif %}{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+
+ {% if query %} +

Results for "{{ query }}"

+ {% else %} +

Packages

+

{{ packages | length }} package{% if packages | length != 1 %}s{% endif %} available

+ {% endif %} +
+ + {% if query %} +
{{ packages | length }} result{% if packages | length != 1 %}s{% endif %}
+ {% endif %} + + {% if packages | length > 0 %} + + {% else %} +
+ + {% if query %} +

No packages match "{{ query }}"

+ {% else %} +

No packages yet.

+ {% endif %} +
+ {% endif %} +
+{% endblock %} diff --git a/dsx_server/templates/repo_path.html.tera b/dsx_server/templates/repo_path.html.tera new file mode 100644 index 0000000..a896316 --- /dev/null +++ b/dsx_server/templates/repo_path.html.tera @@ -0,0 +1,281 @@ +{% extends "base" %} + +{% block title %}{{ file.name }} · {{ package.name }}{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+ + + + +
+ {{ package.name }} + {% for segment in path_segments %} + / + {% if loop.last %} + {{ segment.name }} + {% else %} + {{ segment.name }} + {% endif %} + {% endfor %} +
+ + {% if file.is_dir %} + +
+ +
+
+ {% if file.parent_path is defined %} + + + + + .. + + {% endif %} + {% for entry in file.children %} +
+ + {% if entry.is_dir %} + + {% else %} + + {% endif %} + + + {{ entry.name }} + + {{ entry.last_commit_message | default(value="") }} + {{ entry.last_modified | default(value="") }} +
+ {% endfor %} +
+ + {% elif file.is_image %} + +
+ {{ file.size | default(value="unknown size") }} + raw ↗ +
+
+ {{ file.name }} +
+ + {% elif file.is_binary %} + +
+ {{ file.size | default(value="unknown size") }} · binary file + {% if file.raw_url %}download ↗{% endif %} +
+
+ +

Binary file — cannot be displayed.

+
+ + {% else %} + +
+ {{ file.name }} + + {{ file.line_count | default(value="?") }} lines + · + {{ file.size | default(value="") }} + {% if file.raw_url %}·raw ↗{% endif %} + +
+
+

+                {% if file.lines %}
+                {% for line in file.lines %}
+                
+                    
+                    
+                
+                {% endfor %}
+                {% else %}
+                
+                {% endif %}
+            
{{ loop.index }}{{ line | escape }}
{{ file.content | default(value="") | escape }}
+
+ {% endif %} + +
+{% endblock %} diff --git a/dsx_server/templates/repo_search.html.tera b/dsx_server/templates/repo_search.html.tera new file mode 100644 index 0000000..c9739d3 --- /dev/null +++ b/dsx_server/templates/repo_search.html.tera @@ -0,0 +1,161 @@ +{% extends "base" %} + +{% block title %}Search "{{ query }}" · {{ package.name }}{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} +
+ + + +
+

Search in {{ package.name }}

+
+ +
+ + +
+ + {% if query %} +
+ {% if results and results | length > 0 %} + {{ results | length }} file{% if results | length != 1 %}s{% endif %} matched "{{ query }}" + {% else %} + No results for "{{ query }}" + {% endif %} +
+ + {% if results and results | length > 0 %} +
+ {% for result in results %} +
+ + {% if result.matches and result.matches | length > 0 %} +
+ {% for match in result.matches %} +
+ {{ match.line_number }} + {{ match.text | escape }} +
+ {% endfor %} +
+ {% endif %} +
+ {% endfor %} +
+ {% else %} +
+ +

No files match "{{ query }}".

+
+ {% endif %} + {% endif %} + +
+{% endblock %}