diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index fc9df6eaed..d039e0f108 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -7,7 +7,7 @@ env: CARGO_HOME: /__w/hulk/cargo CARGO_TARGET_DIR: /__w/hulk/target CARGO_TERM_COLOR: always - NAOSDK_HOME: /__w/hulk/naosdk + HULK_DATA_HOME: /__w/hulk/data NAOSDK_AUTOMATIC_YES: 1 jobs: @@ -34,8 +34,9 @@ jobs: path: [ ., - tools/aliveness, - tools/hula, + services/aliveness, + services/breeze, + services/hula, ] runs-on: - self-hosted @@ -119,7 +120,34 @@ jobs: lfs: true - name: Build run: | - ./pepsi build --target ${{ matrix.target }} --profile ${{ matrix.profile }} + ./pepsi build --profile ${{ matrix.profile }} ${{ matrix.target }} + + build_services: + name: Build + strategy: + matrix: + path: + [ + services/aliveness, + services/breeze, + services/hula, + ] + runs-on: + - self-hosted + - v3 + container: + image: ghcr.io/hulks/hulk-ci:1.81.0 + options: --user=1000:1000 + steps: + - uses: actions/checkout@v4 + with: + lfs: true + - name: Build + run: | + ./pepsi sdk install + . $(./pepsi sdk path)/environment-setup-corei7-64-aldebaran-linux + cd ${{ matrix.path }} + cargo build --release build_tools: name: Build @@ -127,14 +155,12 @@ jobs: matrix: path: [ - aliveness, - annotato, - camera_matrix_extractor, - depp, - fanta, - hula, - pepsi, - twix, + tools/annotato, + tools/camera_matrix_extractor, + tools/depp, + tools/fanta, + tools/pepsi, + tools/twix, ] runs-on: - self-hosted @@ -148,5 +174,5 @@ jobs: lfs: true - name: Build run: | - cd tools/${{ matrix.path }} + cd ${{ matrix.path }} cargo build --release diff --git a/.gitignore b/.gitignore index 1ee9339eb9..d6862407d1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ /.wiki/ settings.json -/naosdk *.edited *~ diff --git a/Cargo.lock b/Cargo.lock index 91229fac93..ad5df3021c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -142,7 +142,7 @@ name = "aliveness" version = "0.1.0" dependencies = [ "futures-util", - "hula-types", + "hula_types", "regex", "serde", "serde_json", @@ -328,7 +328,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -520,7 +520,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -555,7 +555,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -700,7 +700,7 @@ dependencies = [ "flate2", "globset", "grep-cli", - "nu-ansi-term", + "nu-ansi-term 0.47.0", "once_cell", "path_abs", "plist", @@ -763,7 +763,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.86", + "syn 2.0.87", "which", ] @@ -786,7 +786,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.86", + "syn 2.0.87", "which", ] @@ -958,7 +958,7 @@ checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1066,9 +1066,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.33" +version = "1.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3788d6ac30243803df38a3e9991cf37e41210232916d41a8222ae378f912624" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" dependencies = [ "jobserver", "libc", @@ -1197,7 +1197,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1246,7 +1246,7 @@ dependencies = [ "proc-macro2", "quote", "source_analyzer", - "syn 2.0.86", + "syn 2.0.87", "thiserror", ] @@ -1415,15 +1415,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "constants" -version = "0.1.0" -dependencies = [ - "lazy_static", - "serde", - "toml", -] - [[package]] name = "content_inspector" version = "0.2.4" @@ -1440,7 +1431,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1683,7 +1674,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2046,7 +2037,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2067,7 +2058,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2079,7 +2070,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2100,7 +2091,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2111,7 +2102,7 @@ checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2354,7 +2345,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2446,7 +2437,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -2926,7 +2917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] -name = "hula-types" +name = "hula_types" version = "0.1.0" dependencies = [ "serde", @@ -3058,7 +3049,6 @@ dependencies = [ "chrono", "clap 4.5.20", "color-eyre", - "constants", "ctrlc", "enum-iterator", "fern", @@ -3873,7 +3863,7 @@ checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -3881,6 +3871,7 @@ name = "nao" version = "0.1.0" dependencies = [ "color-eyre", + "serde", "tokio", ] @@ -4035,6 +4026,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.47.0" @@ -4068,7 +4069,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -4118,7 +4119,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -4360,7 +4361,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -4469,6 +4470,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owned_ttf_parser" version = "0.25.0" @@ -4569,7 +4576,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -4580,7 +4587,7 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pepsi" -version = "3.17.0" +version = "4.0.0" dependencies = [ "aliveness", "argument_parsers", @@ -4588,10 +4595,11 @@ dependencies = [ "clap 4.5.20", "clap_complete", "color-eyre", - "constants", "futures-util", + "glob", "indicatif", "lazy_static", + "log", "nao", "opn", "regex", @@ -4600,9 +4608,12 @@ dependencies = [ "serde_json", "source_analyzer", "spl_network_messages", + "tempfile", "thiserror", "tokio", "toml", + "tracing", + "tracing-subscriber", ] [[package]] @@ -4731,7 +4742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -5012,11 +5023,11 @@ name = "repository" version = "0.1.0" dependencies = [ "color-eyre", - "constants", "futures-util", - "glob", "home", "itertools 0.10.5", + "log", + "nao", "parameters", "semver", "serde", @@ -5026,6 +5037,7 @@ dependencies = [ "tokio", "toml", "types", + "xdg", ] [[package]] @@ -5263,7 +5275,7 @@ source = "git+https://github.com/HULKs/serde.git?rev=73d5e8e404a874ad90023810d63 dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -5295,7 +5307,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -5524,7 +5536,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.86", + "syn 2.0.87", "thiserror", "threadbound", "toposort-scc", @@ -5655,9 +5667,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.86" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -5774,7 +5786,7 @@ checksum = "b08be0f17bd307950653ce45db00cd31200d82b624b36e181337d9c7d92765b5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -5901,7 +5913,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -6015,7 +6027,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -6038,15 +6050,29 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ + "nu-ansi-term 0.46.0", "sharded-slab", + "smallvec", "thread_local", "tracing-core", + "tracing-log", ] [[package]] @@ -6383,7 +6409,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-shared", ] @@ -6417,7 +6443,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6867,7 +6893,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -6889,7 +6915,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -7240,6 +7266,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + [[package]] name = "xdg-home" version = "1.3.0" @@ -7368,7 +7400,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 150bb29c10..80b1c193ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,6 @@ members = [ "crates/calibration", "crates/code_generation", "crates/communication", - "crates/constants", "crates/context_attribute", "crates/control", "crates/coordinate_systems", @@ -19,6 +18,7 @@ members = [ "crates/framework", "crates/geometry", "crates/hardware", + "crates/hula_types", "crates/hulk", "crates/hulk_behavior_simulator", "crates/hulk_imagine", @@ -50,12 +50,15 @@ members = [ "tools/camera_matrix_extractor", "tools/depp", "tools/fanta", - "tools/hula/types", "tools/pepsi", "tools/twix", ] -# HuLA and Aliveness are built independently by yocto -exclude = ["tools/aliveness", "tools/breeze", "tools/hula"] +# services are built independently by yocto +exclude = [ + "services/aliveness", + "services/breeze", + "services/hula" +] [workspace.package] version = "0.1.0" @@ -90,7 +93,6 @@ code_generation = { path = "crates/code_generation" } color-eyre = "0.6.2" communication = { path = "crates/communication" } compiled-nn = "0.12.0" -constants = { path = "crates/constants" } context_attribute = { path = "crates/context_attribute" } control = { path = "crates/control" } convert_case = "0.6.0" @@ -116,7 +118,7 @@ gilrs = "0.10.1" glob = "0.3.0" hardware = { path = "crates/hardware" } home = "0.5.4" -hula-types = { path = "tools/hula/types" } +hula_types = { path = "crates/hula_types" } hulk = { path = "crates/hulk" } hulk_manifest = { path = "crates/hulk_manifest" } i2cdev = "0.5.1" @@ -185,6 +187,8 @@ systemd = "0.10.0" tempfile = "3.3.0" thiserror = "1.0.37" threadbound = "0.1.6" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" tokio = { version = "1.21.2", features = ["full"] } tokio-tungstenite = "0.19.0" tokio-util = "0.7.4" @@ -198,6 +202,7 @@ walkdir = "2.3.2" walking_engine = { path = "crates/walking_engine" } watch = "0.2.3" webots = { version = "0.8.0" } +xdg = "2.5.2" zbus = { version = "3.7.0" } [patch.crates-io] diff --git a/crates/aliveness/Cargo.toml b/crates/aliveness/Cargo.toml index 29174aa074..0f83f588cd 100644 --- a/crates/aliveness/Cargo.toml +++ b/crates/aliveness/Cargo.toml @@ -7,7 +7,7 @@ homepage.workspace = true [dependencies] futures-util = { workspace = true } -hula-types = { path = "../../tools/hula/types/" } +hula_types = { workspace = true } regex = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/constants/Cargo.toml b/crates/constants/Cargo.toml deleted file mode 100644 index ada9adf374..0000000000 --- a/crates/constants/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "constants" -version.workspace = true -edition.workspace = true -license.workspace = true -homepage.workspace = true - -[dependencies] -lazy_static = {workspace = true} -serde = {workspace = true} -toml = {workspace = true} diff --git a/crates/constants/src/lib.rs b/crates/constants/src/lib.rs deleted file mode 100644 index bc30e777e9..0000000000 --- a/crates/constants/src/lib.rs +++ /dev/null @@ -1,32 +0,0 @@ -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; - -pub const HULA_DBUS_INTERFACE: &str = "org.hulks.hula"; -pub const HULA_DBUS_PATH: &str = "/org/hulks/HuLA"; -pub const HULA_DBUS_SERVICE: &str = "org.hulks.hula"; -pub const HULA_SOCKET_PATH: &str = "/tmp/hula"; -pub const OS_IS_NOT_LINUX: bool = !cfg!(target_os = "linux"); -pub const OS_RELEASE_PATH: &str = "/etc/os-release"; -pub const OS_VERSION: &str = "7.5.7"; -pub const SDK_VERSION: &str = "7.5.0"; - -#[derive(Serialize, Deserialize)] -pub struct Team { - pub team_number: u8, - pub naos: Vec, -} - -#[derive(Serialize, Deserialize)] -pub struct Nao { - pub number: u8, - pub hostname: String, - pub body_id: String, - pub head_id: String, -} - -lazy_static! { - pub static ref TEAM: Team = { - let content = include_str!("../../../etc/parameters/team.toml"); - toml::from_str(content).unwrap() - }; -} diff --git a/tools/hula/types/Cargo.toml b/crates/hula_types/Cargo.toml similarity index 89% rename from tools/hula/types/Cargo.toml rename to crates/hula_types/Cargo.toml index 7ea2e993e5..149facbf4b 100644 --- a/tools/hula/types/Cargo.toml +++ b/crates/hula_types/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "hula-types" +name = "hula_types" version = "0.1.0" edition.workspace = true license.workspace = true diff --git a/tools/hula/types/src/control_frame.rs b/crates/hula_types/src/control_frame.rs similarity index 100% rename from tools/hula/types/src/control_frame.rs rename to crates/hula_types/src/control_frame.rs diff --git a/tools/hula/types/src/lib.rs b/crates/hula_types/src/lib.rs similarity index 100% rename from tools/hula/types/src/lib.rs rename to crates/hula_types/src/lib.rs diff --git a/tools/hula/types/src/lola.rs b/crates/hula_types/src/lola.rs similarity index 100% rename from tools/hula/types/src/lola.rs rename to crates/hula_types/src/lola.rs diff --git a/tools/hula/types/src/robot_state.rs b/crates/hula_types/src/robot_state.rs similarity index 100% rename from tools/hula/types/src/robot_state.rs rename to crates/hula_types/src/robot_state.rs diff --git a/crates/hulk_nao/Cargo.toml b/crates/hulk_nao/Cargo.toml index c803c5de3e..52d0100fd8 100644 --- a/crates/hulk_nao/Cargo.toml +++ b/crates/hulk_nao/Cargo.toml @@ -11,7 +11,6 @@ ball_filter = { workspace = true } chrono = { workspace = true } clap = { workspace = true } color-eyre = { workspace = true } -constants = { workspace = true } ctrlc = { workspace = true } enum-iterator = { workspace = true } fern = { workspace = true } diff --git a/crates/hulk_nao/src/hula_wrapper.rs b/crates/hulk_nao/src/hula_wrapper.rs index 6b41446d1d..5f1c4e4de1 100644 --- a/crates/hulk_nao/src/hula_wrapper.rs +++ b/crates/hulk_nao/src/hula_wrapper.rs @@ -12,7 +12,8 @@ use super::{ double_buffered_reader::{DoubleBufferedReader, SelectPoller}, hula::{read_from_hula, write_to_hula, ControlStorage, StateStorage}, }; -use constants::HULA_SOCKET_PATH; + +pub const HULA_SOCKET_PATH: &str = "/tmp/hula"; pub struct HulaWrapper { now: RwLock, diff --git a/crates/nao/Cargo.toml b/crates/nao/Cargo.toml index 7c931bcdf8..ca5d977e1e 100644 --- a/crates/nao/Cargo.toml +++ b/crates/nao/Cargo.toml @@ -7,4 +7,5 @@ homepage.workspace = true [dependencies] color-eyre = { workspace = true } +serde = { workspace = true } tokio = { workspace = true } diff --git a/crates/nao/src/lib.rs b/crates/nao/src/lib.rs index bfeaabdfbe..db4f6e663b 100644 --- a/crates/nao/src/lib.rs +++ b/crates/nao/src/lib.rs @@ -3,36 +3,62 @@ use std::{ net::Ipv4Addr, path::Path, process::Stdio, + time::Duration, }; use color_eyre::{ - eyre::{bail, eyre, WrapErr}, + eyre::{self, bail, eyre, WrapErr}, Result, }; +use serde::Deserialize; use tokio::{ io::{AsyncBufReadExt, BufReader}, process::{Child, Command}, select, }; -pub const PING_TIMEOUT_SECONDS: u32 = 2; +const PING_TIMEOUT: Duration = Duration::from_secs(2); + +const NAO_SSH_FLAGS: &[&str] = &[ + "-lnao", + "-oLogLevel=quiet", + "-oStrictHostKeyChecking=no", + "-oUserKnownHostsFile=/dev/null", +]; + +#[derive(Debug, Deserialize, Hash, Eq, PartialEq)] +#[serde(try_from = "String")] +pub struct NaoNumber { + pub id: u8, +} + +impl TryFrom for NaoNumber { + type Error = eyre::Error; + + fn try_from(value: String) -> Result { + let id = value + .parse() + .wrap_err_with(|| format!("failed to parse `{value}` into Nao number"))?; + Ok(Self { id }) + } +} pub struct Nao { - pub host: Ipv4Addr, + pub address: Ipv4Addr, } impl Nao { - pub fn new(host: Ipv4Addr) -> Self { - Self { host } + pub fn new(address: Ipv4Addr) -> Self { + Self { address } } pub async fn try_new_with_ping(host: Ipv4Addr) -> Result { - Self::try_new_with_ping_and_arguments(host, PING_TIMEOUT_SECONDS).await + Self::try_new_with_ping_and_arguments(host, PING_TIMEOUT).await } pub async fn try_new_with_ping_and_arguments( host: Ipv4Addr, - timeout_seconds: u32, + timeout: Duration, ) -> Result { #[cfg(target_os = "macos")] const TIMEOUT_FLAG: &str = "-t"; @@ -43,7 +69,7 @@ impl Nao { .arg("-c") .arg("1") .arg(TIMEOUT_FLAG) - .arg(timeout_seconds.to_string()) + .arg(timeout.as_secs().to_string()) .arg(host.to_string()) .output() .await @@ -65,27 +91,18 @@ impl Nao { extract_version_number(&stdout).ok_or_else(|| eyre!("could not extract version number")) } - fn get_ssh_flags(&self) -> Vec { - vec![ - "-lnao".to_string(), - "-oLogLevel=quiet".to_string(), - "-oStrictHostKeyChecking=no".to_string(), - "-oUserKnownHostsFile=/dev/null".to_string(), - ] - } - fn ssh_to_nao(&self) -> Command { let mut command = Command::new("ssh"); - for flag in self.get_ssh_flags() { + for flag in NAO_SSH_FLAGS { command.arg(flag); } - command.arg(self.host.to_string()); + command.arg(self.address.to_string()); command } pub fn rsync_with_nao(&self, mkpath: bool) -> Command { let mut command = Command::new("rsync"); - let ssh_flags = self.get_ssh_flags().join(" "); + let ssh_flags = NAO_SSH_FLAGS.join(" "); command .stdout(Stdio::piped()) .arg("--recursive") @@ -183,7 +200,7 @@ impl Nao { let rsync = self .rsync_with_nao(true) .arg("--info=progress2") - .arg(format!("{}:hulk/logs/", self.host)) + .arg(format!("{}:hulk/logs/", self.address)) .arg(local_directory.as_ref().to_str().unwrap()) .spawn() .wrap_err("failed to execute rsync command")?; @@ -259,6 +276,7 @@ impl Nao { pub async fn upload( &self, local_directory: impl AsRef, + remote_directory: impl AsRef, delete_remaining: bool, progress_callback: impl Fn(&str), ) -> Result<()> { @@ -268,7 +286,11 @@ impl Nao { .arg("--copy-links") .arg("--info=progress2") .arg(format!("{}/", local_directory.as_ref().display())) - .arg(format!("{}:hulk/", self.host)); + .arg(format!( + "{}:{}/", + self.address, + remote_directory.as_ref().display() + )); if delete_remaining { command.arg("--delete").arg("--delete-excluded"); @@ -335,7 +357,7 @@ impl Nao { Ok(()) } - pub async fn set_network(&self, network: Network) -> Result<()> { + pub async fn set_wifi(&self, network: Network) -> Result<()> { let command_string = [ Network::SplA, Network::SplB, @@ -389,7 +411,7 @@ impl Nao { .arg("--copy-links") .arg("--info=progress2") .arg(image_path.as_ref().to_str().unwrap()) - .arg(format!("{}:/data/.image/", self.host)) + .arg(format!("{}:/data/.image/", self.address)) .spawn() .wrap_err("failed to execute rsync command")?; @@ -400,7 +422,7 @@ impl Nao { impl Display for Nao { fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(&self.host, formatter) + Display::fmt(&self.address, formatter) } } diff --git a/crates/parameters/src/directory.rs b/crates/parameters/src/directory.rs index 86568d9c74..e49fe73177 100644 --- a/crates/parameters/src/directory.rs +++ b/crates/parameters/src/directory.rs @@ -122,7 +122,7 @@ pub fn serialize( parameters: &Parameters, scope: Scope, path: &str, - parameters_root_path: impl AsRef, + parameters_root: impl AsRef, hardware_ids: &Ids, ) -> Result<(), DirectoryError> where @@ -131,7 +131,7 @@ where let mut parameters = to_value(parameters).map_err(DirectoryError::ParametersNotConvertedToJsonValue)?; let stored_parameters = to_value( - deserialize::(¶meters_root_path, hardware_ids, true).map_err(|error| { + deserialize::(¶meters_root, hardware_ids, true).map_err(|error| { println!("{:?}", error); error })?, @@ -143,7 +143,7 @@ where let Some(sparse_parameters_from_scope_path) = clone_nested_value(¶meters, path) else { return Ok(()); }; - let serialization_file_path = file_path_from_scope(scope, parameters_root_path, hardware_ids); + let serialization_file_path = file_path_from_scope(scope, parameters_root, hardware_ids); let mut parameters = if serialization_file_path.exists() { read_from_file(&serialization_file_path) .map_err(DirectoryError::HeadParametersOfLocationNotGet)? diff --git a/crates/repository/Cargo.toml b/crates/repository/Cargo.toml index f5e28cf0f0..4e5e11b4c6 100644 --- a/crates/repository/Cargo.toml +++ b/crates/repository/Cargo.toml @@ -7,11 +7,11 @@ homepage.workspace = true [dependencies] color-eyre = { workspace = true } -constants = { workspace = true } futures-util = { workspace = true } -glob = { workspace = true } home = { workspace = true } itertools = { workspace = true } +log = { workspace = true } +nao = { workspace = true } parameters = { workspace = true } semver = { workspace = true } serde = { workspace = true } @@ -21,3 +21,4 @@ tempfile = { workspace = true } tokio = { workspace = true } toml = { workspace = true } types = { workspace = true } +xdg = { workspace = true } diff --git a/crates/repository/src/cargo.rs b/crates/repository/src/cargo.rs new file mode 100644 index 0000000000..f1a1cc61d1 --- /dev/null +++ b/crates/repository/src/cargo.rs @@ -0,0 +1,218 @@ +use std::path::Path; + +use color_eyre::{ + eyre::{bail, Context, ContextCompat}, + Result, +}; +use log::info; +use tokio::process::Command; + +use crate::data_home::get_data_home; + +pub enum Executor { + Native, + Docker, +} + +pub struct Environment { + pub executor: Executor, + pub version: String, +} + +pub enum Cargo { + Native, + Sdk { environment: Environment }, +} + +impl Cargo { + pub fn native() -> Self { + Cargo::Native + } + + pub fn sdk(environment: Environment) -> Self { + Cargo::Sdk { environment } + } + + pub fn command<'a>( + self, + sub_command: &'a str, + repository_root: &'a Path, + ) -> Result> { + Ok(CargoCommand { + cargo: self, + sub_command, + repository_root, + manifest_path: None, + profile: None, + workspace: false, + all_features: false, + all_targets: false, + features: None, + passthrough_arguments: None, + }) + } +} + +pub struct CargoCommand<'a> { + cargo: Cargo, + sub_command: &'a str, + repository_root: &'a Path, + manifest_path: Option<&'a Path>, + profile: Option<&'a str>, + workspace: bool, + all_features: bool, + all_targets: bool, + features: Option<&'a [String]>, + passthrough_arguments: Option<&'a [String]>, +} + +impl<'a> CargoCommand<'a> { + pub fn manifest_path(&mut self, manifest_path: &'a Path) -> Result<()> { + if !manifest_path.is_relative() { + bail!("manifest path must be relative to repository root") + } + self.manifest_path = Some(manifest_path); + Ok(()) + } + + pub fn profile(&mut self, profile: &'a str) { + self.profile = Some(profile); + } + + pub fn workspace(&mut self) { + self.workspace = true; + } + + pub fn all_features(&mut self) { + self.all_features = true; + } + + pub fn all_targets(&mut self) { + self.all_targets = true; + } + + pub fn features(&mut self, features: &'a [String]) { + self.features = Some(features); + } + + pub fn passthrough_arguments(&mut self, passthrough_arguments: &'a [String]) { + self.passthrough_arguments = Some(passthrough_arguments); + } + + pub fn shell_command(self) -> Result { + let mut cargo_arguments = String::new(); + + if let Some(manifest_path) = self.manifest_path { + cargo_arguments.push_str(&format!( + "--manifest-path {path}", + path = manifest_path + .to_str() + .wrap_err("failed to convert manifest path to string")? + )); + } + if let Some(profile) = self.profile { + cargo_arguments.push_str(&format!(" --profile {profile}")); + } + if self.workspace { + cargo_arguments.push_str(" --workspace"); + } + if self.all_features { + cargo_arguments.push_str(" --all-features"); + } + if self.all_targets { + cargo_arguments.push_str(" --all-targets"); + } + if let Some(features) = self.features { + cargo_arguments.push_str(" --features "); + cargo_arguments.push_str(&features.join(",")); + } + if let Some(passthrough_arguments) = self.passthrough_arguments { + cargo_arguments.push_str(" -- "); + cargo_arguments.push_str(&passthrough_arguments.join(" ")); + } + + let shell_command = match self.cargo { + Cargo::Native => { + format!( + "cd {repository_root} && cargo {command} {cargo_arguments}", + repository_root = self + .repository_root + .to_str() + .wrap_err("failed to convert repository root to string")?, + command = self.sub_command, + ) + } + Cargo::Sdk { + environment: + Environment { + executor: Executor::Native, + version, + }, + } => { + let data_home = get_data_home().wrap_err("failed to get data home")?; + let environment_file = &data_home.join(format!( + "sdk/{version}/environment-setup-corei7-64-aldebaran-linux" + )); + let sdk_environment_setup = environment_file + .to_str() + .wrap_err("failed to convert sdk environment setup path to string")?; + let cargo_command = format!( + "cargo {command} {cargo_arguments}", + command = self.sub_command, + ); + format!( + "cd {repository_root} && . {sdk_environment_setup} && {cargo_command}", + repository_root = self + .repository_root + .to_str() + .wrap_err("failed to convert repository root to string")?, + ) + } + Cargo::Sdk { + environment: + Environment { + executor: Executor::Docker, + version, + }, + } => { + let data_home = get_data_home().wrap_err("failed to get data home")?; + let cargo_home = data_home.join("container-cargo-home/"); + format!("\ + mkdir -p {cargo_home} && + docker run \ + --volume={repository_root}:/hulk:z \ + --volume={cargo_home}:/naosdk/sysroots/corei7-64-aldebaran-linux/home/cargo:Z \ + --rm \ + --interactive \ + --tty ghcr.io/hulks/naosdk:{version} \ + /bin/bash -c \"\ + cd /hulk && \ + . /naosdk/environment-setup-corei7-64-aldebaran-linux && \ + cargo {command} {cargo_arguments}\ + \" + ", + repository_root=self.repository_root.to_str().wrap_err("failed to convert repository root to string")?, + cargo_home=cargo_home.to_str().wrap_err("failed to convert cargo home to string")?, + command=self.sub_command, + ) + } + }; + Ok(shell_command) + } +} + +pub async fn run_shell(command: &str) -> Result<()> { + info!("Executing command: `{command}`"); + + let status = Command::new("sh") + .arg("-c") + .arg(command) + .status() + .await + .wrap_err("failed to execute cargo command")?; + + if !status.success() { + bail!("cargo command exited with {status}"); + } + Ok(()) +} diff --git a/crates/repository/src/communication.rs b/crates/repository/src/communication.rs new file mode 100644 index 0000000000..173093da11 --- /dev/null +++ b/crates/repository/src/communication.rs @@ -0,0 +1,35 @@ +use std::path::Path; + +use color_eyre::{eyre::Context, Result}; +use serde_json::Value; + +use crate::modify_json::modify_json_inplace; + +pub async fn configure_communication( + enable: bool, + repository_root: impl AsRef, +) -> Result<()> { + let framework_json = repository_root + .as_ref() + .join("etc/parameters/framework.json"); + + let address = if enable { + Value::String("[::]:1337".to_string()) + } else { + Value::Null + }; + + modify_json_inplace(&framework_json, |mut hardware_json: Value| { + hardware_json["communication_addresses"] = address; + hardware_json + }) + .await + .wrap_err_with(|| { + format!( + "failed to configure communication address in {}", + framework_json.display() + ) + })?; + + Ok(()) +} diff --git a/crates/repository/src/configuration.rs b/crates/repository/src/configuration.rs new file mode 100644 index 0000000000..ebd15ea1d7 --- /dev/null +++ b/crates/repository/src/configuration.rs @@ -0,0 +1,32 @@ +use std::path::Path; + +use color_eyre::{eyre::Context, Result}; +use serde::Deserialize; +use tokio::fs::read_to_string; + +#[derive(Debug, Deserialize)] +pub struct Configuration { + pub os_version: String, + pub sdk_version: String, +} + +/// Get the OS version configured in the `hulk.toml`. +pub async fn read_os_version(repository_root: impl AsRef) -> Result { + let hulk = read_hulk_toml(repository_root).await?; + Ok(hulk.os_version) +} + +/// Get the SDK version configured in the `hulk.toml`. +pub async fn read_sdk_version(repository_root: impl AsRef) -> Result { + let hulk = read_hulk_toml(repository_root).await?; + Ok(hulk.sdk_version) +} + +pub async fn read_hulk_toml(repository_root: impl AsRef) -> Result { + let hulk_toml = repository_root.as_ref().join("hulk.toml"); + let hulk_toml = read_to_string(hulk_toml) + .await + .wrap_err("failed to read hulk.toml")?; + let hulk: Configuration = toml::from_str(&hulk_toml).wrap_err("failed to parse hulk.toml")?; + Ok(hulk) +} diff --git a/crates/repository/src/data_home.rs b/crates/repository/src/data_home.rs new file mode 100644 index 0000000000..e05c0fef34 --- /dev/null +++ b/crates/repository/src/data_home.rs @@ -0,0 +1,17 @@ +use std::{env, path::PathBuf}; + +use color_eyre::Result; + +/// Get the data home directory. +/// +/// This function returns the directory where hulk stores its data. The directory is determined by +/// the `HULK_DATA_HOME` environment variable. If the environment variable is not set, the +/// user-specific data directory (set by `XDG_DATA_HOME`) is used. +pub fn get_data_home() -> Result { + if let Ok(home) = env::var("HULK_DATA_HOME") { + return Ok(PathBuf::from(home)); + } + + let base_directories = xdg::BaseDirectories::with_prefix("hulk")?; + Ok(base_directories.get_data_home()) +} diff --git a/crates/repository/src/download.rs b/crates/repository/src/download.rs new file mode 100644 index 0000000000..c2a22e7c91 --- /dev/null +++ b/crates/repository/src/download.rs @@ -0,0 +1,45 @@ +use std::{ffi::OsStr, time::Duration}; + +use color_eyre::{ + eyre::{bail, Context}, + Result, +}; +use log::info; +use tokio::process::Command; + +pub const CONNECT_TIMEOUT: Duration = Duration::from_secs(5); + +/// Download a file from a list of URLs using `curl`. +/// +/// This function takes a list of URLs to download from, a path to the output file, +/// and a connection timeout duration. It tries to download the file from each URL +/// in the list until it succeeds or runs out of URLs. +pub async fn download_with_fallback( + urls: impl IntoIterator>, + output_path: impl AsRef, + connect_timeout: Duration, +) -> Result<()> { + for url in urls.into_iter() { + let url = url.as_ref(); + info!("Downloading from {url:?}"); + + let status = Command::new("curl") + .arg("--connect-timeout") + .arg(connect_timeout.as_secs_f32().to_string()) + .arg("--fail") + .arg("--location") + .arg("--progress-bar") + .arg("--output") + .arg(&output_path) + .arg(url) + .status() + .await + .wrap_err("failed to spawn command")?; + + if status.success() { + return Ok(()); + } + } + + bail!("failed to download from any URL"); +} diff --git a/crates/repository/src/find_root.rs b/crates/repository/src/find_root.rs new file mode 100644 index 0000000000..6d7c204193 --- /dev/null +++ b/crates/repository/src/find_root.rs @@ -0,0 +1,25 @@ +use std::{ + env::var_os, + path::{Path, PathBuf}, +}; + +/// Get the repository root directory. +/// +/// This function searches for the `hulk.toml` in the start directory and its ancestors. +/// If found, it returns the path to the directory containing the `hulk.toml`. If not, it falls +/// back to the HULK_DEFAULT_ROOT environment variable. +pub fn find_repository_root(start: impl AsRef) -> Option { + let ancestors = start.as_ref().ancestors(); + ancestors + .filter_map(|ancestor| std::fs::read_dir(ancestor).ok()) + .flatten() + .find_map(|entry| { + let entry = entry.ok()?; + if entry.file_name() == "hulk.toml" { + Some(entry.path().parent()?.to_path_buf()) + } else { + None + } + }) + .or_else(|| var_os("HULK_DEFAULT_ROOT").map(PathBuf::from)) +} diff --git a/crates/repository/src/image.rs b/crates/repository/src/image.rs new file mode 100644 index 0000000000..16350e8a7e --- /dev/null +++ b/crates/repository/src/image.rs @@ -0,0 +1,42 @@ +use std::path::{Path, PathBuf}; + +use color_eyre::{eyre::Context, Result}; +use tokio::fs::{create_dir_all, rename}; + +use crate::download::{download_with_fallback, CONNECT_TIMEOUT}; + +/// Downloads the NAO image for a specified version. +/// +/// This function ensures the NAO image is downloaded. If the image is already exists, it will +/// do nothing. If not, it will try to download it. +/// +/// Returns the path to the downloaded image. +pub async fn download_image(version: &str, data_home: impl AsRef) -> Result { + let data_home = data_home.as_ref(); + let downloads_directory = data_home.join("image/"); + let image_name = format!("nao-image-HULKs-OS-{version}.ext3.gz.opn"); + let image_path = downloads_directory.join(&image_name); + let download_path = image_path.with_extension("tmp"); + + if image_path.exists() { + return Ok(image_path); + } + + create_dir_all(&downloads_directory) + .await + .wrap_err("failed to create download directory")?; + + let urls = [ + format!("http://bighulk.hulks.dev/image/{image_name}"), + format!("https://github.com/HULKs/meta-nao/releases/download/{version}/{image_name}"), + ]; + download_with_fallback(urls, &download_path, CONNECT_TIMEOUT) + .await + .wrap_err("failed to download image")?; + + rename(download_path, &image_path) + .await + .wrap_err("failed to rename image")?; + + Ok(image_path) +} diff --git a/crates/repository/src/inspect_version.rs b/crates/repository/src/inspect_version.rs new file mode 100644 index 0000000000..f48b638296 --- /dev/null +++ b/crates/repository/src/inspect_version.rs @@ -0,0 +1,51 @@ +use std::{fs::read_to_string, path::Path}; + +use color_eyre::{eyre::Context, Result}; +use log::warn; +use semver::Version; +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +struct Cargo { + package: Package, +} + +#[derive(Deserialize, Debug)] +struct Package { + version: String, +} + +/// Inspects and returns the version of a package from its `Cargo.toml` file. +pub fn inspect_version(toml_path: impl AsRef) -> Result { + let toml_path = toml_path.as_ref(); + let cargo_toml_text = read_to_string(toml_path).wrap_err("failed to read file")?; + let cargo_toml: Cargo = toml::from_str(&cargo_toml_text).wrap_err("failed to parse content")?; + let raw_version = &cargo_toml.package.version; + let version = Version::parse(raw_version) + .wrap_err_with(|| format!("failed to parse version '{raw_version}' as SemVer"))?; + Ok(version) +} + +/// Checks whether the package has a newer version than the provided version. +pub fn check_for_update(own_version: &str, cargo_toml: impl AsRef) -> Result<()> { + let own_version = Version::parse(own_version) + .wrap_err_with(|| format!("failed to parse own version '{own_version}' as SemVer"))?; + let cargo_toml_version = inspect_version(&cargo_toml).wrap_err_with(|| { + format!( + "failed to inspect version of package at {}", + cargo_toml.as_ref().display() + ) + })?; + if own_version < cargo_toml_version { + let crate_path = cargo_toml.as_ref().parent().unwrap(); + warn!( + "New version available! + Own version: {own_version} + New version: {cargo_toml_version} + To install new version use: + cargo install --path {}", + crate_path.display() + ); + } + Ok(()) +} diff --git a/crates/repository/src/lib.rs b/crates/repository/src/lib.rs index 0459653e4d..52902670cc 100644 --- a/crates/repository/src/lib.rs +++ b/crates/repository/src/lib.rs @@ -1,707 +1,21 @@ -use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, - env::{self, consts::ARCH, current_dir}, - ffi::OsStr, - fmt::Display, - fs::Permissions, - io::{self, ErrorKind}, - os::unix::prelude::PermissionsExt, - path::{Path, PathBuf}, - time::Duration, -}; - -use color_eyre::{ - eyre::{bail, eyre, Context}, - Result, -}; -use futures_util::{stream::FuturesUnordered, StreamExt}; -use glob::glob; -use home::home_dir; -use itertools::intersperse; -use parameters::{ - directory::{serialize, Id, Location, Scope}, - json::nest_value_at_path, -}; -use semver::Version; -use serde::Deserialize; -use serde_json::{from_str, to_string_pretty, to_value, Value}; -use tempfile::{tempdir, TempDir}; -use tokio::{ - fs::{ - create_dir_all, read_dir, read_link, read_to_string, remove_dir_all, remove_file, rename, - set_permissions, symlink, try_exists, write, File, - }, - io::AsyncReadExt, - process::Command, -}; - -use constants::{Team, OS_IS_NOT_LINUX, SDK_VERSION}; -use spl_network_messages::PlayerNumber; -use types::hardware::Ids; - -const CONNECT_TIMEOUT: Duration = Duration::from_secs(5); - -#[derive(Clone)] -pub struct Repository { - root: PathBuf, -} - -impl Repository { - pub fn new(root: impl AsRef) -> Self { - Self { - root: root.as_ref().to_path_buf(), - } - } - - pub fn crates_directory(&self) -> PathBuf { - self.root.join("crates") - } - - pub fn parameters_root(&self) -> PathBuf { - self.root.join("etc/parameters") - } - - pub fn find_latest_file(&self, pattern: &str) -> Result { - let path = self.root.join(pattern); - let matching_paths: Vec<_> = glob( - path.to_str() - .ok_or_else(|| eyre!("failed to interpret path as Unicode"))?, - ) - .wrap_err("failed to execute glob() over target directory")? - .map(|entry| { - let path = entry.wrap_err("failed to get glob() entry")?; - let metadata = path - .metadata() - .wrap_err_with(|| format!("failed to get metadata of path {path:?}"))?; - let modified_time = metadata.modified().wrap_err_with(|| { - format!("failed to get modified time from metadata of path {path:?}") - })?; - Ok((path, modified_time)) - }) - .collect::>() - .wrap_err("failed to get matching paths")?; - let (path_with_maximal_modified_time, _modified_time) = matching_paths - .iter() - .max_by_key(|(_path, modified_time)| modified_time) - .ok_or_else(|| eyre!("failed to find any matching path"))?; - Ok(path_with_maximal_modified_time.to_path_buf()) - } - - pub fn check_new_version_available( - &self, - own_version: &str, - path: impl AsRef, - ) -> Result> { - #[derive(Deserialize, Debug)] - struct Cargo { - package: Package, - } - #[derive(Deserialize, Debug)] - struct Package { - version: String, - } - - let absolute_path = self.root.join(&path); - let own_version = Version::parse(own_version).wrap_err("failed to parse own version")?; - let cargo_toml_path = absolute_path.join("Cargo.toml"); - let cargo_toml_text = std::fs::read_to_string(&cargo_toml_path).wrap_err_with(|| { - format!( - "failed to load cargo toml at {}", - cargo_toml_path.to_str().unwrap() - ) - })?; - let cargo_toml: Cargo = toml::from_str(&cargo_toml_text).wrap_err_with(|| { - format!( - "failed to parse package version from {}", - cargo_toml_path.to_str().unwrap() - ) - })?; - let cargo_toml_version = Version::parse(&cargo_toml.package.version).unwrap(); - - if own_version < cargo_toml_version { - println!("New version available!"); - println!("Own version: {own_version}"); - println!("New version: {cargo_toml_version}"); - println!("To install new version use:"); - println!(); - println!(" cargo install --path {}", absolute_path.to_str().unwrap()); - println!(); - Ok(Some((own_version, cargo_toml_version))) - } else { - Ok(None) - } - } - - async fn cargo( - &self, - action: CargoAction, - workspace: bool, - profile: &str, - target: &str, - features: Option>, - passthrough_arguments: &[String], - ) -> Result<()> { - let use_docker = target == "nao" && OS_IS_NOT_LINUX; - - let cargo_command = format!("cargo {action} ") - + format!("--profile {profile} ").as_str() - + if let Some(features) = features { - let features = features.join(","); - format!("--features {features} ") - } else { - String::new() - } - .as_str() - + if workspace { - "--workspace --all-features --all-targets ".to_string() - } else { - let manifest = format!("crates/hulk_{target}/Cargo.toml"); - let root = if use_docker { - Path::new("/hulk") - } else { - &self.root - }; - format!("--manifest-path={} ", root.join(manifest).display()) - } - .as_str() - + "-- " - + match action { - CargoAction::Clippy => "--deny warnings ", - _ => "", - } - + passthrough_arguments.join(" ").as_str(); - - println!("Running: {cargo_command}"); - - let shell_command = if use_docker { - format!( - "docker run --volume={}:/hulk --volume={}:/naosdk/sysroots/corei7-64-aldebaran-linux/home/cargo \ - --rm --interactive --tty ghcr.io/hulks/naosdk:{SDK_VERSION} /bin/bash -c \ - '. /naosdk/environment-setup-corei7-64-aldebaran-linux && {cargo_command}'", - self.root.display(), - self.root.join("naosdk/cargo-home").join(SDK_VERSION).display() - ) - } else if target == "nao" { - format!( - ". {} && {cargo_command}", - self.root - .join(format!( - "naosdk/{SDK_VERSION}/environment-setup-corei7-64-aldebaran-linux" - )) - .display() - ) - } else { - cargo_command - }; - - let status = Command::new("sh") - .arg("-c") - .arg(shell_command) - .status() - .await - .wrap_err("failed to execute cargo command")?; - - if !status.success() { - bail!("cargo command exited with {status}"); - } - - Ok(()) - } - - pub async fn build( - &self, - workspace: bool, - profile: &str, - target: &str, - features: Option>, - passthrough_arguments: &[String], - ) -> Result<()> { - self.cargo( - CargoAction::Build, - workspace, - profile, - target, - features, - passthrough_arguments, - ) - .await - } - - pub async fn check(&self, workspace: bool, profile: &str, target: &str) -> Result<()> { - self.cargo(CargoAction::Check, workspace, profile, target, None, &[]) - .await - } - - pub async fn clippy(&self, workspace: bool, profile: &str, target: &str) -> Result<()> { - self.cargo(CargoAction::Clippy, workspace, profile, target, None, &[]) - .await - } - - pub async fn run( - &self, - profile: &str, - target: &str, - features: Option>, - passthrough_arguments: &[String], - ) -> Result<()> { - self.cargo( - CargoAction::Run, - false, - profile, - target, - features, - passthrough_arguments, - ) - .await - } - - pub async fn set_communication(&self, enable: bool) -> Result<()> { - let file_contents = read_to_string(self.root.join("etc/parameters/framework.json")) - .await - .wrap_err("failed to read framework.json")?; - let mut hardware_json: Value = - from_str(&file_contents).wrap_err("failed to deserialize framework.json")?; - - hardware_json["communication_addresses"] = if enable { - Value::String("[::]:1337".to_string()) - } else { - Value::Null - }; - { - let file_contents = to_string_pretty(&hardware_json) - .wrap_err("failed to serialize framework.json")? - + "\n"; - write( - self.root.join("etc/parameters/framework.json"), - file_contents.as_bytes(), - ) - .await - .wrap_err("failed to write framework.json")?; - } - Ok(()) - } - - pub async fn set_player_number( - &self, - head_id: &str, - player_number: PlayerNumber, - ) -> Result<()> { - let path = "player_number"; - let parameters = nest_value_at_path( - path, - to_value(player_number).wrap_err("failed to serialize player number")?, - ); - serialize( - ¶meters, - Scope { - location: Location::All, - id: Id::Head, - }, - path, - self.parameters_root(), - &Ids { - body_id: "unknown_body_id".to_string(), - head_id: head_id.to_string(), - }, - ) - .wrap_err("failed to serialize parameters directory") - } - - pub async fn set_recording_intervals( - &self, - recording_intervals: HashMap, - ) -> Result<()> { - let file_contents = read_to_string(self.root.join("etc/parameters/framework.json")) - .await - .wrap_err("failed to read framework.json")?; - let mut hardware_json: Value = - from_str(&file_contents).wrap_err("failed to deserialize framework.json")?; - - hardware_json["recording_intervals"] = to_value(recording_intervals) - .wrap_err("failed to convert recording intervals to JSON")?; - { - let file_contents = to_string_pretty(&hardware_json) - .wrap_err("failed to serialize framework.json")? - + "\n"; - write( - self.root.join("etc/parameters/framework.json"), - file_contents.as_bytes(), - ) - .await - .wrap_err("failed to write framework.json")?; - } - Ok(()) - } - - pub async fn link_sdk_home(&self, installation_directory: Option<&Path>) -> Result { - let symlink = self.root.join("naosdk"); - let environment_installation_directory = env::var("NAOSDK_HOME").ok().map(PathBuf::from); - let installation_directory = if let Some(directory) = - installation_directory.or(environment_installation_directory.as_deref()) - { - create_symlink(directory, &symlink).await?; - directory.to_path_buf() - } else if symlink.exists() { - symlink.clone() - } else { - let directory = home_dir() - .ok_or_else(|| eyre!("cannot find HOME directory"))? - .join(".naosdk"); - create_symlink(&directory, &symlink).await?; - directory - }; - - Ok(installation_directory) - } - - pub async fn install_sdk( - &self, - version: Option<&str>, - installation_directory: PathBuf, - ) -> Result<()> { - let version = version.unwrap_or(SDK_VERSION); - let sdk = installation_directory.join(version); - - let incomplete_marker = installation_directory.join(format!("{version}.incomplete")); - if sdk.exists() && incomplete_marker.exists() { - println!("Removing incomplete SDK directory..."); - remove_dir_all(&sdk) - .await - .wrap_err("failed to remove old SDK directory")?; - } - - if !sdk.exists() { - let downloads_directory = installation_directory.join("downloads"); - let installer_name = format!("HULKs-OS-{ARCH}-toolchain-{version}.sh"); - let installer_path = downloads_directory.join(&installer_name); - if !installer_path.exists() { - download_sdk(&downloads_directory, version, &installer_name) - .await - .wrap_err("failed to download SDK")?; - } - - File::create(&incomplete_marker) - .await - .wrap_err("failed to create marker")?; - install_sdk(installer_path, &sdk) - .await - .wrap_err("failed to install SDK")?; - remove_file(&incomplete_marker) - .await - .wrap_err("failed to remove marker")?; - } - Ok(()) - } - - pub async fn create_upload_directory(&self, profile: &str) -> Result<(TempDir, PathBuf)> { - let upload_directory = tempdir().wrap_err("failed to create temporary directory")?; - let hulk_directory = upload_directory.path().join("hulk"); - - // the target directory is "debug" with --profile dev... - let profile_directory = match profile { - "dev" => "debug", - other => other, - }; - - create_dir_all(hulk_directory.join("bin")) - .await - .wrap_err("failed to create directory")?; - - symlink(self.root.join("etc"), hulk_directory.join("etc")) - .await - .wrap_err("failed to link etc directory")?; - - symlink( - self.root.join(format!( - "target/x86_64-aldebaran-linux-gnu/{profile_directory}/hulk_nao" - )), - hulk_directory.join("bin/hulk"), - ) - .await - .wrap_err("failed to link executable")?; - - Ok((upload_directory, hulk_directory)) - } - - pub async fn get_configured_team(&self) -> Result { - let team_toml = self.root.join("etc/parameters/team.toml"); - let mut team_file = File::open(&team_toml) - .await - .wrap_err_with(|| format!("failed to open {}", team_toml.display()))?; - let mut contents = String::new(); - team_file.read_to_string(&mut contents).await?; - let team: Team = toml::from_str(&contents).wrap_err("failed to parse team.toml")?; - Ok(team) - } - - pub async fn get_configured_locations(&self) -> Result>> { - let results: Vec<_> = [ - "nao_location", - "webots_location", - "behavior_simulator_location", - ] - .into_iter() - .map(|target_name| async move { - ( - target_name, - read_link(self.parameters_root().join(target_name)) - .await - .wrap_err_with(|| format!("failed reading location symlink for {target_name}")), - ) - }) - .collect::>() - .collect() - .await; - - results - .into_iter() - .map(|(target_name, path)| match path { - Ok(path) => Ok(( - target_name.to_string(), - Some( - path.file_name() - .ok_or_else(|| eyre!("failed to get file name"))? - .to_str() - .ok_or_else(|| eyre!("failed to convert to UTF-8"))? - .to_string(), - ), - )), - Err(error) - if error.downcast_ref::().unwrap().kind() == ErrorKind::NotFound => - { - Ok((target_name.to_string(), None)) - } - Err(error) => Err(error), - }) - .collect() - } - - pub async fn set_location(&self, target: &str, location: &str) -> Result<()> { - let target_location = self.parameters_root().join(format!("{target}_location")); - let new_location = Path::new(location); - let new_location_path = self.parameters_root().join(location); - if !try_exists(new_location_path).await? { - let location_set = self.list_available_locations().await?; - let available_locations: String = intersperse( - location_set - .into_iter() - .map(|location| format!(" - {location}")), - "\n".to_string(), - ) - .collect(); - bail!("location {location} does not exist. \navailable locations are:\n{available_locations}"); - } - let _ = remove_file(&target_location).await; - symlink(&new_location, &target_location) - .await - .wrap_err_with(|| { - format!("failed creating symlink from {new_location:?} to {target_location:?}, does the location exist?" - ) - }) - } - - pub async fn list_available_locations(&self) -> Result> { - let parameters_path = self.root.join("etc/parameters"); - let mut locations = read_dir(parameters_path) - .await - .wrap_err("failed parameters root")?; - let mut results = BTreeSet::new(); - while let Ok(Some(entry)) = locations.next_entry().await { - if entry.path().is_dir() && !entry.path().is_symlink() { - results.insert( - entry - .path() - .file_name() - .ok_or_else(|| eyre!("failed getting file name for location"))? - .to_str() - .ok_or_else(|| eyre!("failed to convert to UTF-8"))? - .to_string(), - ); - } - } - Ok(results) - } -} - -async fn download_with_fallback( - output_path: impl AsRef, - urls: impl IntoIterator>, - connect_timeout: Duration, -) -> Result<()> { - for (i, url) in urls.into_iter().enumerate() { - let url = url.as_ref(); - if i > 0 { - println!("Falling back to downloading from {url:?}"); - } - - let status = Command::new("curl") - .arg("--connect-timeout") - .arg(connect_timeout.as_secs_f32().to_string()) - .arg("--fail") - .arg("--location") - .arg("--progress-bar") - .arg("--output") - .arg(&output_path) - .arg(url) - .status() - .await - .wrap_err("failed to spawn command")?; - - if status.success() { - return Ok(()); - } - } - - bail!("curl exited with error") -} - -async fn download_image( - downloads_directory: impl AsRef, - version: &str, - image_name: &str, -) -> Result<()> { - if !downloads_directory.as_ref().exists() { - create_dir_all(&downloads_directory) - .await - .wrap_err("failed to create download directory")?; - } - let image_path = downloads_directory.as_ref().join(image_name); - let download_path = image_path.with_extension("tmp"); - let urls = [ - format!("http://bighulk.hulks.dev/image/{image_name}"), - format!("https://github.com/HULKs/meta-nao/releases/download/{version}/{image_name}"), - ]; - - println!("Downloading image from {}", urls[0]); - download_with_fallback(&download_path, urls, CONNECT_TIMEOUT) - .await - .wrap_err("failed to download image")?; - - rename(download_path, image_path) - .await - .wrap_err("failed to rename image") -} - -pub async fn get_image_path(version: &str) -> Result { - let downloads_directory = home_dir() - .ok_or_else(|| eyre!("cannot find HOME directory"))? - .join(".naosdk/images"); - let image_name = format!("nao-image-HULKs-OS-{version}.ext3.gz.opn"); - let image_path = downloads_directory.join(&image_name); - - if !image_path.exists() { - download_image(downloads_directory, version, &image_name).await?; - } - Ok(image_path) -} - -async fn download_sdk( - downloads_directory: impl AsRef, - version: &str, - installer_name: &str, -) -> Result<()> { - if !downloads_directory.as_ref().exists() { - create_dir_all(&downloads_directory) - .await - .wrap_err("failed to create download directory")?; - } - let installer_path = downloads_directory.as_ref().join(installer_name); - let download_path = installer_path.with_extension("tmp"); - let urls = [ - format!("http://bighulk.hulks.dev/sdk/{installer_name}"), - format!("https://github.com/HULKs/meta-nao/releases/download/{version}/{installer_name}"), - ]; - - println!("Downloading SDK from {}", urls[0]); - download_with_fallback(&download_path, urls, CONNECT_TIMEOUT).await?; - - set_permissions(&download_path, Permissions::from_mode(0o755)) - .await - .wrap_err("failed to make installer executable")?; - - rename(download_path, installer_path) - .await - .wrap_err("failed to rename sdk installer") -} - -async fn install_sdk( - installer_path: impl AsRef, - installation_directory: impl AsRef, -) -> Result<()> { - let mut command = Command::new(installer_path.as_ref().as_os_str()); - command.arg("-d"); - command.arg(installation_directory.as_ref().as_os_str()); - if env::var("NAOSDK_AUTOMATIC_YES") - .map(|value| value == "1") - .unwrap_or(false) - { - command.arg("-y"); - } - let status = command.status().await.wrap_err("failed to spawn command")?; - - if !status.success() { - bail!("SDK installer exited with {status}"); - } - Ok(()) -} - -async fn create_symlink(source: &Path, destination: &Path) -> Result<()> { - if destination.read_link().is_ok() { - remove_file(&destination) - .await - .wrap_err("failed to remove current symlink")?; - } - symlink(&source, &destination) - .await - .wrap_err("failed to create symlink")?; - Ok(()) -} - -pub async fn get_repository_root() -> Result { - let path = current_dir().wrap_err("failed to get current directory")?; - let ancestors = path.as_path().ancestors(); - for ancestor in ancestors { - let mut directory = read_dir(ancestor) - .await - .wrap_err_with(|| format!("failed to read directory {}", ancestor.display()))?; - while let Some(child) = directory.next_entry().await.wrap_err_with(|| { - format!( - "failed to get next directory entry while iterating {}", - ancestor.display() - ) - })? { - if child.file_name() == ".git" { - return Ok(child - .path() - .parent() - .ok_or_else(|| eyre!("failed to get parent of {}", child.path().display()))? - .to_path_buf()); - } - } - } - - bail!("failed to find .git directory") -} - -#[derive(Debug, Clone, Copy)] -enum CargoAction { - Build, - Check, - Clippy, - Run, -} - -impl Display for CargoAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - CargoAction::Build => "build", - CargoAction::Check => "check", - CargoAction::Clippy => "clippy", - CargoAction::Run => "run", - } - ) - } -} +//! Tools and utilities for managing development workflows in the repository. +//! +//! This crate simplifies tasks like building for specific targets, handling SDKs, and setting up +//! configurations, making it easier to develop, configure, and deploy for NAO robots. + +pub mod cargo; +pub mod communication; +pub mod configuration; +pub mod data_home; +pub mod download; +pub mod find_root; +pub mod image; +pub mod inspect_version; +pub mod location; +pub mod modify_json; +pub mod player_number; +pub mod recording; +pub mod sdk; +pub mod symlink; +pub mod team; +pub mod upload; diff --git a/crates/repository/src/location.rs b/crates/repository/src/location.rs new file mode 100644 index 0000000000..4dec1c306a --- /dev/null +++ b/crates/repository/src/location.rs @@ -0,0 +1,113 @@ +use std::{io::ErrorKind, path::Path}; + +use color_eyre::{ + eyre::{bail, eyre, Context}, + Result, +}; +use futures_util::{stream::FuturesUnordered, StreamExt}; +use itertools::intersperse; +use tokio::{ + fs::{read_dir, read_link, remove_file, symlink, try_exists}, + io, +}; + +pub async fn list_configured_locations( + repository_root: impl AsRef, +) -> Result)>> { + let parameters_root = &repository_root.as_ref().join("etc/parameters"); + let results: Vec<_> = [ + "nao_location", + "webots_location", + "behavior_simulator_location", + ] + .into_iter() + .map(|target_name| async move { + ( + target_name, + read_link(parameters_root.join(target_name)) + .await + .wrap_err_with(|| format!("failed reading location symlink for {target_name}")), + ) + }) + .collect::>() + .collect() + .await; + + results + .into_iter() + .map(|(target_name, path)| match path { + Ok(path) => Ok(( + target_name.to_string(), + Some( + path.file_name() + .ok_or_else(|| eyre!("failed to get file name"))? + .to_str() + .ok_or_else(|| eyre!("failed to convert to UTF-8"))? + .to_string(), + ), + )), + Err(error) + if error.downcast_ref::().unwrap().kind() == ErrorKind::NotFound => + { + Ok((target_name.to_string(), None)) + } + Err(error) => Err(error), + }) + .collect() +} + +pub async fn set_location( + target: &str, + location: &str, + repository_root: impl AsRef, +) -> Result<()> { + let parameters_root = repository_root.as_ref().join("etc/parameters"); + if !try_exists(parameters_root.join(location)) + .await + .wrap_err_with(|| format!("failed checking if location '{location}' exists"))? + { + let location_set = list_available_locations(&repository_root) + .await + .unwrap_or_default(); + let available_locations: String = intersperse( + location_set + .into_iter() + .map(|location| format!(" - {location}")), + "\n".to_string(), + ) + .collect(); + bail!( + "location {location} does not exist.\navailable locations are:\n{available_locations}" + ); + } + let target_location = parameters_root.join(format!("{target}_location")); + let _ = remove_file(&target_location).await; + symlink(location, &target_location).await.wrap_err_with(|| { + format!( + "failed creating symlink named {target_location} pointing to {location}", + target_location = target_location.display() + ) + }) +} + +pub async fn list_available_locations(repository_root: impl AsRef) -> Result> { + let parameters_root = repository_root.as_ref().join("etc/parameters"); + let mut locations = read_dir(parameters_root) + .await + .wrap_err("failed to read parameters directory")?; + let mut results = Vec::new(); + while let Ok(Some(entry)) = locations.next_entry().await { + if entry.path().is_dir() && !entry.path().is_symlink() { + results.push( + entry + .path() + .file_name() + .ok_or_else(|| eyre!("failed getting file name for location"))? + .to_str() + .ok_or_else(|| eyre!("failed to convert to UTF-8"))? + .to_string(), + ); + } + } + Ok(results) +} diff --git a/crates/repository/src/modify_json.rs b/crates/repository/src/modify_json.rs new file mode 100644 index 0000000000..51a7a08918 --- /dev/null +++ b/crates/repository/src/modify_json.rs @@ -0,0 +1,36 @@ +use std::path::Path; + +use color_eyre::{eyre::Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::to_string_pretty; +use tokio::fs::{read_to_string, write}; + +/// Modifies a JSON file in place. +/// +/// This function reads the contents of a JSON file, deserializes it into a value, applies a +/// modification to the value, serializes the modified value back into JSON, and writes the JSON +/// back to the file. +pub async fn modify_json_inplace( + path: impl AsRef, + modification: impl FnOnce(I) -> O, +) -> Result<()> +where + for<'de> I: Deserialize<'de>, + O: Serialize, +{ + let file_contents = read_to_string(&path) + .await + .wrap_err("failed to read contents")?; + + let value = serde_json::from_str(&file_contents).wrap_err("failed to deserialize value")?; + + let out = modification(value); + + let json = to_string_pretty(&out).wrap_err("failed to serialize value")?; + let file_contents = json + "\n"; + write(&path, file_contents.as_bytes()) + .await + .wrap_err("failed to write file contents back")?; + + Ok(()) +} diff --git a/crates/repository/src/player_number.rs b/crates/repository/src/player_number.rs new file mode 100644 index 0000000000..805550fcc2 --- /dev/null +++ b/crates/repository/src/player_number.rs @@ -0,0 +1,37 @@ +use std::path::Path; + +use color_eyre::{eyre::Context, Result}; +use parameters::{ + directory::{serialize, Id, Location, Scope}, + json::nest_value_at_path, +}; +use spl_network_messages::PlayerNumber; +use types::hardware::Ids; + +pub async fn configure_player_number( + head_id: &str, + player_number: PlayerNumber, + repository_root: impl AsRef, +) -> Result<()> { + let parameters_root = repository_root.as_ref().join("etc/parameters/"); + let path = "player_number"; + let parameters = nest_value_at_path( + path, + serde_json::to_value(player_number).wrap_err("failed to serialize player number")?, + ); + serialize( + ¶meters, + Scope { + location: Location::All, + id: Id::Head, + }, + path, + parameters_root, + &Ids { + body_id: "unknown_body_id".to_string(), + head_id: head_id.to_string(), + }, + ) + .wrap_err("failed to serialize parameters directory")?; + Ok(()) +} diff --git a/crates/repository/src/recording.rs b/crates/repository/src/recording.rs new file mode 100644 index 0000000000..29fbcaecd9 --- /dev/null +++ b/crates/repository/src/recording.rs @@ -0,0 +1,31 @@ +use std::{collections::HashMap, path::Path}; + +use color_eyre::{eyre::Context, Result}; +use serde_json::{to_value, Value}; + +use crate::modify_json::modify_json_inplace; + +pub async fn configure_recording_intervals( + recording_intervals: HashMap, + repository_root: impl AsRef, +) -> Result<()> { + let framework_json = repository_root + .as_ref() + .join("etc/parameters/framework.json"); + let serialized_intervals = + to_value(recording_intervals).wrap_err("failed to convert recording intervals to JSON")?; + + modify_json_inplace(&framework_json, |mut hardware_json: Value| { + hardware_json["recording_intervals"] = serialized_intervals; + hardware_json + }) + .await + .wrap_err_with(|| { + format!( + "failed to configure recording intervals in {}", + framework_json.display() + ) + })?; + + Ok(()) +} diff --git a/crates/repository/src/sdk.rs b/crates/repository/src/sdk.rs new file mode 100644 index 0000000000..c6df339f17 --- /dev/null +++ b/crates/repository/src/sdk.rs @@ -0,0 +1,105 @@ +use std::{ + env::{self, consts::ARCH}, + fs::Permissions, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, +}; + +use color_eyre::{ + eyre::{bail, Context}, + Result, +}; +use log::info; +use tokio::{ + fs::{create_dir_all, remove_dir_all, remove_file, rename, set_permissions, File}, + process::Command, +}; + +use crate::download::{download_with_fallback, CONNECT_TIMEOUT}; + +/// Downloads and installs a specified SDK version. +/// +/// This function ensures an installed SDK version. If it is not installed, it will download the +/// SDK installer and run it to install the SDK. If the SDK installation is incomplete, it will +/// remove the incomplete installation and try again. +pub async fn download_and_install(version: &str, data_home: impl AsRef) -> Result<()> { + let data_home = data_home.as_ref(); + let sdk_home = data_home.join("sdk/"); + let installation_directory = sdk_home.join(version); + + let incomplete_marker = sdk_home.join(format!("{version}.incomplete")); + if installation_directory.exists() && incomplete_marker.exists() { + info!("Removing incomplete SDK ({version}) of previous installation attempt..."); + remove_dir_all(&installation_directory) + .await + .wrap_err("failed to remove incomplete SDK directory")?; + } + + if !installation_directory.exists() { + let installer_path = download(version, &sdk_home) + .await + .wrap_err("failed to download SDK")?; + + File::create(&incomplete_marker) + .await + .wrap_err("failed to create marker")?; + run_installer(installer_path, &installation_directory) + .await + .wrap_err("failed to install SDK")?; + remove_file(&incomplete_marker) + .await + .wrap_err("failed to remove marker")?; + } + Ok(()) +} + +async fn download(version: &str, sdk_home: impl AsRef) -> Result { + let downloads_directory = sdk_home.as_ref().join("downloads"); + let installer_name = format!("HULKs-OS-{ARCH}-toolchain-{version}.sh"); + let installer_path = downloads_directory.join(&installer_name); + let download_path = installer_path.with_extension("tmp"); + + create_dir_all(&downloads_directory) + .await + .wrap_err("failed to create download directory")?; + + let urls = [ + format!("http://bighulk.hulks.dev/sdk/{installer_name}"), + format!("https://github.com/HULKs/meta-nao/releases/download/{version}/{installer_name}"), + ]; + download_with_fallback(urls, &download_path, CONNECT_TIMEOUT) + .await + .wrap_err("failed to download SDK")?; + + set_permissions(&download_path, Permissions::from_mode(0o755)) + .await + .wrap_err("failed to mark installer executable")?; + + rename(download_path, &installer_path) + .await + .wrap_err("failed to rename sdk installer")?; + + Ok(installer_path) +} + +async fn run_installer( + installer: impl AsRef, + target_directory: impl AsRef, +) -> Result<()> { + let var_name = Command::new(installer.as_ref().as_os_str()); + let mut command = var_name; + command.arg("-d"); + command.arg(target_directory.as_ref().as_os_str()); + if env::var("NAOSDK_AUTOMATIC_YES") + .map(|value| value == "1") + .unwrap_or(false) + { + command.arg("-y"); + } + let status = command.status().await.wrap_err("failed to spawn command")?; + + if !status.success() { + bail!("SDK installer exited with {status}"); + } + Ok(()) +} diff --git a/crates/repository/src/symlink.rs b/crates/repository/src/symlink.rs new file mode 100644 index 0000000000..e515e03b07 --- /dev/null +++ b/crates/repository/src/symlink.rs @@ -0,0 +1,20 @@ +use std::path::Path; + +use color_eyre::{eyre::Context, Result}; +use tokio::fs::{remove_file, symlink}; + +/// Create or replace a symlink from the source path to the destination path. +/// +/// If a symlink already exists at the destination path, it is removed before creating the new symlink. +/// If the symlink cannot be created, an error is returned. +pub async fn create_symlink(source: &Path, destination: &Path) -> Result<()> { + if destination.read_link().is_ok() { + remove_file(&destination) + .await + .wrap_err("failed to remove current symlink")?; + } + symlink(&source, &destination) + .await + .wrap_err("failed to create symlink")?; + Ok(()) +} diff --git a/crates/repository/src/team.rs b/crates/repository/src/team.rs new file mode 100644 index 0000000000..b0972b5703 --- /dev/null +++ b/crates/repository/src/team.rs @@ -0,0 +1,30 @@ +use std::path::Path; + +use color_eyre::{eyre::Context, Result}; +use serde::{Deserialize, Serialize}; +use tokio::fs::read_to_string; + +#[derive(Serialize, Deserialize)] +pub struct Team { + pub team_number: u8, + pub naos: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct Nao { + pub number: u8, + pub hostname: String, + pub body_id: String, + pub head_id: String, +} + +pub async fn read_team_configuration(repository_root: impl AsRef) -> Result { + let team_toml = repository_root.as_ref().join("etc/parameters/team.toml"); + + let content = read_to_string(&team_toml) + .await + .wrap_err_with(|| format!("failed to read {}", team_toml.display()))?; + + let team = toml::from_str(&content).wrap_err("failed to parse team.toml")?; + Ok(team) +} diff --git a/crates/repository/src/upload.rs b/crates/repository/src/upload.rs new file mode 100644 index 0000000000..518216b77a --- /dev/null +++ b/crates/repository/src/upload.rs @@ -0,0 +1,37 @@ +use std::path::Path; + +use color_eyre::{eyre::Context, Result}; +use tokio::fs::{create_dir_all, symlink}; + +pub async fn populate_upload_directory( + upload_directory: impl AsRef, + profile: &str, + repository_root: impl AsRef, +) -> Result<()> { + let upload_directory = upload_directory.as_ref(); + let repository_root = repository_root.as_ref(); + + // the target directory is "debug" with --profile dev... + let profile_directory = match profile { + "dev" => "debug", + other => other, + }; + + symlink(repository_root.join("etc"), upload_directory.join("etc")) + .await + .wrap_err("failed to link etc directory")?; + + create_dir_all(upload_directory.join("bin")) + .await + .wrap_err("failed to create directory for binaries")?; + + let hulk_binary = format!("target/x86_64-aldebaran-linux-gnu/{profile_directory}/hulk_nao"); + symlink( + repository_root.join(hulk_binary), + upload_directory.join("bin/hulk"), + ) + .await + .wrap_err("failed to link executable")?; + + Ok(()) +} diff --git a/crates/source_analyzer/src/pretty.rs b/crates/source_analyzer/src/pretty.rs index 2028e70aaf..3617982cb6 100644 --- a/crates/source_analyzer/src/pretty.rs +++ b/crates/source_analyzer/src/pretty.rs @@ -90,7 +90,7 @@ impl ToWriterPretty for Contexts { impl ToWriterPretty for Field { fn to_writer_pretty(&self, writer: &mut impl Write) -> fmt::Result { match self { - Field::AdditionalOutput { name, .. } => write!(writer, "{name}: AdditfmtnalOutput"), + Field::AdditionalOutput { name, .. } => write!(writer, "{name}: AdditionalOutput"), Field::CyclerState { name, .. } => write!(writer, "{name}: CyclerState"), Field::HardwareInterface { name, .. } => write!(writer, "{name}: HardwareInterface"), Field::HistoricInput { name, .. } => write!(writer, "{name}: HistoricInput"), diff --git a/docs/tooling/pepsi.md b/docs/tooling/pepsi.md index 968ff2370e..f5cd96d134 100644 --- a/docs/tooling/pepsi.md +++ b/docs/tooling/pepsi.md @@ -42,7 +42,7 @@ Many subcommands can act on multiple robots concurrently. `upload` builds a binary for the NAO target, and then uploads it and parameter files to one or more robot. -`wireless`, `reboot`, `poweroff`, and `hulk` directly interact with the robot(s), whereas `communication`, and `playernumber` only change the local configuration parameters. +`wifi`, `reboot`, `poweroff`, and `hulk` directly interact with the robot(s), whereas `communication`, and `playernumber` only change the local configuration parameters. `pregame` combines deactivating communication (to avoid sending illegal messages), assigning playernumbers, setting a wifi network, uploading, and restarting the HULK service. diff --git a/hulk.toml b/hulk.toml new file mode 100644 index 0000000000..1bf7376892 --- /dev/null +++ b/hulk.toml @@ -0,0 +1,2 @@ +os_version = "7.5.7" +sdk_version = "7.5.0" diff --git a/tools/aliveness/Cargo.lock b/services/aliveness/Cargo.lock similarity index 97% rename from tools/aliveness/Cargo.lock rename to services/aliveness/Cargo.lock index 58520e8560..d76803eed6 100644 --- a/tools/aliveness/Cargo.lock +++ b/services/aliveness/Cargo.lock @@ -31,7 +31,7 @@ name = "aliveness" version = "0.1.0" dependencies = [ "futures-util", - "hula-types", + "hula_types", "regex", "serde", "serde_json", @@ -47,11 +47,10 @@ dependencies = [ "aliveness", "color-eyre", "configparser", - "constants", "env_logger", "futures-util", "hostname", - "hula-types", + "hula_types", "log", "serde_json", "tokio", @@ -361,15 +360,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "constants" -version = "0.1.0" -dependencies = [ - "lazy_static", - "serde", - "toml", -] - [[package]] name = "cpufeatures" version = "0.2.14" @@ -672,7 +662,7 @@ dependencies = [ ] [[package]] -name = "hula-types" +name = "hula_types" version = "0.1.0" dependencies = [ "serde", @@ -971,7 +961,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit 0.19.15", + "toml_edit", ] [[package]] @@ -1148,15 +1138,6 @@ dependencies = [ "syn 2.0.86", ] -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - [[package]] name = "sha1" version = "0.10.6" @@ -1350,26 +1331,11 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.22", -] - [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -1379,20 +1345,7 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.6.20", + "winnow", ] [[package]] @@ -1683,15 +1636,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] - [[package]] name = "xdg-home" version = "1.3.0" diff --git a/tools/aliveness/Cargo.toml b/services/aliveness/Cargo.toml similarity index 85% rename from tools/aliveness/Cargo.toml rename to services/aliveness/Cargo.toml index 700c019ad2..f10b161396 100644 --- a/tools/aliveness/Cargo.toml +++ b/services/aliveness/Cargo.toml @@ -9,11 +9,10 @@ homepage = "https://github.com/hulks/hulk" aliveness = { path = "../../crates/aliveness" } color-eyre = "0.6.2" configparser = { version = "3.0.2", features = ["async-std"] } -constants = { path = "../../crates/constants" } env_logger = "0.10.0" futures-util = "0.3.25" hostname = "0.3.1" -hula-types = { path = "../hula/types/" } +hula_types = { path = "../../crates/hula_types/" } log = "0.4.17" serde_json = "1.0.89" tokio = { version = "1.22.0", features = ["full"] } diff --git a/tools/aliveness/src/main.rs b/services/aliveness/src/main.rs similarity index 100% rename from tools/aliveness/src/main.rs rename to services/aliveness/src/main.rs diff --git a/tools/aliveness/src/robot_info.rs b/services/aliveness/src/robot_info.rs similarity index 98% rename from tools/aliveness/src/robot_info.rs rename to services/aliveness/src/robot_info.rs index 3636910e8e..a8652cab5a 100644 --- a/tools/aliveness/src/robot_info.rs +++ b/services/aliveness/src/robot_info.rs @@ -1,11 +1,12 @@ use color_eyre::eyre::{eyre, Context, Result}; use configparser::ini::Ini; -use constants::OS_RELEASE_PATH; use hula_types::{Battery, JointsArray}; use tokio::process::Command; use zbus::{dbus_proxy, zvariant::Optional, Connection}; +const OS_RELEASE_PATH: &str = "/etc/os-release"; + #[dbus_proxy( default_service = "org.hulks.hula", interface = "org.hulks.hula", diff --git a/tools/breeze/Cargo.lock b/services/breeze/Cargo.lock similarity index 100% rename from tools/breeze/Cargo.lock rename to services/breeze/Cargo.lock diff --git a/tools/breeze/Cargo.toml b/services/breeze/Cargo.toml similarity index 100% rename from tools/breeze/Cargo.toml rename to services/breeze/Cargo.toml diff --git a/tools/breeze/src/main.rs b/services/breeze/src/main.rs similarity index 100% rename from tools/breeze/src/main.rs rename to services/breeze/src/main.rs diff --git a/tools/hula/Cargo.lock b/services/hula/Cargo.lock similarity index 92% rename from tools/hula/Cargo.lock rename to services/hula/Cargo.lock index b6ff82e6a4..515ea03461 100644 --- a/tools/hula/Cargo.lock +++ b/services/hula/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.17" @@ -206,7 +221,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -241,7 +256,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -311,6 +326,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1522ac6ee801a11bf9ef3f80403f4ede6eb41291fac3dde3de09989679305f25" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -319,9 +340,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.33" +version = "1.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3788d6ac30243803df38a3e9991cf37e41210232916d41a8222ae378f912624" +checksum = "67b9470d453346108f93a59222a9a1a5724db32d0a4727b7ab7ace4b4d822dc9" dependencies = [ "shlex", ] @@ -332,6 +353,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "clap" version = "4.5.20" @@ -363,7 +398,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -415,13 +450,10 @@ dependencies = [ ] [[package]] -name = "constants" -version = "0.1.0" -dependencies = [ - "lazy_static", - "serde", - "toml", -] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" @@ -497,7 +529,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -620,7 +652,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -758,20 +790,21 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" name = "hula" version = "0.5.0" dependencies = [ + "chrono", "clap", "color-eyre", - "constants", "env_logger", "epoll", - "hula-types", + "hula_types", "log", "rmp-serde", + "serde", "systemd", "zbus", ] [[package]] -name = "hula-types" +name = "hula_types" version = "0.1.0" dependencies = [ "serde", @@ -784,6 +817,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indenter" version = "0.3.3" @@ -837,6 +893,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1051,7 +1116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit 0.19.15", + "toml_edit", ] [[package]] @@ -1203,7 +1268,7 @@ checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1214,16 +1279,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", + "syn 2.0.87", ] [[package]] @@ -1305,9 +1361,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.86" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -1361,26 +1417,11 @@ dependencies = [ "once_cell", ] -[[package]] -name = "toml" -version = "0.8.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.22", -] - [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] [[package]] name = "toml_edit" @@ -1390,20 +1431,7 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.6.20", + "winnow", ] [[package]] @@ -1425,7 +1453,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] @@ -1518,6 +1546,61 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + [[package]] name = "winapi" version = "0.3.9" @@ -1549,6 +1632,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1706,15 +1798,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] - [[package]] name = "xdg-home" version = "1.3.0" @@ -1809,7 +1892,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.86", + "syn 2.0.87", ] [[package]] diff --git a/tools/hula/Cargo.toml b/services/hula/Cargo.toml similarity index 64% rename from tools/hula/Cargo.toml rename to services/hula/Cargo.toml index 9e55548879..dce66a0654 100644 --- a/tools/hula/Cargo.toml +++ b/services/hula/Cargo.toml @@ -1,23 +1,17 @@ -[workspace] -resolver = "2" -members = [ - "types", - "proxy", -] - -[workspace.package] +[package] +name = "hula" +version = "0.5.0" edition = "2021" license = "GPL-3.0-only" homepage = "https://github.com/hulks/hulk" -[workspace.dependencies] +[dependencies] chrono = "0.4.23" clap = { version = "4.0.29", features = ["derive"] } color-eyre = "0.6.2" -constants = { path = "../../crates/constants" } env_logger = "0.10.0" epoll = "4.3.1" -hula-types = { path = "./types" } +hula_types = { path = "../../crates/hula_types/" } log = "0.4.17" rmp-serde = "1.1.1" serde = { version = "1.0.149", features = ["derive"] } diff --git a/tools/hula/proxy/src/dbus.rs b/services/hula/src/dbus.rs similarity index 93% rename from tools/hula/proxy/src/dbus.rs rename to services/hula/src/dbus.rs index 9244acfb26..50accef164 100644 --- a/tools/hula/proxy/src/dbus.rs +++ b/services/hula/src/dbus.rs @@ -8,7 +8,9 @@ use zbus::{ }; use crate::SharedState; -use constants::{HULA_DBUS_PATH, HULA_DBUS_SERVICE}; + +const HULA_DBUS_SERVICE: &str = "org.hulks.hula"; +const HULA_DBUS_PATH: &str = "/org/hulks/HuLA"; struct RobotInfo { shared_state: Arc>, diff --git a/tools/hula/proxy/src/idle.rs b/services/hula/src/idle.rs similarity index 100% rename from tools/hula/proxy/src/idle.rs rename to services/hula/src/idle.rs diff --git a/tools/hula/proxy/src/main.rs b/services/hula/src/main.rs similarity index 100% rename from tools/hula/proxy/src/main.rs rename to services/hula/src/main.rs diff --git a/tools/hula/proxy/src/proxy.rs b/services/hula/src/proxy.rs similarity index 99% rename from tools/hula/proxy/src/proxy.rs rename to services/hula/src/proxy.rs index f35e6cf90b..3d72433fe5 100644 --- a/tools/hula/proxy/src/proxy.rs +++ b/services/hula/src/proxy.rs @@ -25,8 +25,8 @@ use crate::{ idle::{charging_skull, send_idle}, SharedState, }; -use constants::HULA_SOCKET_PATH; +const HULA_SOCKET_PATH: &str = "/tmp/hula"; const LOLA_SOCKET_PATH: &str = "/tmp/robocup"; const LOLA_SOCKET_RETRY_COUNT: usize = 60; const LOLA_SOCKET_RETRY_INTERVAL: Duration = Duration::from_secs(1); diff --git a/tools/hula/proxy/Cargo.toml b/tools/hula/proxy/Cargo.toml deleted file mode 100644 index f3827c9585..0000000000 --- a/tools/hula/proxy/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "hula" -version = "0.5.0" -edition.workspace = true -license.workspace = true -homepage.workspace = true - -[dependencies] -clap = { workspace = true } -color-eyre = { workspace = true } -constants = { workspace = true } -env_logger = { workspace = true } -epoll = { workspace = true } -hula-types = { workspace = true } -log = { workspace = true } -rmp-serde = { workspace = true } -systemd = { workspace = true } -zbus = { workspace = true } diff --git a/tools/pepsi/Cargo.toml b/tools/pepsi/Cargo.toml index 8c5aeea021..89d56c3a5a 100644 --- a/tools/pepsi/Cargo.toml +++ b/tools/pepsi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pepsi" -version = "3.17.0" +version = "4.0.0" edition.workspace = true license.workspace = true homepage.workspace = true @@ -12,10 +12,11 @@ bat = { workspace = true } clap = { workspace = true } clap_complete = { workspace = true } color-eyre = { workspace = true } -constants = { workspace = true } futures-util = { workspace = true } +glob = { workspace = true } indicatif = { workspace = true } lazy_static = { workspace = true } +log = { workspace = true } nao = { workspace = true } opn = { workspace = true } regex = { workspace = true } @@ -24,6 +25,9 @@ serde = { workspace = true } serde_json = { workspace = true } source_analyzer = { workspace = true } spl_network_messages = { workspace = true } +tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } toml = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/tools/pepsi/src/aliveness.rs b/tools/pepsi/src/aliveness.rs index f0b2a9f7dc..93f1d51e27 100644 --- a/tools/pepsi/src/aliveness.rs +++ b/tools/pepsi/src/aliveness.rs @@ -1,7 +1,12 @@ -use std::{collections::BTreeMap, net::IpAddr, num::ParseIntError, time::Duration}; +use std::{collections::BTreeMap, net::IpAddr, num::ParseIntError, path::Path, time::Duration}; use clap::{arg, Args}; -use color_eyre::owo_colors::{OwoColorize, Style}; +use color_eyre::{ + eyre::Context, + owo_colors::{OwoColorize, Style}, + Result, +}; +use log::error; use aliveness::{ query_aliveness, @@ -9,15 +14,15 @@ use aliveness::{ AlivenessError, AlivenessState, Battery, JointsArray, }; use argument_parsers::NaoAddress; -use constants::OS_VERSION; +use repository::configuration::read_os_version; #[derive(Args)] pub struct Arguments { /// Output verbose version of the aliveness information - #[arg(long, short = 'v')] + #[arg(long, short = 'v', conflicts_with = "json")] verbose: bool, /// Output aliveness information as json - #[arg(long, short = 'j')] + #[arg(long, short = 'j', conflicts_with = "verbose")] json: bool, /// Timeout in ms for waiting for responses #[arg(long, short = 't', value_parser = parse_duration, default_value = "200")] @@ -33,27 +38,34 @@ fn parse_duration(arg: &str) -> Result { type AlivenessList = BTreeMap; -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("failed to query aliveness")] - QueryFailed(AlivenessError), - #[error("failed to serialize data")] - SerializeFailed(serde_json::Error), -} - -pub async fn aliveness(arguments: Arguments) -> Result<(), Error> { +pub async fn aliveness( + arguments: Arguments, + repository_root: Result>, +) -> Result<()> { let states = query_aliveness_list(&arguments) .await - .map_err(Error::QueryFailed)?; + .wrap_err("failed to query aliveness")?; if arguments.json { - println!( - "{}", - serde_json::to_string(&states).map_err(Error::SerializeFailed)? - ); + let json = serde_json::to_string(&states).wrap_err("failed to serialize aliveness")?; + println!("{json}"); } else if arguments.verbose { print_verbose(&states); } else { - print_summary(&states); + let expected_os_version = match repository_root { + Ok(repository_root) => match read_os_version(repository_root).await { + Ok(version) => Some(version), + Err(error) => { + error!("{error:#?}"); + None + } + }, + Err(error) => { + error!("{error:#?}"); + None + } + }; + + print_summary(&states, expected_os_version); } Ok(()) } @@ -136,8 +148,8 @@ impl SummaryElements { } } - fn append_os_version(&mut self, version: &str) { - if version != OS_VERSION { + fn append_os_version(&mut self, version: &str, expected_os_version: &str) { + if version != expected_os_version { self.append(OS_ICON, version, Style::new()); } } @@ -153,7 +165,7 @@ impl SummaryElements { } } -fn print_summary(states: &AlivenessList) { +fn print_summary(states: &AlivenessList, expected_os_version: Option) { for (ip, state) in states.iter() { let id = match ip { IpAddr::V4(ip) => ip.octets()[3], @@ -164,7 +176,9 @@ fn print_summary(states: &AlivenessList) { output.append_battery(&state.battery); output.append_temperature(&state.temperature); - output.append_os_version(&state.hulks_os_version); + if let Some(expected_os_version) = &expected_os_version { + output.append_os_version(&state.hulks_os_version, expected_os_version); + } let SystemServices { hal, hula, diff --git a/tools/pepsi/src/analyze.rs b/tools/pepsi/src/analyze.rs index d85cc06b15..bdaa37351d 100644 --- a/tools/pepsi/src/analyze.rs +++ b/tools/pepsi/src/analyze.rs @@ -1,12 +1,41 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use bat::{PagingMode, PrettyPrinter}; use clap::Subcommand; -use color_eyre::{eyre::WrapErr, Result}; +use color_eyre::{ + eyre::{eyre, WrapErr}, + Result, +}; -use repository::Repository; use source_analyzer::{contexts::Contexts, node::parse_rust_file, pretty::to_string_pretty}; +fn find_latest_file(path_pattern: impl AsRef) -> Result { + let matching_paths: Vec<_> = glob::glob( + path_pattern + .as_ref() + .to_str() + .ok_or_else(|| eyre!("failed to interpret path as Unicode"))?, + ) + .wrap_err("failed to execute glob() over target directory")? + .map(|entry| { + let path = entry.wrap_err("failed to get glob() entry")?; + let metadata = path + .metadata() + .wrap_err_with(|| format!("failed to get metadata of path {path:?}"))?; + let modified_time = metadata.modified().wrap_err_with(|| { + format!("failed to get modified time from metadata of path {path:?}") + })?; + Ok((path, modified_time)) + }) + .collect::>() + .wrap_err("failed to get matching paths")?; + let (path_with_maximal_modified_time, _modified_time) = matching_paths + .iter() + .max_by_key(|(_path, modified_time)| modified_time) + .ok_or_else(|| eyre!("failed to find any matching path"))?; + Ok(path_with_maximal_modified_time.to_path_buf()) +} + #[derive(Subcommand)] #[allow(clippy::enum_variant_names)] pub enum Arguments { @@ -20,7 +49,10 @@ pub enum Arguments { }, } -pub async fn analyze(arguments: Arguments, repository: &Repository) -> Result<()> { +pub async fn analyze( + arguments: Arguments, + repository_root: Result>, +) -> Result<()> { match arguments { Arguments::DumpContexts { file_path } => { let file = parse_rust_file(file_path).wrap_err("failed to parse rust file")?; @@ -30,10 +62,10 @@ pub async fn analyze(arguments: Arguments, repository: &Repository) -> Result<() print!("{string}"); } Arguments::DumpLatest { file_name } => { + let repository_root = repository_root?; let glob = format!("target/**/{file_name}"); println!("{glob}"); - let file_path = repository - .find_latest_file(&glob) + let file_path = find_latest_file(repository_root.as_ref().join(glob)) .wrap_err("failed find latest generated file")?; println!("{}", file_path.display()); PrettyPrinter::new() diff --git a/tools/pepsi/src/cargo.rs b/tools/pepsi/src/cargo.rs index ffeda64d7a..15fdbd20d2 100644 --- a/tools/pepsi/src/cargo.rs +++ b/tools/pepsi/src/cargo.rs @@ -1,105 +1,47 @@ +use std::path::Path; + use clap::Args; -use color_eyre::{ - eyre::{bail, WrapErr}, - Result, +use color_eyre::{eyre::WrapErr, Result}; +use repository::{ + cargo::{run_shell, Cargo, Environment, Executor}, + configuration::read_sdk_version, + data_home::get_data_home, + sdk::download_and_install, }; -use tokio::process::Command as TokioCommand; - -use constants::OS_IS_NOT_LINUX; -use repository::Repository; -#[derive(Args)] +#[derive(Args, Clone)] pub struct Arguments { - /// Apply to entire workspace (only valid for build/check/clippy) - #[arg(long)] - pub workspace: bool, #[arg(long, default_value = "incremental")] pub profile: String, - #[arg(long, default_value = "webots")] - pub target: String, - #[arg(long)] - pub no_sdk_installation: bool, #[arg(long, default_value = None, num_args = 1..)] pub features: Option>, - /// Pass through arguments to cargo ... -- PASSTHROUGH_ARGUMENTS + #[arg(default_value = "nao")] + pub target: String, + #[arg(long)] + pub workspace: bool, + /// Pass through arguments to cargo #[arg(last = true, value_parser)] pub passthrough_arguments: Vec, - /// Use a remote machine for compilation, see ./scripts/remote for details + /// Use the SDK (automatically set when target is `nao`) + #[arg(long)] + pub sdk: bool, + /// Use docker for execution, only relevant when using the SDK + #[arg(long)] + pub docker: bool, + /// Use a remote machine for execution, see ./scripts/remote for details #[arg(long)] pub remote: bool, } -#[derive(Debug)] -pub enum Command { - Build, - Check, - Clippy, - Run, -} - -pub async fn cargo(arguments: Arguments, repository: &Repository, command: Command) -> Result<()> { +pub async fn cargo( + command: &str, + arguments: Arguments, + repository_root: impl AsRef, +) -> Result<()> { if arguments.remote { - return remote(arguments, command).await; - } - - if !arguments.no_sdk_installation && arguments.target == "nao" { - let installation_directory = repository - .link_sdk_home(None) - .await - .wrap_err("failed to link SDK home")?; - - let use_docker = OS_IS_NOT_LINUX; - if !use_docker { - repository - .install_sdk(None, installation_directory) - .await - .wrap_err("failed to install SDK")?; - } - } - - match command { - Command::Build => repository - .build( - arguments.workspace, - &arguments.profile, - &arguments.target, - arguments.features, - &arguments.passthrough_arguments, - ) - .await - .wrap_err("failed to build")?, - Command::Check => repository - .check(arguments.workspace, &arguments.profile, &arguments.target) - .await - .wrap_err("failed to check")?, - Command::Clippy => repository - .clippy(arguments.workspace, &arguments.profile, &arguments.target) - .await - .wrap_err("failed to run clippy")?, - Command::Run => { - if arguments.workspace { - println!("INFO: Found --workspace with run subcommand, ignoring...") - } - repository - .run( - &arguments.profile, - &arguments.target, - arguments.features, - &arguments.passthrough_arguments, - ) - .await - .wrap_err("failed to run")? - } - } - - Ok(()) -} - -pub async fn remote(arguments: Arguments, command: Command) -> Result<()> { - match command { - Command::Build => { - let mut command = TokioCommand::new("./scripts/remoteWorkspace"); - + let remote_script = "./scripts/remoteWorkspace"; + let mut remote_command = remote_script.to_string(); + if command == "build" { let profile_name = match arguments.profile.as_str() { "dev" => "debug", other => other, @@ -108,44 +50,93 @@ pub async fn remote(arguments: Arguments, command: Command) -> Result<()> { "nao" => "x86_64-aldebaran-linux-gnu/", _ => "", }; - command.args([ - "--return-file", - &format!( - "target/{toolchain_name}{profile_name}/hulk_{}", - arguments.target - ), - ]); + remote_command.push_str(&format!( + " --return-file target/{toolchain_name}{profile_name}/hulk_{target}", + profile_name = profile_name, + toolchain_name = toolchain_name, + target = arguments.target + )); + } + remote_command.push_str(&format!( + " ./pepsi {command} --profile {profile}", + profile = arguments.profile, + )); + if let Some(features) = &arguments.features { + remote_command.push_str(&format!( + " --features {features}", + features = features.join(",") + )); + } + if arguments.workspace { + remote_command.push_str(" --workspace"); + } + if arguments.sdk { + remote_command.push_str(" --sdk"); + } + if arguments.docker { + remote_command.push_str(" --docker"); + } + remote_command.push_str(&format!(" {target}", target = arguments.target)); + if !arguments.passthrough_arguments.is_empty() { + remote_command.push_str(" -- "); + remote_command.push_str(&arguments.passthrough_arguments.join(" ")); + } + run_shell(&remote_command) + .await + .wrap_err("failed to run remote script")?; + } else { + let sdk_version = read_sdk_version(&repository_root) + .await + .wrap_err("failed to get HULK OS version")?; + let data_home = get_data_home().wrap_err("failed to get data home")?; + let use_sdk = arguments.sdk || (command == "build" && arguments.target == "nao"); + let cargo = if use_sdk { + if arguments.docker || !cfg!(target_os = "linux") { + Cargo::sdk(Environment { + executor: Executor::Docker, + version: sdk_version, + }) + } else { + download_and_install(&sdk_version, data_home) + .await + .wrap_err("failed to install SDK")?; + Cargo::sdk(Environment { + executor: Executor::Native, + version: sdk_version, + }) + } + } else { + Cargo::native() + }; - command - .arg("./pepsi") - .arg("build") - .arg("--profile") - .arg(arguments.profile) - .arg("--target") - .arg(arguments.target); + let mut cargo_command = cargo.command(command, repository_root.as_ref())?; - if arguments.workspace { - command.arg("--workspace"); - } - if arguments.no_sdk_installation { - command.arg("--no-sdk-installation"); - } - command.arg("--"); - command.args(arguments.passthrough_arguments); + cargo_command.profile(&arguments.profile); - let status = command - .status() - .await - .wrap_err("failed to execute remote script")?; + if let Some(features) = &arguments.features { + cargo_command.features(features); + } - if !status.success() { - bail!("remote script exited with code {}", status.code().unwrap()) - } + let manifest_path = format!( + "./crates/hulk_{target}/Cargo.toml", + target = arguments.target + ); + cargo_command.manifest_path(Path::new(&manifest_path))?; - Ok(()) + if !arguments.passthrough_arguments.is_empty() { + cargo_command.passthrough_arguments(&arguments.passthrough_arguments); } - Command::Check | Command::Clippy | Command::Run => { - unimplemented!("remote option is not compatible with cargo command: {command:?}") + + if arguments.workspace { + cargo_command.workspace(); + cargo_command.all_targets(); + cargo_command.all_features(); } + + let shell_command = cargo_command.shell_command()?; + run_shell(&shell_command) + .await + .wrap_err("failed to run cargo build")?; } + Ok(()) } diff --git a/tools/pepsi/src/communication.rs b/tools/pepsi/src/communication.rs index f9b40fecfb..a1a35096ac 100644 --- a/tools/pepsi/src/communication.rs +++ b/tools/pepsi/src/communication.rs @@ -1,7 +1,8 @@ +use std::path::Path; + use clap::Subcommand; use color_eyre::{eyre::WrapErr, Result}; - -use repository::Repository; +use repository::communication::configure_communication; #[derive(Subcommand)] pub enum Arguments { @@ -9,9 +10,8 @@ pub enum Arguments { Disable, } -pub async fn communication(arguments: Arguments, repository: &Repository) -> Result<()> { - repository - .set_communication(matches!(arguments, Arguments::Enable)) +pub async fn communication(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { + configure_communication(matches!(arguments, Arguments::Enable), repository_root) .await .wrap_err("failed to set communication enablement") } diff --git a/tools/pepsi/src/completions.rs b/tools/pepsi/src/completions.rs index 2cc28773a5..3f7e57c3d1 100644 --- a/tools/pepsi/src/completions.rs +++ b/tools/pepsi/src/completions.rs @@ -85,9 +85,9 @@ fn dynamic_completions(shell: Shell, static_completions: String) { ("reboot", ""), ("shell", ""), ("upload", ""), - ("wireless", "list"), - ("wireless", "set"), - ("wireless", "status"), + ("wifi", "list"), + ("wifi", "set"), + ("wifi", "status"), ]; for (first, second) in COMPLETION_SUBCOMMANDS { if second.is_empty() { diff --git a/tools/pepsi/src/gammaray.rs b/tools/pepsi/src/gammaray.rs index ed0d48c499..5a823cec24 100644 --- a/tools/pepsi/src/gammaray.rs +++ b/tools/pepsi/src/gammaray.rs @@ -1,13 +1,12 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use clap::Args; use color_eyre::{eyre::WrapErr, Result}; use argument_parsers::NaoAddress; -use constants::OS_VERSION; use nao::Nao; use opn::verify_image; -use repository::{get_image_path, Repository}; +use repository::{configuration::read_os_version, data_home::get_data_home, image::download_image}; use crate::progress_indicator::ProgressIndicator; @@ -18,23 +17,29 @@ pub struct Arguments { image_path: Option, /// Alternative HULKs-OS version e.g. 3.3 #[arg(long)] - os_version: Option, + version: Option, /// The NAOs to flash the image to, e.g. 20w or 10.1.24.22 #[arg(required = true)] naos: Vec, } -pub async fn gammaray(arguments: Arguments, repository: &Repository) -> Result<()> { - let version = arguments.os_version.as_deref().unwrap_or(OS_VERSION); +pub async fn gammaray(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { + let version = match arguments.version { + Some(version) => version, + None => read_os_version(&repository_root) + .await + .wrap_err("failed to get OS version")?, + }; + let data_home = get_data_home()?; let image_path = match arguments.image_path { Some(image_path) => image_path, - None => get_image_path(version).await?, + None => download_image(&version, data_home).await?, }; let image_path = image_path.as_path(); verify_image(image_path).wrap_err("image verification failed")?; - let team_toml = &repository.parameters_root().join("team.toml"); + let team_toml = &repository_root.as_ref().join("etc/parameters/team.toml"); ProgressIndicator::map_tasks( arguments.naos, @@ -48,8 +53,8 @@ pub async fn gammaray(arguments: Arguments, repository: &Repository) -> Result<( .wrap_err_with(|| format!("failed to flash image to {nao_address}"))?; progress_bar.set_message("Uploading team configuration..."); nao.rsync_with_nao(false) - .arg(team_toml.to_str().unwrap()) - .arg(format!("{}:/media/internal/", nao.host)) + .arg(team_toml) + .arg(format!("{}:/media/internal/", nao.address)) .spawn() .wrap_err("failed to upload team configuration")?; nao.reboot() diff --git a/tools/pepsi/src/location.rs b/tools/pepsi/src/location.rs index f6bf272b69..6ab048e821 100644 --- a/tools/pepsi/src/location.rs +++ b/tools/pepsi/src/location.rs @@ -1,7 +1,8 @@ +use std::path::Path; + use clap::Subcommand; use color_eyre::{eyre::WrapErr, Result}; - -use repository::Repository; +use repository::location::{list_available_locations, list_configured_locations, set_location}; #[derive(Subcommand)] pub enum Arguments { @@ -20,12 +21,11 @@ pub enum Arguments { Status, } -pub async fn location(arguments: Arguments, repository: &Repository) -> Result<()> { +pub async fn location(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { match arguments { Arguments::List {} => { println!("Available Locations:"); - for location in repository - .list_available_locations() + for location in list_available_locations(repository_root) .await .wrap_err("failed to list available locations")? { @@ -33,15 +33,13 @@ pub async fn location(arguments: Arguments, repository: &Repository) -> Result<( } } Arguments::Set { target, location } => { - repository - .set_location(&target, &location) + set_location(&target, &location, repository_root) .await .wrap_err_with(|| format!("failed setting location for {target}"))?; } Arguments::Status {} => { println!("Configured Locations:"); - for (target, location) in repository - .get_configured_locations() + for (target, location) in list_configured_locations(repository_root) .await .wrap_err("failed to get configured locations")? { diff --git a/tools/pepsi/src/main.rs b/tools/pepsi/src/main.rs index e693bb8b19..efc2936039 100644 --- a/tools/pepsi/src/main.rs +++ b/tools/pepsi/src/main.rs @@ -1,11 +1,17 @@ -use std::path::PathBuf; +use std::{env::current_dir, path::PathBuf}; use clap::{CommandFactory, Parser, Subcommand}; -use color_eyre::{config::HookBuilder, eyre::WrapErr, Result}; +use color_eyre::{ + config::HookBuilder, + eyre::{ContextCompat, WrapErr}, + Result, +}; +use log::warn; +use repository::{find_root::find_repository_root, inspect_version::check_for_update}; use crate::aliveness::{aliveness, Arguments as AlivenessArguments}; use analyze::{analyze, Arguments as AnalyzeArguments}; -use cargo::{cargo, Arguments as CargoArguments, Command as CargoCommand}; +use cargo::{cargo, Arguments as CargoArguments}; use communication::{communication, Arguments as CommunicationArguments}; use completions::{completions, Arguments as CompletionArguments}; use gammaray::{gammaray, Arguments as GammarayArguments}; @@ -19,11 +25,10 @@ use power_off::{power_off, Arguments as PoweroffArguments}; use pre_game::{pre_game, Arguments as PreGameArguments}; use reboot::{reboot, Arguments as RebootArguments}; use recording::{recording, Arguments as RecordingArguments}; -use repository::{get_repository_root, Repository}; use sdk::{sdk, Arguments as SdkArguments}; use shell::{shell, Arguments as ShellArguments}; use upload::{upload, Arguments as UploadArguments}; -use wireless::{wireless, Arguments as WirelessArguments}; +use wifi::{wifi, Arguments as WiFiArguments}; mod aliveness; mod analyze; @@ -45,159 +50,164 @@ mod recording; mod sdk; mod shell; mod upload; -mod wireless; +mod wifi; + +#[derive(Parser)] +#[clap(version, name = "pepsi")] +struct Arguments { + /// Alternative repository root + #[arg(long)] + repository_root: Option, + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Analyze source code + #[clap(subcommand)] + Analyze(AnalyzeArguments), + /// Get aliveness information from NAOs + Aliveness(AlivenessArguments), + /// Builds the code for a target + Build(CargoArguments), + /// Checks the code with cargo check + Check(CargoArguments), + /// Checks the code with cargo clippy + Clippy(CargoArguments), + /// Enable/disable communication + #[command(subcommand)] + Communication(CommunicationArguments), + /// Generates shell completion files + Completions(CompletionArguments), + /// Flash a HULKs-OS image to NAOs + Gammaray(GammarayArguments), + /// Control the HULK service + Hulk(HulkArguments), + /// Control the configured location + #[command(subcommand)] + Location(LocationArguments), + /// Logging on the NAO + #[command(subcommand)] + Logs(LogsArguments), + /// Change player numbers of the NAOs in local parameters + Playernumber(PlayerNumberArguments), + /// Ping NAOs + Ping(PingArguments), + /// Disable NAOs after a game (downloads logs, unsets WiFi network, etc.) + Postgame(PostGameArguments), + /// Power NAOs off + Poweroff(PoweroffArguments), + /// Get NAOs ready for a game (sets player numbers, uploads, sets WiFi network, etc.) + Pregame(PreGameArguments), + /// Reboot NAOs + Reboot(RebootArguments), + /// Set cycler instances to be recorded + Recording(RecordingArguments), + /// Runs the code for a target + Run(CargoArguments), + /// Manage the NAO SDK + #[command(subcommand)] + Sdk(SdkArguments), + /// Opens a command line shell to a NAO + Shell(ShellArguments), + /// Upload the code to NAOs + Upload(UploadArguments), + /// Control WiFi network on the NAO + #[command(subcommand, name = "wifi")] + WiFi(WiFiArguments), +} #[tokio::main] async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); HookBuilder::new().display_env_section(false).install()?; let arguments = Arguments::parse(); - let repository_root = match arguments.repository_root { - Some(repository_root) => Ok(repository_root), - None => get_repository_root() - .await - .wrap_err("failed to get repository root"), - }; - let repository = repository_root.map(Repository::new); - if let Ok(repository) = &repository { - repository.check_new_version_available(env!("CARGO_PKG_VERSION"), "tools/pepsi")?; + let repository_root = arguments.repository_root.map(Ok).unwrap_or_else(|| { + let current_directory = current_dir().wrap_err("failed to get current directory")?; + find_repository_root(current_directory).wrap_err("failed to find repository root") + }); + if let Ok(repository_root) = &repository_root { + if let Err(error) = check_for_update( + env!("CARGO_PKG_VERSION"), + repository_root.join("tools/pepsi/Cargo.toml"), + ) { + warn!("{error:#?}"); + } } match arguments.command { - Command::Analyze(arguments) => analyze(arguments, &repository?) + Command::Analyze(arguments) => analyze(arguments, repository_root) .await .wrap_err("failed to execute analyze command")?, - Command::Aliveness(arguments) => aliveness(arguments) + Command::Aliveness(arguments) => aliveness(arguments, repository_root) .await .wrap_err("failed to execute aliveness command")?, - Command::Build(arguments) => cargo(arguments, &repository?, CargoCommand::Build) + Command::Build(arguments) => cargo("build", arguments, repository_root?) .await .wrap_err("failed to execute build command")?, - Command::Check(arguments) => cargo(arguments, &repository?, CargoCommand::Check) + Command::Check(arguments) => cargo("check", arguments, repository_root?) .await .wrap_err("failed to execute check command")?, - Command::Clippy(arguments) => cargo(arguments, &repository?, CargoCommand::Clippy) + Command::Clippy(arguments) => cargo("clippy", arguments, repository_root?) .await .wrap_err("failed to execute clippy command")?, - Command::Communication(arguments) => communication(arguments, &repository?) + Command::Communication(arguments) => communication(arguments, repository_root?) .await .wrap_err("failed to execute communication command")?, Command::Completions(arguments) => completions(arguments, Arguments::command()) .await .wrap_err("failed to execute completion command")?, - Command::Gammaray(arguments) => gammaray(arguments, &repository?) + Command::Gammaray(arguments) => gammaray(arguments, repository_root?) .await .wrap_err("failed to execute gammaray command")?, Command::Hulk(arguments) => hulk(arguments) .await .wrap_err("failed to execute hulk command")?, - Command::Location(arguments) => location(arguments, &repository?) + Command::Location(arguments) => location(arguments, repository_root?) .await .wrap_err("failed to execute location command")?, Command::Logs(arguments) => logs(arguments) .await .wrap_err("failed to execute logs command")?, Command::Ping(arguments) => ping(arguments).await, - Command::Playernumber(arguments) => player_number(arguments, &repository?) + Command::Playernumber(arguments) => player_number(arguments, repository_root?) .await .wrap_err("failed to execute player_number command")?, Command::Postgame(arguments) => post_game(arguments) .await .wrap_err("failed to execute post_game command")?, - Command::Poweroff(arguments) => power_off(arguments) + Command::Poweroff(arguments) => power_off(arguments, repository_root) .await .wrap_err("failed to execute power_off command")?, - Command::Pregame(arguments) => pre_game(arguments, &repository?) + Command::Pregame(arguments) => pre_game(arguments, repository_root?) .await .wrap_err("failed to execute pre_game command")?, Command::Reboot(arguments) => reboot(arguments) .await .wrap_err("failed to execute reboot command")?, - Command::Recording(arguments) => recording(arguments, &repository?) + Command::Recording(arguments) => recording(arguments, repository_root?) .await .wrap_err("failed to execute recording command")?, - Command::Run(arguments) => cargo(arguments, &repository?, CargoCommand::Run) + Command::Run(arguments) => cargo("run", arguments, repository_root?) .await .wrap_err("failed to execute run command")?, - Command::Sdk(arguments) => sdk(arguments, &repository?) + Command::Sdk(arguments) => sdk(arguments, repository_root?) .await .wrap_err("failed to execute sdk command")?, Command::Shell(arguments) => shell(arguments) .await .wrap_err("failed to execute shell command")?, - Command::Upload(arguments) => upload(arguments, &repository?) + Command::Upload(arguments) => upload(arguments, repository_root?) .await .wrap_err("failed to execute upload command")?, - Command::Wireless(arguments) => wireless(arguments) + Command::WiFi(arguments) => wifi(arguments) .await - .wrap_err("failed to execute wireless command")?, + .wrap_err("failed to execute wifi command")?, } Ok(()) } - -#[derive(Parser)] -#[clap(version, name = "pepsi")] -struct Arguments { - /// Alternative repository root (if not given the parent of .git is used) - #[arg(long)] - repository_root: Option, - #[command(subcommand)] - command: Command, -} - -#[derive(Subcommand)] -enum Command { - /// Analyze source code - #[clap(subcommand)] - Analyze(AnalyzeArguments), - /// Get aliveness information from NAOs - Aliveness(AlivenessArguments), - /// Builds the code for a target - Build(CargoArguments), - /// Checks the code with cargo check - Check(CargoArguments), - /// Checks the code with cargo clippy - Clippy(CargoArguments), - /// Enable/disable communication - #[command(subcommand)] - Communication(CommunicationArguments), - /// Generates shell completion files - Completions(CompletionArguments), - /// Flash a HULKs-OS image to NAOs - Gammaray(GammarayArguments), - /// Control the HULK service - Hulk(HulkArguments), - /// Control the configured location - #[command(subcommand)] - Location(LocationArguments), - /// Logging on the NAO - #[command(subcommand)] - Logs(LogsArguments), - /// Change player numbers of the NAOs in local parameters - Playernumber(PlayerNumberArguments), - /// Ping NAOs - Ping(PingArguments), - /// Disable NAOs after a game (downloads logs, unsets wireless network, etc.) - Postgame(PostGameArguments), - /// Power NAOs off - Poweroff(PoweroffArguments), - /// Get NAOs ready for a game (sets player numbers, uploads, sets wireless network, etc.) - Pregame(PreGameArguments), - /// Reboot NAOs - Reboot(RebootArguments), - /// Set cycler instances to be recorded - Recording(RecordingArguments), - /// Runs the code for a target - Run(CargoArguments), - /// Manage the NAO SDK - #[command(subcommand)] - Sdk(SdkArguments), - /// Opens a command line shell to a NAO - Shell(ShellArguments), - /// Upload the code to NAOs - Upload(UploadArguments), - /// Control wireless network on the NAO - #[command(subcommand)] - Wireless(WirelessArguments), -} diff --git a/tools/pepsi/src/ping.rs b/tools/pepsi/src/ping.rs index 92a114ed02..70555fd164 100644 --- a/tools/pepsi/src/ping.rs +++ b/tools/pepsi/src/ping.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use clap::Args; use argument_parsers::NaoAddress; @@ -7,9 +9,9 @@ use crate::progress_indicator::ProgressIndicator; #[derive(Args)] pub struct Arguments { - /// Time in seconds after which ping is aborted + /// Timeout in seconds after which ping is aborted #[arg(long, default_value = "2")] - pub timeout_seconds: u32, + pub timeout: u64, /// The NAOs to ping to e.g. 20w or 10.1.24.22 #[arg(required = true)] pub naos: Vec, @@ -20,9 +22,12 @@ pub async fn ping(arguments: Arguments) { arguments.naos, "Pinging NAO...", |nao_address, _progress_bar| async move { - Nao::try_new_with_ping_and_arguments(nao_address.ip, arguments.timeout_seconds) - .await - .map(|_| ()) + Nao::try_new_with_ping_and_arguments( + nao_address.ip, + Duration::from_secs(arguments.timeout), + ) + .await + .map(|_| ()) }, ) .await; diff --git a/tools/pepsi/src/player_number.rs b/tools/pepsi/src/player_number.rs index c82b48af47..76e99e5381 100644 --- a/tools/pepsi/src/player_number.rs +++ b/tools/pepsi/src/player_number.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::{collections::HashSet, path::Path}; use clap::Args; use color_eyre::{ @@ -7,7 +7,7 @@ use color_eyre::{ }; use argument_parsers::NaoNumberPlayerAssignment; -use repository::Repository; +use repository::{player_number::configure_player_number, team::read_team_configuration}; use crate::progress_indicator::ProgressIndicator; @@ -18,29 +18,17 @@ pub struct Arguments { pub assignments: Vec, } -pub async fn player_number(arguments: Arguments, repository: &Repository) -> Result<()> { - let team = repository - .get_configured_team() +pub async fn player_number(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { + let repository_root = repository_root.as_ref(); + let team = read_team_configuration(repository_root) .await - .wrap_err("failed to get configured team")?; + .wrap_err("failed to get team configuration")?; - // Check if two NaoNumbers are assigned to the same PlayerNumber - // or if a NaoNumber is assigned to multiple PlayerNumbers - let mut existing_player_numbers = HashSet::new(); - let mut existing_nao_numbers = HashSet::new(); + check_for_duplication(&arguments.assignments)?; - if arguments.assignments.iter().any( - |NaoNumberPlayerAssignment { - nao_number, - player_number, - }| { - !existing_nao_numbers.insert(nao_number) - || !existing_player_numbers.insert(player_number) - }, - ) { - bail!("Duplication in NAO to player number assignments") - } + // reborrows the team to avoid moving it into the closure let naos = &team.naos; + ProgressIndicator::map_tasks( arguments.assignments, "Setting player number...", @@ -50,8 +38,7 @@ pub async fn player_number(arguments: Arguments, repository: &Repository) -> Res .iter() .find(|nao| nao.number == number) .ok_or_else(|| eyre!("NAO with Hardware ID {number} does not exist"))?; - repository - .set_player_number(&nao.head_id, assignment.player_number) + configure_player_number(&nao.head_id, assignment.player_number, repository_root) .await .wrap_err_with(|| format!("failed to set player number for {assignment}")) }, @@ -60,3 +47,23 @@ pub async fn player_number(arguments: Arguments, repository: &Repository) -> Res Ok(()) } + +fn check_for_duplication(assignments: &[NaoNumberPlayerAssignment]) -> Result<()> { + // Check if two NaoNumbers are assigned to the same PlayerNumber + // or if a NaoNumber is assigned to multiple PlayerNumbers + let mut existing_player_numbers = HashSet::new(); + let mut existing_nao_numbers = HashSet::new(); + + if assignments.iter().any( + |NaoNumberPlayerAssignment { + nao_number, + player_number, + }| { + !existing_nao_numbers.insert(nao_number) + || !existing_player_numbers.insert(player_number) + }, + ) { + bail!("duplication in NAO to player number assignments") + } + Ok(()) +} diff --git a/tools/pepsi/src/post_game.rs b/tools/pepsi/src/post_game.rs index 08cc762d7a..221b57200a 100644 --- a/tools/pepsi/src/post_game.rs +++ b/tools/pepsi/src/post_game.rs @@ -1,28 +1,18 @@ use std::path::PathBuf; -use clap::{ - builder::{PossibleValuesParser, TypedValueParser}, - Args, -}; +use clap::Args; use color_eyre::{eyre::WrapErr, Result}; -use argument_parsers::{parse_network, NaoAddress, NETWORK_POSSIBLE_VALUES}; -use nao::{Network, SystemctlAction}; +use argument_parsers::NaoAddress; +use nao::{Nao, Network, SystemctlAction}; -use crate::{ - hulk::{hulk, Arguments as HulkArguments}, - logs::{logs, Arguments as LogsArguments}, - wireless::{wireless, Arguments as WirelessArguments}, -}; +use crate::progress_indicator::ProgressIndicator; #[derive(Args)] pub struct Arguments { - /// The network to connect the wireless device to (None disconnects from anything) - #[arg( - value_parser = PossibleValuesParser::new(NETWORK_POSSIBLE_VALUES) - .map(|s| parse_network(&s).unwrap())) - ] - pub network: Network, + /// Do not disconnect from the WiFi network + #[arg(long)] + pub no_disconnect: bool, /// Directory where to store the downloaded logs (will be created if not existing) pub log_directory: PathBuf, /// The NAOs to execute that command on e.g. 20w or 10.1.24.22 @@ -31,26 +21,33 @@ pub struct Arguments { } pub async fn post_game(arguments: Arguments) -> Result<()> { - hulk(HulkArguments { - action: SystemctlAction::Stop, - naos: arguments.naos.clone(), - }) - .await - .wrap_err("failed to start HULK service")?; - - logs(LogsArguments::Download { - log_directory: arguments.log_directory, - naos: arguments.naos.clone(), - }) - .await - .wrap_err("failed to download logs")?; - - wireless(WirelessArguments::Set { - network: arguments.network, - naos: arguments.naos, - }) - .await - .wrap_err("failed to set wireless network")?; + let arguments = &arguments; + ProgressIndicator::map_tasks( + &arguments.naos, + "Executing postgame tasks...", + |nao_address, progress_bar| async move { + let nao = Nao::try_new_with_ping(nao_address.ip).await?; + progress_bar.set_message("Stopping HULK service..."); + nao.execute_systemctl(SystemctlAction::Stop, "hulk") + .await + .wrap_err_with(|| format!("failed to execute systemctl hulk on {nao_address}"))?; + + progress_bar.set_message("Disconnecting from WiFi..."); + nao.set_wifi(Network::None) + .await + .wrap_err_with(|| format!("failed to set network on {nao_address}"))?; + + progress_bar.set_message("Downloading logs..."); + let log_directory = arguments.log_directory.join(nao_address.to_string()); + nao.download_logs(log_directory, |status| { + progress_bar.set_message(format!("Downloading logs: {status}")) + }) + .await + .wrap_err_with(|| format!("failed to download logs from {nao_address}"))?; + Ok(()) + }, + ) + .await; Ok(()) } diff --git a/tools/pepsi/src/power_off.rs b/tools/pepsi/src/power_off.rs index 45a1738d6c..6f5f6106fd 100644 --- a/tools/pepsi/src/power_off.rs +++ b/tools/pepsi/src/power_off.rs @@ -1,10 +1,12 @@ +use std::path::Path; + use clap::Args; use color_eyre::{eyre::WrapErr, Result}; use argument_parsers::{number_to_ip, Connection, NaoAddress}; -use constants::TEAM; use futures_util::{stream::FuturesUnordered, StreamExt}; use nao::Nao; +use repository::team::read_team_configuration; use crate::progress_indicator::ProgressIndicator; @@ -18,9 +20,16 @@ pub struct Arguments { pub naos: Vec, } -pub async fn power_off(arguments: Arguments) -> Result<()> { +pub async fn power_off( + arguments: Arguments, + repository_root: Result>, +) -> Result<()> { if arguments.all { - let addresses = TEAM + let repository_root = repository_root?; + let team = read_team_configuration(repository_root) + .await + .wrap_err("failed to get team configuration")?; + let addresses = team .naos .iter() .map(|nao| async move { diff --git a/tools/pepsi/src/pre_game.rs b/tools/pepsi/src/pre_game.rs index f6a23f1afa..1be1cac360 100644 --- a/tools/pepsi/src/pre_game.rs +++ b/tools/pepsi/src/pre_game.rs @@ -1,27 +1,37 @@ +use std::{collections::HashMap, path::Path}; + use clap::{ builder::{PossibleValuesParser, TypedValueParser}, Args, }; -use color_eyre::{eyre::WrapErr, Result}; +use color_eyre::{ + eyre::{bail, WrapErr}, + Result, +}; -use argument_parsers::{parse_network, NaoAddressPlayerAssignment, NETWORK_POSSIBLE_VALUES}; -use nao::Network; -use repository::Repository; +use argument_parsers::{ + parse_network, NaoAddress, NaoAddressPlayerAssignment, NETWORK_POSSIBLE_VALUES, +}; +use indicatif::ProgressBar; +use log::warn; +use nao::{Nao, Network, SystemctlAction}; +use repository::{ + communication::configure_communication, configuration::read_os_version, location::set_location, + recording::configure_recording_intervals, upload::populate_upload_directory, +}; +use tempfile::tempdir; use crate::{ + cargo::{cargo, Arguments as CargoArguments}, player_number::{player_number, Arguments as PlayerNumberArguments}, - recording::{parse_key_value, recording, Arguments as RecordingArguments}, - upload::{upload, Arguments as UploadArguments}, - wireless::{wireless, Arguments as WirelessArguments}, + progress_indicator::ProgressIndicator, + recording::parse_key_value, }; #[derive(Args)] pub struct Arguments { - #[arg(long, default_value = "release")] - pub profile: String, - /// Do not update nor install SDK - #[arg(long)] - pub no_sdk_installation: bool, + #[command(flatten)] + pub cargo: CargoArguments, /// Do not build before uploading #[arg(long)] pub no_build: bool, @@ -31,56 +41,61 @@ pub struct Arguments { /// Do not remove existing remote files during uploading #[arg(long)] pub no_clean: bool, - /// Enable communication - #[arg(long)] - pub with_communication: bool, /// Skip the OS version check #[arg(long)] pub skip_os_check: bool, - /// Prepare everything for the upload without performing the actual one + /// Enable communication, communication is disabled by default #[arg(long)] - pub prepare: bool, + pub with_communication: bool, /// Intervals between cycle recordings, e.g. Control=1,VisionTop=30 to record every cycle in Control /// and one out of every 30 in VisionTop. Set to 0 or don't specify to disable recording for a cycler. - #[arg(long, value_delimiter=',', value_parser = parse_key_value::, default_value = "Control=1,VisionTop=30,VisionBottom=30,SplNetwork=1")] + #[arg( + long, + value_delimiter=',', + value_parser = parse_key_value::, + default_value = "Control=1,VisionTop=30,VisionBottom=30,SplNetwork=1", + )] pub recording_intervals: Vec<(String, usize)>, + /// Prepare everything for the upload without performing the actual one + #[arg(long)] + pub prepare: bool, /// The location to use for parameters pub location: String, - /// The network to connect the wireless device to (None disconnects from anything) + /// The network to connect the wifi device to (None disconnects from anything) #[arg( value_parser = PossibleValuesParser::new(NETWORK_POSSIBLE_VALUES) .map(|s| parse_network(&s).unwrap())) ] - pub network: Network, + pub wifi: Network, /// The NAOs to upload to with player number assignments e.g. 20w:2 or 10.1.24.22:5 (player numbers start from 1) #[arg(required = true)] pub assignments: Vec, - /// Use a remote machine for compilation, see ./scripts/remote for details - #[arg(long)] - pub remote: bool, } -pub async fn pre_game(arguments: Arguments, repository: &Repository) -> Result<()> { +pub async fn pre_game(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { + let repository_root = repository_root.as_ref(); + let naos: Vec<_> = arguments .assignments .iter() .map(|assignment| assignment.nao_address) .collect(); - recording( - RecordingArguments { - recording_intervals: arguments.recording_intervals, - }, - repository, + configure_recording_intervals( + HashMap::from_iter(arguments.recording_intervals.clone()), + repository_root, ) .await - .wrap_err("failed to set cyclers to be recorded")?; + .wrap_err("failed to set recording settings")?; - repository - .set_location("nao", &arguments.location) + set_location("nao", &arguments.location, repository_root) .await .wrap_err_with(|| format!("failed setting location for nao to {}", arguments.location))?; + configure_communication(arguments.with_communication, repository_root) + .await + .wrap_err("failed to set communication")?; + player_number( PlayerNumberArguments { assignments: arguments @@ -91,41 +106,107 @@ pub async fn pre_game(arguments: Arguments, repository: &Repository) -> Result<( .collect::, _>>() .wrap_err("failed to convert NAO address assignments into NAO number assignments for player number setting")? }, - repository + repository_root ) .await .wrap_err("failed to set player numbers")?; - if !arguments.prepare { - wireless(WirelessArguments::Scan { naos: naos.clone() }) + if !arguments.no_build { + cargo("build", arguments.cargo.clone(), repository_root) .await - .wrap_err("failed to scan for networks")?; + .wrap_err("failed to build the code")?; + } - wireless(WirelessArguments::Set { - network: arguments.network, - naos: naos.clone(), - }) - .await - .wrap_err("failed to set wireless network")?; + if arguments.prepare { + warn!("Preparation complete, skipping the rest"); + return Ok(()); } - upload( - UploadArguments { - profile: arguments.profile, - no_sdk_installation: arguments.no_sdk_installation, - no_build: arguments.no_build, - no_restart: arguments.no_restart, - no_clean: arguments.no_clean, - no_communication: !arguments.with_communication, - prepare: arguments.prepare, - skip_os_check: arguments.skip_os_check, - naos, - remote: arguments.remote, + let upload_directory = tempdir().wrap_err("failed to get temporary directory")?; + populate_upload_directory(&upload_directory, &arguments.cargo.profile, repository_root) + .await + .wrap_err("failed to populate upload directory")?; + + let arguments = &arguments; + let upload_directory = &upload_directory; + + ProgressIndicator::map_tasks( + &naos, + "Executing pregame tasks", + |nao_address, progress_bar| async move { + setup_nao( + nao_address, + upload_directory, + arguments, + progress_bar, + repository_root, + ) + .await }, - repository, ) + .await; + + Ok(()) +} + +async fn setup_nao( + nao_address: &NaoAddress, + upload_directory: impl AsRef, + arguments: &Arguments, + progress: ProgressBar, + repository_root: &Path, +) -> Result<()> { + progress.set_message("Pinging NAO..."); + let nao = Nao::try_new_with_ping(nao_address.ip).await?; + + if !arguments.skip_os_check { + progress.set_message("Checking OS version..."); + let nao_os_version = nao + .get_os_version() + .await + .wrap_err_with(|| format!("failed to get OS version of {nao_address}"))?; + let expected_os_version = read_os_version(repository_root) + .await + .wrap_err("failed to get configured OS version")?; + if nao_os_version != expected_os_version { + bail!("mismatched OS versions: Expected {expected_os_version}, found {nao_os_version}"); + } + } + + progress.set_message("Stopping HULK..."); + nao.execute_systemctl(SystemctlAction::Stop, "hulk") + .await + .wrap_err_with(|| format!("failed to stop HULK service on {nao_address}"))?; + + progress.set_message("Uploading: ..."); + nao.upload(upload_directory, "hulk", !arguments.no_clean, |status| { + progress.set_message(format!("Uploading: {}", status)) + }) .await - .wrap_err("failed to upload")?; + .wrap_err_with(|| format!("failed to upload binary to {nao_address}"))?; + + if arguments.wifi != Network::None { + progress.set_message("Scanning for WiFi..."); + nao.scan_networks() + .await + .wrap_err_with(|| format!("failed to scan for networks on {nao_address}"))?; + } + + progress.set_message("Setting WiFi..."); + nao.set_wifi(arguments.wifi) + .await + .wrap_err_with(|| format!("failed to set network on {nao_address}"))?; + + if !arguments.no_restart { + progress.set_message("Restarting HULK..."); + if let Err(error) = nao.execute_systemctl(SystemctlAction::Start, "hulk").await { + let logs = nao + .retrieve_logs() + .await + .wrap_err("failed to retrieve logs")?; + bail!("failed to restart hulk: {error:#?}\nLogs:\n{logs}") + }; + } Ok(()) } diff --git a/tools/pepsi/src/recording.rs b/tools/pepsi/src/recording.rs index d9467d1c1d..2e8cd9a0b1 100644 --- a/tools/pepsi/src/recording.rs +++ b/tools/pepsi/src/recording.rs @@ -1,9 +1,8 @@ -use std::{collections::HashMap, error::Error}; +use std::{collections::HashMap, error::Error, path::Path}; use clap::Args; use color_eyre::{eyre::WrapErr, Result}; - -use repository::Repository; +use repository::recording::configure_recording_intervals; #[derive(Args)] pub struct Arguments { @@ -13,11 +12,13 @@ pub struct Arguments { pub recording_intervals: Vec<(String, usize)>, } -pub async fn recording(arguments: Arguments, repository: &Repository) -> Result<()> { - repository - .set_recording_intervals(HashMap::from_iter(arguments.recording_intervals)) - .await - .wrap_err("failed to set recording enablement") +pub async fn recording(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { + configure_recording_intervals( + HashMap::from_iter(arguments.recording_intervals), + repository_root, + ) + .await + .wrap_err("failed to set recording settings") } pub fn parse_key_value(string: &str) -> Result<(T, U), Box> diff --git a/tools/pepsi/src/sdk.rs b/tools/pepsi/src/sdk.rs index eaf239fda0..6b624b43fa 100644 --- a/tools/pepsi/src/sdk.rs +++ b/tools/pepsi/src/sdk.rs @@ -1,41 +1,41 @@ -use std::path::PathBuf; +use std::path::Path; use clap::Subcommand; -use color_eyre::{eyre::WrapErr, Result}; +use color_eyre::{eyre::Context, Result}; -use constants::OS_IS_NOT_LINUX; -use repository::Repository; +use repository::{ + configuration::read_sdk_version, data_home::get_data_home, sdk::download_and_install, +}; #[derive(Subcommand)] pub enum Arguments { Install { - /// Alternative SDK version e.g. 3.3 + /// SDK version e.g. `3.3.1`. If not provided, version specified by `hulk.toml` is be used. #[arg(long)] - sdk_version: Option, - /// Alternative SDK installation directory e.g. ~/.naosdk/ - #[arg(long)] - installation_directory: Option, + version: Option, }, + Path, } -pub async fn sdk(arguments: Arguments, repository: &Repository) -> Result<()> { +pub async fn sdk(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { match arguments { - Arguments::Install { - sdk_version, - installation_directory, - } => { - let installation_directory = repository - .link_sdk_home(installation_directory.as_deref()) - .await - .wrap_err("failed to link SDK home")?; - - let use_docker = OS_IS_NOT_LINUX; - if !use_docker { - repository - .install_sdk(sdk_version.as_deref(), installation_directory) + Arguments::Install { version } => { + let data_home = get_data_home()?; + let version = match version { + Some(version) => version, + None => read_sdk_version(repository_root) .await - .wrap_err("failed to install SDK")?; - } + .wrap_err("failed to get OS version")?, + }; + download_and_install(&version, data_home).await?; + } + Arguments::Path => { + let sdk_version = read_sdk_version(&repository_root) + .await + .wrap_err("failed to get HULK OS version")?; + let data_home = get_data_home().wrap_err("failed to get data home")?; + let path = &data_home.join(format!("sdk/{sdk_version}/")); + println!("{}", path.display()); } } diff --git a/tools/pepsi/src/upload.rs b/tools/pepsi/src/upload.rs index 1156bcac3e..30f1095dae 100644 --- a/tools/pepsi/src/upload.rs +++ b/tools/pepsi/src/upload.rs @@ -6,25 +6,20 @@ use color_eyre::{ eyre::{bail, WrapErr}, Result, }; -use constants::OS_VERSION; use futures_util::{stream::FuturesUnordered, StreamExt}; use nao::{Nao, SystemctlAction}; -use repository::Repository; +use repository::{configuration::read_os_version, upload::populate_upload_directory}; +use tempfile::tempdir; use crate::{ - cargo::{cargo, Arguments as CargoArguments, Command}, - communication::communication, - communication::Arguments as CommunicationArguments, + cargo::{cargo, Arguments as CargoArguments}, progress_indicator::{ProgressIndicator, Task}, }; #[derive(Args)] pub struct Arguments { - #[arg(long, default_value = "incremental")] - pub profile: String, - /// Do not update nor install SDK - #[arg(long)] - pub no_sdk_installation: bool, + #[command(flatten)] + pub cargo: CargoArguments, /// Do not build before uploading #[arg(long)] pub no_build: bool, @@ -34,40 +29,35 @@ pub struct Arguments { /// Do not remove existing remote files during uploading #[arg(long)] pub no_clean: bool, - /// Do not enable communication - #[arg(long)] - pub no_communication: bool, /// Skip the OS version check #[arg(long)] pub skip_os_check: bool, - /// Prepare everything for the upload without performing the actual one - #[arg(long)] - pub prepare: bool, /// The NAOs to upload to e.g. 20w or 10.1.24.22 #[arg(required = true)] pub naos: Vec, - /// Use a remote machine for compilation, see ./scripts/remote for details - #[arg(long)] - pub remote: bool, } async fn upload_with_progress( nao_address: &NaoAddress, - hulk_directory: impl AsRef, + upload_directory: impl AsRef, arguments: &Arguments, progress: &Task, + repository_root: impl AsRef, ) -> Result<()> { progress.set_message("Pinging NAO..."); let nao = Nao::try_new_with_ping(nao_address.ip).await?; if !arguments.skip_os_check { progress.set_message("Checking OS version..."); - let os_version = nao + let nao_os_version = nao .get_os_version() .await .wrap_err_with(|| format!("failed to get OS version of {nao_address}"))?; - if os_version != OS_VERSION { - bail!("mismatched OS versions: Expected {OS_VERSION}, found {os_version}"); + let expected_os_version = read_os_version(repository_root) + .await + .wrap_err("failed to get configured OS version")?; + if nao_os_version != expected_os_version { + bail!("mismatched OS versions: Expected {expected_os_version}, found {nao_os_version}"); } } @@ -77,7 +67,7 @@ async fn upload_with_progress( .wrap_err_with(|| format!("failed to stop HULK service on {nao_address}"))?; progress.set_message("Uploading: ..."); - nao.upload(hulk_directory, !arguments.no_clean, |status| { + nao.upload(upload_directory, "hulk", !arguments.no_clean, |status| { progress.set_message(format!("Uploading: {}", status)) }) .await @@ -96,65 +86,46 @@ async fn upload_with_progress( Ok(()) } -pub async fn upload(arguments: Arguments, repository: &Repository) -> Result<()> { +pub async fn upload(arguments: Arguments, repository_root: impl AsRef) -> Result<()> { + let repository_root = repository_root.as_ref(); + if !arguments.no_build { - cargo( - CargoArguments { - workspace: false, - profile: arguments.profile.clone(), - target: "nao".to_string(), - no_sdk_installation: arguments.no_sdk_installation, - features: None, - passthrough_arguments: Vec::new(), - remote: arguments.remote, - }, - repository, - Command::Build, - ) - .await - .wrap_err("failed to build the code")?; + cargo("build", arguments.cargo.clone(), repository_root) + .await + .wrap_err("failed to build the code")?; } - let (_temporary_directory, hulk_directory) = repository - .create_upload_directory(arguments.profile.as_str()) + let upload_directory = tempdir().wrap_err("failed to get temporary directory")?; + populate_upload_directory(&upload_directory, &arguments.cargo.profile, repository_root) .await - .wrap_err("failed to create upload directory")?; + .wrap_err("failed to populate upload directory")?; - communication( - match arguments.no_communication { - true => CommunicationArguments::Disable, - false => CommunicationArguments::Enable, - }, - repository, - ) - .await - .wrap_err("failed to set communication")?; + let arguments = &arguments; + let upload_directory = &upload_directory; let multi_progress = ProgressIndicator::new(); - - if arguments.prepare { - eprintln!("WARNING: This upload was only prepared, no actual upload was performed!") - } else { - arguments - .naos - .iter() - .map(|nao_address| (nao_address, multi_progress.task(nao_address.to_string()))) - .map(|(nao_address, progress)| { - let arguments = &arguments; - let hulk_directory = hulk_directory.clone(); - - progress.enable_steady_tick(); - async move { - progress.finish_with( - upload_with_progress(nao_address, hulk_directory, arguments, &progress) - .await, + arguments + .naos + .iter() + .map(|nao_address| { + let progress = multi_progress.task(nao_address.to_string()); + progress.enable_steady_tick(); + async move { + progress.finish_with( + upload_with_progress( + nao_address, + upload_directory, + arguments, + &progress, + repository_root, ) - } - }) - .collect::>() - .collect::>() - .await; - } + .await, + ) + } + }) + .collect::>() + .collect::>() + .await; Ok(()) } diff --git a/tools/pepsi/src/wireless.rs b/tools/pepsi/src/wifi.rs similarity index 97% rename from tools/pepsi/src/wireless.rs rename to tools/pepsi/src/wifi.rs index b5a82ca044..83a2b9f550 100644 --- a/tools/pepsi/src/wireless.rs +++ b/tools/pepsi/src/wifi.rs @@ -43,7 +43,7 @@ pub enum Arguments { }, } -pub async fn wireless(arguments: Arguments) -> Result<()> { +pub async fn wifi(arguments: Arguments) -> Result<()> { match arguments { Arguments::Status { naos } => status(naos).await, Arguments::Scan { naos } => scan(naos).await, @@ -102,7 +102,7 @@ async fn set(naos: Vec, network: Network) { "Setting network...", |nao_address, _progress_bar| async move { let nao = Nao::try_new_with_ping(nao_address.ip).await?; - nao.set_network(network) + nao.set_wifi(network) .await .wrap_err_with(|| format!("failed to set network on {nao_address}")) }, diff --git a/tools/twix/src/main.rs b/tools/twix/src/main.rs index 0b2d92c187..6c01437a81 100644 --- a/tools/twix/src/main.rs +++ b/tools/twix/src/main.rs @@ -1,6 +1,8 @@ use std::{ + env::current_dir, fmt::{self, Display, Formatter}, net::IpAddr, + path::PathBuf, str::FromStr, sync::Arc, time::{Duration, SystemTime}, @@ -10,7 +12,7 @@ use aliveness::query_aliveness; use argument_parsers::NaoAddress; use clap::Parser; use color_eyre::{ - eyre::{bail, eyre}, + eyre::{bail, eyre, Context as _, ContextCompat}, Result, }; @@ -30,7 +32,7 @@ use eframe::{ use egui_dock::{DockArea, DockState, Node, NodeIndex, Split, SurfaceIndex, TabAddAlign, TabIndex}; use fern::{colors::ColoredLevelConfig, Dispatch, InitError}; -use log::error; +use log::{error, warn}; use nao::Nao; use panel::Panel; use panels::{ @@ -39,7 +41,7 @@ use panels::{ RemotePanel, TextPanel, VisionTunerPanel, }; -use repository::{get_repository_root, Repository}; +use repository::{find_root::find_repository_root, inspect_version::check_for_update}; use serde_json::{from_str, to_string, Value}; use tokio::{ runtime::{Builder, Runtime}, @@ -65,7 +67,9 @@ mod zoom_and_pan; struct Arguments { /// Nao address to connect to (overrides the address saved in the configuration file) pub address: Option, - + /// Alternative repository root + #[arg(long)] + repository_root: Option, /// Delete the current panel setup #[arg(long)] pub clear: bool, @@ -89,13 +93,28 @@ fn setup_logger() -> Result<(), InitError> { fn main() -> Result<(), eframe::Error> { setup_logger().unwrap(); - let arguments = Arguments::parse(); - let runtime = Runtime::new().unwrap(); - if let Ok(repository_root) = runtime.block_on(get_repository_root()) { - Repository::new(repository_root) - .check_new_version_available(env!("CARGO_PKG_VERSION"), "tools/twix") - .unwrap(); + let arguments = Arguments::parse(); + let repository_root = arguments + .repository_root + .clone() + .map(Ok) + .unwrap_or_else(|| { + let current_directory = current_dir().wrap_err("failed to get current directory")?; + find_repository_root(current_directory).wrap_err("failed to find repository root") + }); + match &repository_root { + Ok(repository_root) => { + if let Err(error) = check_for_update( + env!("CARGO_PKG_VERSION"), + repository_root.join("tools/pepsi/Cargo.toml"), + ) { + error!("{error:#?}"); + } + } + Err(error) => { + warn!("{error:#?}"); + } } let configuration = Configuration::load() @@ -110,6 +129,7 @@ fn main() -> Result<(), eframe::Error> { creation_context, arguments, configuration, + repository_root.ok(), ))) }), ) @@ -189,6 +209,7 @@ impl TwixApp { creation_context: &CreationContext, arguments: Arguments, configuration: Configuration, + repository_root: Option, ) -> Self { let address = arguments .address @@ -200,7 +221,7 @@ impl TwixApp { .or_else(|| creation_context.storage?.get_string("address")) .unwrap_or_else(|| "localhost".to_string()); - let nao = Arc::new(Nao::new(format!("ws://{address}:1337"))); + let nao = Arc::new(Nao::new(format!("ws://{address}:1337"), repository_root)); let connection_intent = creation_context .storage diff --git a/tools/twix/src/nao.rs b/tools/twix/src/nao.rs index 84eb7cb243..4df1fd3db8 100644 --- a/tools/twix/src/nao.rs +++ b/tools/twix/src/nao.rs @@ -12,9 +12,8 @@ use communication::{ client::{Client, ClientHandle, PathsEvent, Status}, messages::{Path, TextOrBinary}, }; -use log::{error, warn}; +use log::error; use parameters::{directory::Scope, json::nest_value_at_path}; -use repository::{get_repository_root, Repository}; use serde_json::Value; use tokio::{ runtime::{Builder, Runtime}, @@ -30,28 +29,20 @@ use crate::{ pub struct Nao { runtime: Runtime, client: ClientHandle, - repository: Option, + repository_root: Option, } impl Nao { - pub fn new(address: String) -> Self { + pub fn new(address: String, repository_root: Option) -> Self { let runtime = Builder::new_multi_thread().enable_all().build().unwrap(); let (client, handle) = Client::new(address); runtime.spawn(client.run()); - let repository = match runtime.block_on(get_repository_root()) { - Ok(root) => Some(Repository::new(root)), - Err(error) => { - warn!("{error:#}"); - None - } - }; - Self { runtime, client: handle, - repository, + repository_root, } } @@ -190,13 +181,14 @@ impl Nao { pub fn store_parameters(&self, path: &str, value: Value, scope: Scope) -> Result<()> { let client = self.client.clone(); - let root = self - .repository + let parameters_root = self + .repository_root .as_ref() .ok_or_eyre("repository not available, cannot store parameters")? - .parameters_root(); + .join("etc/parameters/"); self.runtime.block_on(async { - if let Err(error) = store_parameters(&client, path, value, scope, root).await { + if let Err(error) = store_parameters(&client, path, value, scope, parameters_root).await + { error!("{error:#}") } }); @@ -209,12 +201,12 @@ async fn store_parameters( path: &str, value: Value, scope: Scope, - root: PathBuf, + parameters_root: impl AsRef, ) -> Result<()> { let (_, bytes) = client.read_binary("hardware_ids").await?; let ids: Ids = bincode::deserialize(&bytes).wrap_err("bincode deserialization failed")?; let parameters = nest_value_at_path(path, value); - parameters::directory::serialize(¶meters, scope, path, root, &ids) + parameters::directory::serialize(¶meters, scope, path, parameters_root, &ids) .wrap_err("serialization failed")?; Ok(()) }