diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..35049cb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92a3c56..48b7f5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,23 +56,23 @@ jobs: - name: Lint with Clippy (Stable Only) if: matrix.toolchain == 'stable' - run: cargo clippy --all-targets --features="runtime-features" + run: cargo clippy --all-targets --all-features - name: Check Code Formatting (Stable Only) if: matrix.toolchain == 'stable' run: cargo fmt -- --check - name: Build the Project with nuget - run: cargo build --features="runtime-features" --verbose + run: cargo build --all-features --verbose - name: Build the Project without nuget run: cargo build --all-features --verbose - name: Run Tests - run: cargo test --features="runtime-features" --verbose + run: cargo test --all-features --verbose - name: Generate Documentation (${{ matrix.DOCTYPE }}, ${{ matrix.toolchain }}) - run: cargo doc --no-deps --features="runtime-features" + run: cargo doc --no-deps --all-features - name: Install and Run Security Audit (Nightly Only) if: matrix.toolchain == 'nightly' @@ -120,7 +120,7 @@ jobs: echo "CARGO_TARGET_${var_name}_AR=${{ matrix.target.ar }}" >> $GITHUB_ENV - name: Build Project for target ${{ matrix.target.triple }} - run: cargo build --target=${{ matrix.target.triple }} --features=no-nuget --release + run: cargo build --target=${{ matrix.target.triple }} --all-features --release - name: Run doc run: cargo doc --target=${{ matrix.target.triple }} --no-deps --all-features diff --git a/.github/workflows/release-and-deploy.yml b/.github/workflows/release-and-deploy.yml index c16cc62..1a3d691 100644 --- a/.github/workflows/release-and-deploy.yml +++ b/.github/workflows/release-and-deploy.yml @@ -148,7 +148,7 @@ jobs: continue-on-error: false - name: Run Cargo Publish in Dry-Run Mode - run: cargo publish --dry-run + run: cargo publish -p wslpluginapi-sys--dry-run - name: Create GitHub Release id: release @@ -163,6 +163,6 @@ jobs: prerelease: ${{ env.prerelease == 'true' }} - name: Cargo Publish on crates.io - run: cargo publish + run: cargo -p wslpluginapi-sys publish env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/release-checks.yml b/.github/workflows/release-checks.yml index 9c9260c..1956f28 100644 --- a/.github/workflows/release-checks.yml +++ b/.github/workflows/release-checks.yml @@ -75,7 +75,7 @@ jobs: exit 1 } cargo generate-lockfile - $pkgid = cargo pkgid --quiet + $pkgid = cargo pkgid -p wslpluginapi-sys --quiet if ($pkgid -match "#(.+)$") { $currentVersion = $matches[1] } else { @@ -125,11 +125,11 @@ jobs: - name: Run Tests shell: pwsh run: | - cargo test --all-targets + cargo test -p wslpluginapi-sys --all-targets - name: Publish Dry-Run env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} shell: pwsh run: | - cargo publish --dry-run + cargo publish -p wslpluginapi-sys --dry-run diff --git a/.gitignore b/.gitignore index 958eef2..f8ad1a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target -/nuget_packages/* -Cargo.lock \ No newline at end of file +/nuget_packages/ +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index e6780ee..87f89f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,10 @@ -[package] +[workspace] +resolver = "3" +members = ["wslpluginapi-sys", "xtask"] +default-members = ["wslpluginapi-sys"] + + +[workspace.package] name = "wslpluginapi-sys" version = "0.1.0-beta.3+2.1.3" edition = "2021" @@ -7,49 +13,3 @@ authors = ["Mickaël Véril "] description = "Rust bindings for the WSL Plugin API" license = "MIT" repository = "https://github.com/mveril/wslpluginapi-sys" -keywords = ["wsl", "plugin", "windows", "linux", "ffi"] -categories = ["os::windows-apis", "external-ffi-bindings", "virtualization"] -build = "build/main.rs" - -[features] -# Enables all features that modify the library's behavior at runtime. -# Unlike build-related features, this impacts the API's functionalities. -runtime-features = ["hooks-field-names"] -hooks-field-names = ["dep:struct-field-names-as-array"] -# build features -no-nuget = ["dep:zip", "dep:reqwest", "dep:tempfile"] - -[build-dependencies] -zip = { version = "2.2", optional = true } -tempfile = { version = "3.16", optional = true } -reqwest = { version = "0.12", optional = true, features = ["blocking"] } -semver = { version = "1.0" } -bindgen = "0.71" -cfg-if = "1.0" -constcat = "0.6.0" - - -[dependencies] -libc = "0.2" -struct-field-names-as-array = { version = "0.3", features = [ - "derive", -], optional = true } - -[dependencies.windows] -version = ">0.32" -features = [ - "Win32_Foundation", - "Win32_Security", - "Win32_System_Diagnostics_Debug", - "Win32_Networking_WinSock", -] - -[package.metadata.docs.rs] -all-features = true -targets = [ - "x86_64-pc-windows-msvc", - "x86_64-pc-windows-gnu", - "x86_64-pc-windows-gnullvm", - "aarch64-pc-windows-msvc", - "aarch64-pc-windows-gnullvm", -] diff --git a/README.MD b/README.MD index 0e1b772..b0e381b 100644 --- a/README.MD +++ b/README.MD @@ -1,17 +1,17 @@ # wslpluginapi-sys -[![Crates.io](https://img.shields.io/crates/v/wslpluginapi-sys?logo=rust)](https://crates.io/crates/wslpluginapi-sys) -[![Docs.rs](https://img.shields.io/badge/docs.rs-wslpluginapi--sys-blue?logo=docs.rs)](https://docs.rs/wslpluginapi-sys) -[![Build Status](https://github.com/mveril/wslpluginapi-sys/actions/workflows/ci.yml/badge.svg?logo=github)](https://github.com/mveril/wslpluginapi-sys/actions) -[![License](https://img.shields.io/badge/license-MIT-blue.svg?logo=license)](LICENSE) -[![Native API version](https://img.shields.io/badge/Microsoft.WSL.PluginApi-2.1.3-blue?logo=nuget)](https://www.nuget.org/packages/Microsoft.WSL.PluginApi/2.1.3) +[![Crates.io](https://img.shields.io/crates/v/wslpluginapi-sys?logo=rust)](https://crates.io/crates/wslpluginapi-sys) +[![Docs.rs](https://img.shields.io/badge/docs.rs-wslpluginapi--sys-blue?logo=docs.rs)](https://docs.rs/wslpluginapi-sys) +[![Build Status](https://github.com/mveril/wslpluginapi-sys/actions/workflows/ci.yml/badge.svg?logo=github)](https://github.com/mveril/wslpluginapi-sys/actions) +[![License](https://img.shields.io/badge/license-MIT-blue.svg?logo=license)](LICENSE) +[![Native API version](https://img.shields.io/badge/Microsoft.WSL.PluginApi-2.1.3-blue?logo=nuget)](https://www.nuget.org/packages/Microsoft.WSL.PluginApi/2.1.3) [![Platform](https://img.shields.io/badge/platform-Windows-blue?logo=windows&logoColor=white)](#) `wslpluginapi-sys` is a Rust crate that provides low-level bindings to the Windows Subsystem for Linux (WSL) Plugin API. It offers a direct interface to the functions and structures defined in the WSL Plugin API, facilitating the development of WSL plugins in Rust. ## Features -- **Comprehensive Bindings**: Provides complete bindings to the WSL Plugin API, including structures like `GUID` and other essential components. +- **Comprehensive Bindings**: Provides complete bindings to the WSL Plugin API, including structures like `WSLPluginAPIV1` or `WSLPluginHooksV1` and other essential components. - **Unsafe Abstractions**: Direct, unsafe bindings closely mirroring the original C API for maximum control and flexibility. ## Prerequisites @@ -19,7 +19,10 @@ Before using `wslpluginapi-sys`, ensure you have the following installed: - **Rust**: Latest stable version. -- **Nuget**: require nuget cli in the Path or use the `no-nuget` feature to manage the nuget packages via reqwest and zip (useful if nuget is not installed or on non windows environement). + +### Optional + +- **NuGet**: Use the NuGet CLI in the xtask nuget process to download NuGet (if not present, we use reqwest to do this process). ## Installation @@ -30,10 +33,16 @@ Add `wslpluginapi-sys` to your `Cargo.toml`: wslpluginapi-sys = "0.1.0-beta.3+2.1.3" ``` -Safety +## Safety + This crate provides unsafe bindings that closely follow the original C API. Users must ensure they uphold the necessary safety invariants when interacting with these bindings. Proper handling of pointers, memory management, and adherence to the API's expected usage patterns are crucial. -License +## NuGet package dependency + +This project depends on a third-party dependency called [Microsoft.WSL.PluginApi](https://www.nuget.org/packages/Microsoft.WSL.PluginApi) from Microsoft, available on NuGet and providing bindings for it. To upgrade it, change the version build metadata of the project and run `cargo xtask nuget` (don't forget to commit changes generated from the xtask). This xtask extracts all the needed content from the NuGet package. For more info see `THIRD-PARTY-NOTICES.md` + +## License + This project is licensed under the MIT License. See the LICENSE file for details. -Note: This crate is part of the [WSLPlugin-rs](https://github.com/mveril/wslpluginapi-sys) project, which aims to create an idiomatic Rust framework for developing WSL plugins. +Note: This crate is part of the [WSLPlugins-rs](https://github.com/mveril/wslplugins-rs) project, which aims to create an idiomatic Rust framework for developing WSL plugins. \ No newline at end of file diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..523b4cd --- /dev/null +++ b/THIRD-PARTY-NOTICES.md @@ -0,0 +1,22 @@ +# Third Party Notices + +## wslpluginapi-sys + +### Microsoft.WSL.PluginApi + +**Version :** **2.1.3** + +**License :** MIT + +**Source:** +[Microsoft.WSL.PluginApi](https://www.nuget.org/packages/Microsoft.WSL.PluginApi/2.1.3) + +#### Included files from the package. + +- `wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/include/WslPluginApi.h` unmodified +- `wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/README.MD` unmodified +- `wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/LICENSE` Generated from package metadata + +**copyright:** + +> © Microsoft Corporation. All rights reserved. diff --git a/build/nuget.rs b/build/nuget.rs deleted file mode 100644 index 964765f..0000000 --- a/build/nuget.rs +++ /dev/null @@ -1,77 +0,0 @@ -extern crate bindgen; -extern crate semver; -use cfg_if::cfg_if; -use std::{env, path::PathBuf}; - -cfg_if! { - if #[cfg(feature = "no-nuget")] { - use reqwest::blocking::get; - use zip::ZipArchive; - use tempfile::NamedTempFile; - use std::fs; - } else { - use std::process::Command; - } -} -const LOCAL_NUGET_FOLDER: &str = "nuget_packages"; - -pub(crate) fn ensure_package_installed( - package_name: &str, - package_version: &str, -) -> Result> { - let out_dir: PathBuf = env::var("OUT_DIR")?.into(); - let package_dir = out_dir.join(LOCAL_NUGET_FOLDER); - let package_output = package_dir.join(format!("{}.{}", package_name, package_version)); - - cfg_if! { - if #[cfg(feature = "no-nuget")] { - fs::create_dir_all(&package_dir)?; - - let package_url = format!( - "https://www.nuget.org/api/v2/package/{}/{}", - package_name, package_version - ); - println!("Downloading NuGet package from: {}", package_url); - - let mut response = get(&package_url)?; - if !response.status().is_success() { - return Err(format!("Failed to download NuGet package: HTTP {}", response.status()).into()); - } - - let mut temp_file = NamedTempFile::new()?; - response.copy_to(&mut temp_file)?; - - let temp_path = temp_file.path(); - let zip_file = fs::File::open(temp_path)?; - let mut archive = ZipArchive::new(zip_file)?; - - println!("Extracting NuGet package to: {:?}", package_output); - archive.extract(&package_output)?; - - } else { - println!("Installing NuGet package using NuGet CLI..."); - - let status = Command::new("nuget") - .args([ - "install", - package_name, - "-Version", - package_version, - "-OutputDirectory", - package_dir.to_str().ok_or("Invalid package directory path")?, - "-NonInteractive", - ]) - .status()?; - - if !status.success() { - return Err(format!( - "NuGet install command failed with status: {:?}", - status.code() - ).into()); - } - } - } - - println!("NuGet package installed successfully: {:?}", package_output); - Ok(package_output) -} diff --git a/wslpluginapi-sys/Cargo.toml b/wslpluginapi-sys/Cargo.toml new file mode 100644 index 0000000..4799a1c --- /dev/null +++ b/wslpluginapi-sys/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "wslpluginapi-sys" +version.workspace = true +edition.workspace = true +readme.workspace = true +authors.workspace = true +description.workspace = true +license.workspace = true +publish-lockfile = false +repository.workspace = true +keywords = ["wsl", "plugin", "windows", "linux", "ffi"] +categories = ["os::windows-apis", "external-ffi-bindings", "virtualization"] +build = "build/main.rs" + +[features] +# Enables all features that modify the library's behavior at runtime. +# Unlike build-related features, this impacts the API's functionalities. +hooks-field-names = ["dep:struct-field-names-as-array"] + +[build-dependencies] +tempfile = { version = "3.16", optional = true } +bindgen = "0.71" +cfg-if = "1.0" +constcat = "0.6.0" + + +[dependencies] +libc = "0.2" +struct-field-names-as-array = { version = "0.3", features = [ + "derive", +], optional = true } + +[dependencies.windows] +version = ">0.32" +features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_Diagnostics_Debug", + "Win32_Networking_WinSock", +] + +[package.metadata.docs.rs] +all-features = true +targets = [ + "x86_64-pc-windows-msvc", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-gnullvm", + "aarch64-pc-windows-msvc", + "aarch64-pc-windows-gnullvm", +] diff --git a/wslpluginapi-sys/THIRD-PARTY-NOTICES.md b/wslpluginapi-sys/THIRD-PARTY-NOTICES.md new file mode 100644 index 0000000..0e5ad12 --- /dev/null +++ b/wslpluginapi-sys/THIRD-PARTY-NOTICES.md @@ -0,0 +1,20 @@ +# Third Party Notices + +## Microsoft.WSL.PluginApi + +**Version :** **2.1.3** + +**License :** MIT + +**Source:** +[Microsoft.WSL.PluginApi](https://www.nuget.org/packages/Microsoft.WSL.PluginApi/2.1.3) + +### Included files from the package. + +- `third_party/Microsoft.WSL.PluginApi/include/WslPluginApi.h` unmodified +- `third_party/Microsoft.WSL.PluginApi/README.MD` unmodified +- `third_party/Microsoft.WSL.PluginApi/LICENSE` Generated from package metadata + +**copyright:** + +> © Microsoft Corporation. All rights reserved. diff --git a/build/header_processing.rs b/wslpluginapi-sys/build/header_processing.rs similarity index 100% rename from build/header_processing.rs rename to wslpluginapi-sys/build/header_processing.rs diff --git a/build/main.rs b/wslpluginapi-sys/build/main.rs similarity index 50% rename from build/main.rs rename to wslpluginapi-sys/build/main.rs index 43effe8..cc4915e 100644 --- a/build/main.rs +++ b/wslpluginapi-sys/build/main.rs @@ -1,45 +1,30 @@ extern crate bindgen; -extern crate semver; mod header_processing; use constcat::concat; -use semver::Version; use std::env; -mod nuget; -use nuget::ensure_package_installed; use std::path::PathBuf; - -const WSL_PACKAGE_NAME: &str = "Microsoft.WSL.PluginApi"; const WSL_PLUGIN_API_FILE_NAME: &str = "WslPluginApi"; -const WSL_PLUGIN_API_BINDGEN_OUTPUT_FILE_NAME: &str = concat!(WSL_PLUGIN_API_FILE_NAME, ".rs"); const WSL_PLUGIN_API_HEADER_FILE: &str = concat!(WSL_PLUGIN_API_FILE_NAME, ".h"); fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=build.rs"); let host = env::var("HOST")?; let target = env::var("TARGET")?; - - let version = Version::parse(env!("CARGO_PKG_VERSION"))?; - println!("cargo:version={}", version); - - let package_version = version.build.to_string(); let out_path: PathBuf = env::var("OUT_DIR")?.into(); - let package_path = ensure_package_installed(WSL_PACKAGE_NAME, &package_version)?; - - let header_file_path = package_path.join(format!( - "build/native/include/{}", + let manifest_dir = + PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is not set")); + let header_file_path = manifest_dir.join(format!( + "third_party/Microsoft.WSL.PluginApi/include/{}", WSL_PLUGIN_API_HEADER_FILE )); if !header_file_path.exists() { return Err(format!("Header file does not exist: {:?}", header_file_path).into()); } - let out_file = out_path.join(WSL_PLUGIN_API_BINDGEN_OUTPUT_FILE_NAME); + print!("cargo:rerun-if-changed={}", header_file_path.display()); + let out_file = out_path.join("bindings.rs"); let api_header = header_processing::process(header_file_path, host, target)?; api_header.write_to_file(&out_file)?; - println!( - "cargo:rustc-env=WSL_PLUGIN_API_BINDGEN_OUTPUT_FILE_NAME={}", - out_file.display() - ); Ok(()) } diff --git a/src/bindgen.rs b/wslpluginapi-sys/src/bindgen.rs similarity index 60% rename from src/bindgen.rs rename to wslpluginapi-sys/src/bindgen.rs index 99f7b05..2474ec1 100644 --- a/src/bindgen.rs +++ b/wslpluginapi-sys/src/bindgen.rs @@ -1,4 +1,4 @@ #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] -include!(env!("WSL_PLUGIN_API_BINDGEN_OUTPUT_FILE_NAME")); +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/src/lib.rs b/wslpluginapi-sys/src/lib.rs similarity index 100% rename from src/lib.rs rename to wslpluginapi-sys/src/lib.rs diff --git a/src/manual.rs b/wslpluginapi-sys/src/manual.rs similarity index 100% rename from src/manual.rs rename to wslpluginapi-sys/src/manual.rs diff --git a/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/LICENSE b/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/LICENSE new file mode 100644 index 0000000..415e372 --- /dev/null +++ b/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/README.MD b/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/README.MD new file mode 100644 index 0000000..53cbe92 --- /dev/null +++ b/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/README.MD @@ -0,0 +1,5 @@ +# WSL plugin api + +This package contains the `WslPluginApi.h` header which defines the WSL plugin interface. + +For more details, see: https://learn.microsoft.com/en-us/windows/wsl/ . \ No newline at end of file diff --git a/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/include/WslPluginApi.h b/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/include/WslPluginApi.h new file mode 100644 index 0000000..1187490 --- /dev/null +++ b/wslpluginapi-sys/third_party/Microsoft.WSL.PluginApi/include/WslPluginApi.h @@ -0,0 +1,130 @@ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define WSLPLUGINAPI_ENTRYPOINTV1 WSLPluginAPIV1_EntryPoint +#define WSL_E_PLUGIN_REQUIRES_UPDATE MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 0x8004032A) + +#define WSL_PLUGIN_REQUIRE_VERSION(_Major, _Minor, _Revision, Api) \ + if (Api->Version.Major < (_Major) || (Api->Version.Major == (_Major) && Api->Version.Minor < (_Minor)) || \ + (Api->Version.Major == (_Major) && Api->Version.Minor == (_Minor) && Api->Version.Revision < (_Revision))) \ + { \ + return WSL_E_PLUGIN_REQUIRES_UPDATE; \ + } + +struct WSLVersion +{ + uint32_t Major; + uint32_t Minor; + uint32_t Revision; +}; + +enum WSLUserConfiguration +{ + None = 0, + WSLUserConfigurationCustomKernel = 1, + WSLUserConfigurationCustomKernelCommandLine = 2 +}; + +#ifdef __cplusplus +DEFINE_ENUM_FLAG_OPERATORS(WSLUserConfiguration); +#endif + +struct WSLVmCreationSettings +{ + enum WSLUserConfiguration CustomConfigurationFlags; +}; + +typedef DWORD WSLSessionId; + +struct WSLSessionInformation +{ + WSLSessionId SessionId; + HANDLE UserToken; + PSID UserSid; +}; + +struct WSLDistributionInformation +{ + GUID Id; // Distribution ID, guaranteed to be the same accross reboots + LPCWSTR Name; + uint64_t PidNamespace; + LPCWSTR PackageFamilyName; // Package family name, or NULL if none + uint32_t InitPid; // Pid of the init process. Introduced in 2.0.5 +}; + +struct WslOfflineDistributionInformation +{ + GUID Id; // Distribution ID, guaranteed to be the same accross reboots + LPCWSTR Name; + LPCWSTR PackageFamilyName; // Package family name, or NULL if none +}; + +// Create plan9 mount between Windows & Linux +typedef HRESULT (*WSLPluginAPI_MountFolder)(WSLSessionId Session, LPCWSTR WindowsPath, LPCWSTR LinuxPath, BOOL ReadOnly, LPCWSTR Name); + +// Execute a program in the root namespace. +// On success, 'Socket' is connected to stdin & stdout (stderr goes to dmesg) // 'Arguments' is expected to be NULL terminated +typedef HRESULT (*WSLPluginAPI_ExecuteBinary)(WSLSessionId Session, LPCSTR Path, LPCSTR* Arguments, SOCKET* Socket); + +// Execute a program in a user distribution +// On success, 'Socket' is connected to stdin & stdout (stderr goes to dmesg) // 'Arguments' is expected to be NULL terminated +typedef HRESULT (*WSLPluginAPI_ExecuteBinaryInDistribution)(WSLSessionId Session, const GUID* Distribution, LPCSTR Path, LPCSTR* Arguments, SOCKET* Socket); + +// Set the error message to display to the user if the VM or distribution creation fails. +// Must be called synchronously in either OnVMStarted() or OnDistributionStarted(). +typedef HRESULT (*WSLPluginAPI_PluginError)(LPCWSTR UserMessage); + +// Synchronous notifications sent to the plugin + +// Called when the VM has started. +// 'Session' and 'UserSettings' are only valid during while the call is in progress. +typedef HRESULT (*WSLPluginAPI_OnVMStarted)(const struct WSLSessionInformation* Session, const struct WSLVmCreationSettings* UserSettings); + +// Called when the VM is about to stop. +// 'Session' is only valid during while the call is in progress. +typedef HRESULT (*WSLPluginAPI_OnVMStopping)(const struct WSLSessionInformation* Session); + +// Called when a distribution has started. +// 'Session' and 'Distribution' is only valid during while the call is in progress. +typedef HRESULT (*WSLPluginAPI_OnDistributionStarted)(const struct WSLSessionInformation* Session, const struct WSLDistributionInformation* Distribution); + +// Called when a distribution is about to stop. +// 'Session' and 'Distribution' is only valid during while the call is in progress. +// Note: It's possible that stopping a distribution fails (for instance if a file is in use). +// In this case, it's possible for this notification to be called multiple times for the same distribution. +typedef HRESULT (*WSLPluginAPI_OnDistributionStopping)(const struct WSLSessionInformation* Session, const struct WSLDistributionInformation* Distribution); + +// Called when a distribution is registered or unregisteed. +// Returning failure will NOT cause the operation to fail. +typedef HRESULT (*WSLPluginAPI_OnDistributionRegistered)(const struct WSLSessionInformation* Session, const struct WslOfflineDistributionInformation* Distribution); + +struct WSLPluginHooksV1 +{ + WSLPluginAPI_OnVMStarted OnVMStarted; + WSLPluginAPI_OnVMStopping OnVMStopping; + WSLPluginAPI_OnDistributionStarted OnDistributionStarted; + WSLPluginAPI_OnDistributionStopping OnDistributionStopping; + WSLPluginAPI_OnDistributionRegistered OnDistributionRegistered; // Introduced in 2.1.2 + WSLPluginAPI_OnDistributionRegistered OnDistributionUnregistered; // Introduced in 2.1.2 +}; + +struct WSLPluginAPIV1 +{ + struct WSLVersion Version; + WSLPluginAPI_MountFolder MountFolder; + WSLPluginAPI_ExecuteBinary ExecuteBinary; + WSLPluginAPI_PluginError PluginError; + WSLPluginAPI_ExecuteBinaryInDistribution ExecuteBinaryInDistribution; // Introduced in 2.1.2 +}; + +typedef HRESULT (*WSLPluginAPI_EntryPointV1)(const struct WSLPluginAPIV1* Api, struct WSLPluginHooksV1* Hooks); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..671337e --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +chrono = { version = "0.4", default-features = false, features = ["now"] } +clap = { version = "4.0", features = ["derive"] } +cargo_metadata = "0.20" +reqwest = { version = "0.12", features = ["blocking"] } +tempfile = "3.19" +zip = "4.0" +serde = { version = "1.0", features = ["derive"] } +serde-xml-rs = "0.8" +spdx = { version = "0.10", features = ["text"] } +log = "0.4" +env_logger = "0.11" +clap-verbosity-flag = "3.0" +anyhow = "1.0" +regex = { version = "1.11", features = ["use_std"] } +thiserror = "2.0" +markdown-builder = "1.0" +walkdir = "2.5" diff --git a/xtask/src/licence_definition.rs b/xtask/src/licence_definition.rs new file mode 100644 index 0000000..23eec95 --- /dev/null +++ b/xtask/src/licence_definition.rs @@ -0,0 +1,44 @@ +use regex::Regex; +use spdx::Expression; +use std::sync::LazyLock; + +static YEAR_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\s*").unwrap()); + +#[derive(Debug, Clone)] +pub struct LicenseDefinition { + pub license: Expression, + pub year: Option, + pub holders: String, +} + +impl LicenseDefinition { + /// Create a new license definition. + pub fn new(license: Expression, year: Option, holders: impl Into) -> Self { + LicenseDefinition { + license, + year, + holders: holders.into(), + } + } + + /// Generate the license body with placeholders replaced by year and holders. + pub fn generate_body(&self) -> Vec { + self.license + .requirements() + .flat_map(|req| req.req.license.id()) + .map(|id| { + let raw_text = id.text(); + let text_with_holders = + raw_text.replace("", self.holders.as_ref()); + if let Some(year) = self.year { + // Replace placeholder with the actual year + text_with_holders.replace("", &year.to_string()) + } else { + // Remove placeholders if no year is specified + let without_year = YEAR_REGEX.replace_all(&text_with_holders, ""); + without_year.into_owned() + } + }) + .collect() + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..60fa699 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,297 @@ +mod licence_definition; +mod nuget; +mod nuspec; +mod third_pary_mangement; + +use anyhow::Result; +use nuspec::LicenceContent; +use std::{fs, io::Write, path::Path}; +use third_pary_mangement::{ + DistributedFile, Status, + notice::{NoticeGeneration, ThirdPartyNotice, ThirdPartyNoticeItem, ThirdPartyNoticePackage}, +}; +use walkdir::WalkDir; + +use crate::nuget::{Mode, ensure_package_installed}; +use clap::{Parser, Subcommand, builder::OsStr}; +use clap_verbosity_flag::{InfoLevel, Verbosity}; +use env_logger; +use log::{debug, error, info, trace, warn}; +use zip::ZipArchive; + +/// Tâches de build et développement personnalisées pour le projet. +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[command(flatten)] + verbose: Verbosity, + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Nuget, +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + // Configure le logger selon le niveau de verbosité + env_logger::Builder::new() + .filter_level(cli.verbose.log_level_filter()) + .init(); + + match &cli.command { + Commands::Nuget => { + info!("Running Nuget command..."); + let metadata = fetch_cargo_metadata()?; + + let workspace_root: &Path = &metadata.workspace_root.as_ref(); + let mut notice = ThirdPartyNotice::default(); + for package in metadata.workspace_packages() { + if package + .manifest_path + .parent() + .map_or(true, |p| p.as_str() != env!("CARGO_MANIFEST_DIR")) + { + notice.push(process_package(package, workspace_root)?); + } else { + info!("Skipping package: {}", package.name); + continue; + }; + } + notice.generate_notice(&workspace_root.join("THIRD-PARTY-NOTICES.md"))?; + Ok(()) + } + } +} + +fn fetch_cargo_metadata() -> Result { + debug!("Fetching cargo metadata..."); + let metadata = cargo_metadata::MetadataCommand::new().exec()?; + trace!("Full cargo metadata: {:#?}", metadata); + Ok(metadata) +} + +fn process_package( + package: &cargo_metadata::Package, + workspace_root: &Path, +) -> Result { + debug!("Processing package: {}", package.name); + + let version = &package.version; + let nuget_package_version = &version.build; + debug!( + "Package '{}' build metadata version: {:?}", + package.name, nuget_package_version + ); + if nuget_package_version.is_empty() { + warn!("No version found for package: {}", package.name); + return Ok(ThirdPartyNoticePackage::new(package.name.clone())); + } + + let nuget_package_name = "Microsoft.WSL.PluginApi"; + debug!( + "Ensuring NuGet package installed: {} @ {}", + nuget_package_name, nuget_package_version + ); + + let nuget_pkg_path = ensure_package_installed( + nuget_package_name, + nuget_package_version.as_str(), + workspace_root, + Mode::TryNuget, + )?; + debug!("NuGet package path: {}", nuget_pkg_path.display()); + + let third_party_dir = package.manifest_path.parent().unwrap().join("third_party"); + let third_party_wsl_nuget_dir = third_party_dir.join(nuget_package_name); + prepare_third_party_dirs( + &third_party_dir.as_std_path(), + &third_party_wsl_nuget_dir.as_std_path(), + )?; + let nuspec_data = get_nuspec_from_nupkg( + &nuget_pkg_path, + nuget_package_name, + nuget_package_version.as_str(), + )? + .unwrap(); + let licence: Option = nuspec_data.metadata.get_licence_content()?; + let mut notice_item = ThirdPartyNoticeItem::new( + nuget_package_name.into(), + nuspec_data.metadata.version.clone(), + format!( + "https://www.nuget.org/packages/{}/{}", + nuspec_data.metadata.id, nuspec_data.metadata.version, + ), + nuspec_data.metadata.copyright.clone(), + licence, + ); + let headers = copy_native_headers(&nuget_pkg_path, &third_party_wsl_nuget_dir.as_std_path())?; + notice_item.files_mut().extend(headers); + let readme: Option = handle_readme( + &nuspec_data, + nuget_pkg_path.as_ref(), + third_party_wsl_nuget_dir.as_ref(), + )?; + notice_item.files_mut().extend(readme.into_iter()); + let licenses = handle_license( + &nuspec_data, + nuget_pkg_path.as_ref(), + third_party_wsl_nuget_dir.as_ref(), + )?; + notice_item.files_mut().extend(licenses.into_iter()); + let mut notice = ThirdPartyNoticePackage::new(package.name.clone()); + notice.push(notice_item); + notice.generate_notice( + &package + .manifest_path + .parent() + .unwrap() + .join("THIRD-PARTY-NOTICES.md"), + )?; + Ok(notice) +} + +fn prepare_third_party_dirs( + third_party_dir: &Path, + third_party_wsl_nuget_dir: &Path, +) -> Result<()> { + debug!( + "Creating third_party directory at: {}", + third_party_dir.display() + ); + fs::create_dir_all(third_party_dir)?; + debug!( + "Creating third_party directory for the package at: {}", + third_party_wsl_nuget_dir.display() + ); + fs::create_dir_all(third_party_wsl_nuget_dir)?; + Ok(()) +} + +fn copy_native_headers( + nuget_pkg_path: &Path, + third_party_nuget_package_dir: &Path, +) -> Result> { + debug!("Copying native headers..."); + let nuget_native_path = nuget_pkg_path.join("build/native"); + let mut vec = Vec::with_capacity(1); + for entry in WalkDir::new(&nuget_native_path) { + let entry = entry?; + let path = entry.path(); + let result_path = + third_party_nuget_package_dir.join(path.strip_prefix(&nuget_native_path).unwrap()); + if path.is_dir() { + debug!("Copying directory: {}", path.display()); + fs::create_dir_all(&result_path)?; + } else { + debug!("Copying file: {}", path.display()); + fs::create_dir_all(&result_path.parent().unwrap())?; + fs::copy(&path, &result_path)?; + let distributed_file = DistributedFile::new(result_path, Status::Unmodified); + vec.push(distributed_file); + } + } + Ok(vec.into_boxed_slice()) +} + +fn get_nuspec_from_nupkg( + nuget_pkg_path: &Path, + nuget_package_name: &str, + nuget_package_version: &str, +) -> Result> { + let nuspec_name = format!("{}.nuspec", nuget_package_name); + debug!("Looking for nuspec file: {}", nuspec_name); + + let zip_file = fs::File::open(&nuget_pkg_path.join(format!( + "{}.{}.nupkg", + nuget_package_name, nuget_package_version + )))?; + let mut archive = ZipArchive::new(zip_file)?; + trace!("ZIP archive opened with {} files", archive.len()); + match archive.by_name(&nuspec_name) { + Ok(nuspec_file) => { + debug!("Found .nuspec file: {}", nuspec_name); + let package_data: nuspec::Package = serde_xml_rs::from_reader(nuspec_file)?; + trace!("Parsed nuspec data: {:#?}", package_data); + Ok(Some(package_data)) + } + Err(_) => { + warn!( + "Warning: .nuspec file '{}' not found inside {}", + nuspec_name, + nuget_pkg_path.display() + ); + Ok(None) + } + } +} + +fn handle_readme( + package_data: &nuspec::Package, + nuget_pkg_path: &Path, + third_party_wsl_nuget_dir: &Path, +) -> Result> { + if let Some(readme_nuget_path) = package_data.metadata.readme.as_deref() { + let readme_path = third_party_wsl_nuget_dir.join( + &readme_nuget_path + .file_name() + .unwrap_or(&OsStr::from("README")), + ); + debug!("Copying README file to: {}", readme_path.display()); + fs::copy(nuget_pkg_path.join(readme_nuget_path), &readme_path)?; + return Ok(Some(DistributedFile::new(readme_path, Status::Unmodified))); + } else { + info!("No README file specified in nuspec."); + } + Ok(None) +} + +fn handle_license( + package_data: &nuspec::Package, + nuget_pkg_path: &Path, + third_party_wsl_nuget_dir: &Path, +) -> Result> { + let some_licence_content = package_data.metadata.get_licence_content()?; + if let Some(licence_content) = some_licence_content { + match licence_content { + LicenceContent::Body(body) => { + debug!("License file or expression found in nuspec."); + match body { + nuspec::LicenceBody::Generator(generator) => { + debug!("License generator found in nuspec."); + let license_body = generator.generate_body(); + let license_path = if license_body.len() == 1 { + third_party_wsl_nuget_dir.join("LICENSE") + } else { + third_party_wsl_nuget_dir.join("LICENSES") + }; + debug!("Writing license to: {}", &license_path.display()); + fs::File::create(&license_path)? + .write_all(license_body.join("\n\n").as_bytes())?; + Ok(Some(DistributedFile::new( + license_path, + Status::PackageMetadataGenerated, + ))) + } + nuspec::LicenceBody::File(file) => { + debug!("License file found in nuspec."); + let license_path = third_party_wsl_nuget_dir.join("LICENSE"); + debug!("Copy license to: {}", &license_path.display()); + fs::copy(nuget_pkg_path.join(file), &license_path)?; + Ok(Some(DistributedFile::new(license_path, Status::Unmodified))) + } + } + } + LicenceContent::URL(url) => { + debug!("License URL found in nuspec: {}", url); + Ok(None) + } + } + } else { + debug!("No license file or expression specified in nuspec."); + Ok(None) + } +} diff --git a/xtask/src/nuget.rs b/xtask/src/nuget.rs new file mode 100644 index 0000000..970fe54 --- /dev/null +++ b/xtask/src/nuget.rs @@ -0,0 +1,106 @@ +use anyhow::Result; +use reqwest::blocking::get; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::process::ExitStatus; +use tempfile::NamedTempFile; +use zip::ZipArchive; + +#[allow(dead_code)] +#[derive(Default)] +pub(crate) enum Mode { + #[default] + TryNuget, + NoNuget, + Nuget, +} + +const LOCAL_NUGET_FOLDER: &str = "nuget_packages"; + +/// Ensures that a NuGet package is installed into OUT_DIR/nuget_packages. +/// +/// # Arguments +/// * `package_name` - The identifier of the NuGet package. +/// * `package_version` - The version string of the package. +/// * `out_dir` - The output directory for the installation. +/// * `mode` - Controls whether to use the NuGet CLI or manual download. +/// +/// # Returns +/// The path to the extracted or installed package folder. +pub(crate) fn ensure_package_installed, S: AsRef>( + package_name: S, + package_version: S, + out_dir: P, + mode: Mode, +) -> Result { + let out_dir = out_dir.as_ref(); + let package_name = package_name.as_ref(); + let package_version = package_version.as_ref(); + let package_dir = out_dir.join(LOCAL_NUGET_FOLDER); + let package_output = package_dir.join(format!("{}.{}", package_name, package_version)); + + fs::create_dir_all(&package_dir)?; + + match mode { + Mode::Nuget => install_with_nuget_cli(package_name, package_version, &package_dir)?, + Mode::NoNuget => download_and_extract(package_name, package_version, &package_output)?, + Mode::TryNuget => { + if let Err(e) = install_with_nuget_cli(package_name, package_version, &package_dir) { + println!( + "NuGet CLI failed: {}. Falling back to manual download...", + e + ); + download_and_extract(package_name, package_version, &package_output)?; + } + } + } + + println!("NuGet package installed successfully: {:?}", package_output); + Ok(package_output) +} + +fn install_with_nuget_cli( + package_name: &str, + package_version: &str, + package_dir: &Path, +) -> Result { + println!("Installing NuGet package using NuGet CLI..."); + let status = Command::new("nuget") + .args([ + "install", + package_name, + "-Version", + package_version, + "-OutputDirectory", + package_dir.to_str().unwrap(), + "-NonInteractive", + ]) + .status()?; + Ok(status) +} + +fn download_and_extract( + package_name: &str, + package_version: &str, + package_output: &Path, +) -> Result<()> { + let package_url = format!( + "https://www.nuget.org/api/v2/package/{}/{}", + package_name, package_version + ); + println!("Downloading NuGet package from: {}", package_url); + + let mut response = get(&package_url)?.error_for_status()?; + + let mut temp_file = NamedTempFile::new()?; + response.copy_to(&mut temp_file)?; + + let zip_file = fs::File::open(temp_file.path())?; + let mut archive = ZipArchive::new(zip_file)?; + println!("Extracting NuGet package to: {:?}", package_output); + archive.extract(package_output)?; + + Ok(()) +} diff --git a/xtask/src/nuspec.rs b/xtask/src/nuspec.rs new file mode 100644 index 0000000..fbe1a98 --- /dev/null +++ b/xtask/src/nuspec.rs @@ -0,0 +1,155 @@ +use crate::licence_definition::LicenseDefinition; +use anyhow::{Ok, Result}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use spdx::Expression; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "package")] +pub struct Package { + #[serde(rename = "metadata")] + pub metadata: Metadata, +} +#[derive(Debug, Clone)] +pub enum LicenceContent { + Body(LicenceBody), + URL(String), +} + +impl From for LicenceContent { + fn from(body: LicenceBody) -> Self { + LicenceContent::Body(body) + } +} + +#[derive(Debug, Clone)] +pub enum LicenceBody { + Generator(LicenseDefinition), + File(String), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Metadata { + pub id: String, + pub version: String, + pub authors: String, + #[serde(default)] + pub owners: Option, + #[serde(default)] + pub readme: Option, + #[serde(default)] + pub copyright: Option, + pub description: String, + #[serde(default)] + pub release_notes: Option, + #[serde(default)] + pub tags: Option, + #[serde(default)] + pub project_url: Option, + #[serde(default)] + pub license_url: Option, + #[serde(default)] + pub license: Option, + #[serde(default)] + pub require_license_acceptance: Option, + #[serde(default)] + pub dependencies: Option, +} + +impl Metadata { + pub fn get_year(&self) -> Option { + let re = Regex::new(r"\d{4}").unwrap(); + self.copyright + .as_deref() + .map(|copyright| re.captures(©right).map(|year| year[0].parse().unwrap())) + .flatten() + } + + pub fn get_holders(&self) -> &str { + if let Some(owners) = &self.owners { + owners + } else { + &self.authors + } + } + + pub fn get_licence_content(&self) -> Result> { + let year = self.get_year(); + let holders = self.get_holders(); + Ok(if let Some(license) = &self.license { + Some(LicenceContent::Body(license.get_body(year, &holders)?)) + } else if let Some(license_url) = &self.license_url { + Some(LicenceContent::URL(license_url.clone())) + } else { + None + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum LicenseType { + Expression, + File, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct License { + #[serde(rename = "type")] + pub kind: LicenseType, + #[serde(rename = "$value")] + value: String, +} + +impl License { + pub fn get_body(&self, year: Option, holders: &str) -> Result { + let result = match self.kind { + LicenseType::File => { + let path = Path::new(&self.value); + LicenceBody::File(fs::read_to_string(path)?) + } + LicenseType::Expression => LicenceBody::Generator(LicenseDefinition::new( + Expression::parse(&self.value)?, + year, + holders, + )), + }; + Ok(result) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "dependencies")] +pub struct Dependencies { + #[serde(rename = "group")] + #[serde(default)] + pub group: Vec, + #[serde(rename = "dependency")] + #[serde(default)] + pub dependency: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "group")] +pub struct DependencyGroup { + #[serde(rename = "@targetFramework")] + #[serde(default)] + pub target_framework: Option, + #[serde(rename = "dependency")] + pub dependency: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename = "dependency")] +pub struct Dependency { + #[serde(rename = "@id")] + pub id: String, + #[serde(rename = "@version")] + pub version: String, + #[serde(rename = "@exclude")] + #[serde(default)] + pub exclude: Option, +} diff --git a/xtask/src/third_pary_mangement/distributed_file.rs b/xtask/src/third_pary_mangement/distributed_file.rs new file mode 100644 index 0000000..e500e27 --- /dev/null +++ b/xtask/src/third_pary_mangement/distributed_file.rs @@ -0,0 +1,31 @@ +use std::{fmt::Display, path::PathBuf}; + +#[derive(Debug, Clone)] +pub struct DistributedFile { + pub path: PathBuf, + pub status: Status, +} + +impl DistributedFile { + pub fn new(path: PathBuf, status: Status) -> Self { + Self { path, status } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + Modified, + Unmodified, + PackageMetadataGenerated, +} + +impl Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let output = match self { + Status::Modified => "modified", + Status::Unmodified => "unmodified", + Status::PackageMetadataGenerated => "generated from package metadata", + }; + f.write_str(output) + } +} diff --git a/xtask/src/third_pary_mangement/mod.rs b/xtask/src/third_pary_mangement/mod.rs new file mode 100644 index 0000000..d40855d --- /dev/null +++ b/xtask/src/third_pary_mangement/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod distributed_file; +pub(crate) mod notice; +pub use distributed_file::{DistributedFile, Status}; diff --git a/xtask/src/third_pary_mangement/notice/mod.rs b/xtask/src/third_pary_mangement/notice/mod.rs new file mode 100644 index 0000000..77c5276 --- /dev/null +++ b/xtask/src/third_pary_mangement/notice/mod.rs @@ -0,0 +1,8 @@ +mod notice_generation; +mod third_party_notice; +mod third_party_notice_item; +mod third_party_notice_package; +pub use notice_generation::NoticeGeneration; +pub use third_party_notice::ThirdPartyNotice; +pub use third_party_notice_item::ThirdPartyNoticeItem; +pub use third_party_notice_package::ThirdPartyNoticePackage; diff --git a/xtask/src/third_pary_mangement/notice/notice_generation.rs b/xtask/src/third_pary_mangement/notice/notice_generation.rs new file mode 100644 index 0000000..250d112 --- /dev/null +++ b/xtask/src/third_pary_mangement/notice/notice_generation.rs @@ -0,0 +1,28 @@ +use markdown_builder::Markdown; +use std::{borrow::Cow, fs, path::Path}; +pub trait NoticeGeneration { + fn generate_notice>(&self, output_path: P) -> std::io::Result<()> { + let output_path = if output_path.as_ref().is_absolute() { + Cow::Borrowed(output_path.as_ref()) + } else { + Cow::Owned(output_path.as_ref().canonicalize()?) + }; + let mut md = Markdown::new(); + Self::header_generation(&mut md); + self.generate_content_in_place(&mut md, &output_path, 1)?; + fs::create_dir_all(output_path.parent().unwrap())?; + fs::write(output_path, md.render())?; + Ok(()) + } + + fn generate_content_in_place>( + &self, + md: &mut Markdown, + output_path: P, + header_level: usize, + ) -> std::io::Result<()>; + + fn header_generation(md: &mut Markdown) { + md.header1("Third Party Notices"); + } +} diff --git a/xtask/src/third_pary_mangement/notice/third_party_notice.rs b/xtask/src/third_pary_mangement/notice/third_party_notice.rs new file mode 100644 index 0000000..cc04a16 --- /dev/null +++ b/xtask/src/third_pary_mangement/notice/third_party_notice.rs @@ -0,0 +1,91 @@ +use std::path::Path; + +use super::{ThirdPartyNoticePackage, notice_generation::NoticeGeneration}; + +#[derive(Debug, Clone, Default)] +pub struct ThirdPartyNotice { + packages: Vec, +} + +impl ThirdPartyNotice { + pub fn add_item(&mut self, item: ThirdPartyNoticePackage) { + self.packages.push(item); + } +} + +impl NoticeGeneration for ThirdPartyNotice { + fn generate_content_in_place>( + &self, + md: &mut markdown_builder::Markdown, + output_path: P, + header_level: usize, + ) -> std::io::Result<()> { + let add_header = self.len() > 0; + let package_level = if add_header { + header_level + 1 + } else { + header_level + }; + for package in self { + if add_header { + md.header(package.name(), header_level + 1); + } + package.generate_content_in_place(md, &output_path, package_level)?; + } + Ok(()) + } +} + +impl std::ops::Deref for ThirdPartyNotice { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.packages + } +} + +impl std::ops::DerefMut for ThirdPartyNotice { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.packages + } +} + +impl std::iter::FromIterator for ThirdPartyNotice { + fn from_iter>(iter: I) -> Self { + Self { + packages: Vec::from_iter(iter), + } + } +} + +impl IntoIterator for ThirdPartyNotice { + type Item = ThirdPartyNoticePackage; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.packages.into_iter() + } +} + +impl<'a> IntoIterator for &'a ThirdPartyNotice { + type Item = &'a ThirdPartyNoticePackage; + type IntoIter = std::slice::Iter<'a, ThirdPartyNoticePackage>; + + fn into_iter(self) -> Self::IntoIter { + self.packages.iter() + } +} + +impl<'a> IntoIterator for &'a mut ThirdPartyNotice { + type Item = &'a mut ThirdPartyNoticePackage; + type IntoIter = std::slice::IterMut<'a, ThirdPartyNoticePackage>; + + fn into_iter(self) -> Self::IntoIter { + self.packages.iter_mut() + } +} + +impl Extend for ThirdPartyNotice { + fn extend>(&mut self, iter: T) { + self.packages.extend(iter); + } +} diff --git a/xtask/src/third_pary_mangement/notice/third_party_notice_item.rs b/xtask/src/third_pary_mangement/notice/third_party_notice_item.rs new file mode 100644 index 0000000..6e017b0 --- /dev/null +++ b/xtask/src/third_pary_mangement/notice/third_party_notice_item.rs @@ -0,0 +1,174 @@ +use chrono::format; +use log::debug; +use markdown_builder::{BlockQuote, Bold, Inline, Link, List}; + +use crate::{nuspec::LicenceContent, third_pary_mangement::distributed_file::DistributedFile}; +use std::{borrow::Cow, path::Path}; + +use super::notice_generation::NoticeGeneration; + +#[derive(Debug, Clone)] +pub struct ThirdPartyNoticeItem { + name: String, + version: String, + link: String, + copyright: Option, + license: Option, + files: Vec, +} + +impl NoticeGeneration for ThirdPartyNoticeItem { + fn generate_content_in_place>( + &self, + md: &mut markdown_builder::Markdown, + output_path: P, + header_level: usize, + ) -> std::io::Result<()> { + md.header(self.name(), header_level); + md.paragraph(&format!( + "{} {}", + "Version :".to_bold(), + self.version().to_bold() + )); + let licence_txt = if let Some(license) = self.license() { + match license { + crate::nuspec::LicenceContent::Body(licence_body) => match licence_body { + crate::nuspec::LicenceBody::Generator(license_definition) => { + Cow::Borrowed(license_definition.license.as_ref()) + } + crate::nuspec::LicenceBody::File(_) => { + Cow::Borrowed("See the LICENSE file for details.") + } + }, + crate::nuspec::LicenceContent::URL(url) => Cow::Owned(format!( + "See the {} for details.", + Link::builder().text("license").url(url).inlined().build() + )), + } + } else { + Cow::Borrowed("Not specified") + }; + md.paragraph(format!("{} {}", "License :".to_bold(), licence_txt)); + md.paragraph(format!( + "{} {}", + "Source:".to_bold(), + Link::builder() + .text(self.link()) + .url(self.name()) + .footer(false) + .inlined() + .build() + )); + + if !self.files().is_empty() { + md.header("Included files from the package.", header_level + 1); + } + let md_files_list = self + .files() + .iter() + .fold(List::builder(), |builder, file| { + debug!("Adding file to list: {:?}", &file.path); + builder.append(format!( + "{} {}", + file.path + .strip_prefix(output_path.as_ref().parent().unwrap()) + .unwrap() + .to_string_lossy() + .replace("\\", "/") + .to_inline(), + file.status + )) + }) + .unordered(); + md.list(md_files_list); + if let Some(copyright) = self.copyright() { + md.paragraph("copyright:".to_bold()) + .paragraph(copyright.to_block_quote()); + } + Ok(()) + } +} + +impl ThirdPartyNoticeItem { + pub fn new( + name: String, + version: String, + link: String, + copyright: Option, + license: Option, + ) -> Self { + Self { + name, + version, + link, + copyright, + license, + files: Vec::default(), + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn version(&self) -> &str { + &self.version + } + + pub fn link(&self) -> &str { + &self.link + } + + pub fn copyright(&self) -> Option<&str> { + self.copyright.as_deref() + } + + pub fn license(&self) -> Option<&LicenceContent> { + self.license.as_ref() + } + + pub fn files(&self) -> &Vec { + &self.files + } + + pub fn files_mut(&mut self) -> &mut Vec { + &mut self.files + } + + pub fn add_file(&mut self, file: DistributedFile) { + self.files.push(file); + } + + // Méthodes utiles de collection + pub fn len(&self) -> usize { + self.files.len() + } + + pub fn is_empty(&self) -> bool { + self.files.is_empty() + } + + pub fn iter(&self) -> std::slice::Iter<'_, DistributedFile> { + self.files.iter() + } + + pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, DistributedFile> { + self.files.iter_mut() + } + + pub fn clear(&mut self) { + self.files.clear(); + } + + pub fn remove(&mut self, index: usize) -> DistributedFile { + self.files.remove(index) + } + + pub fn get(&self, index: usize) -> Option<&DistributedFile> { + self.files.get(index) + } + + pub fn get_mut(&mut self, index: usize) -> Option<&mut DistributedFile> { + self.files.get_mut(index) + } +} diff --git a/xtask/src/third_pary_mangement/notice/third_party_notice_package.rs b/xtask/src/third_pary_mangement/notice/third_party_notice_package.rs new file mode 100644 index 0000000..ee11336 --- /dev/null +++ b/xtask/src/third_pary_mangement/notice/third_party_notice_package.rs @@ -0,0 +1,82 @@ +use std::path::Path; + +use super::{ThirdPartyNoticeItem, notice_generation::NoticeGeneration}; + +#[derive(Debug, Clone)] +pub struct ThirdPartyNoticePackage { + name: String, + items: Vec, +} + +impl ThirdPartyNoticePackage { + pub fn new(name: String) -> Self { + Self { + name, + items: Vec::new(), + } + } + + pub fn name(&self) -> &str { + &self.name + } +} + +impl NoticeGeneration for ThirdPartyNoticePackage { + fn generate_content_in_place>( + &self, + md: &mut markdown_builder::Markdown, + output_path: P, + header_level: usize, + ) -> std::io::Result<()> { + for item in self { + item.generate_content_in_place(md, &output_path, header_level + 1)?; + } + Ok(()) + } +} + +impl std::ops::Deref for ThirdPartyNoticePackage { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.items + } +} + +impl std::ops::DerefMut for ThirdPartyNoticePackage { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.items + } +} + +impl IntoIterator for ThirdPartyNoticePackage { + type Item = ThirdPartyNoticeItem; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} + +impl<'a> IntoIterator for &'a ThirdPartyNoticePackage { + type Item = &'a ThirdPartyNoticeItem; + type IntoIter = std::slice::Iter<'a, ThirdPartyNoticeItem>; + + fn into_iter(self) -> Self::IntoIter { + self.items.iter() + } +} + +impl<'a> IntoIterator for &'a mut ThirdPartyNoticePackage { + type Item = &'a mut ThirdPartyNoticeItem; + type IntoIter = std::slice::IterMut<'a, ThirdPartyNoticeItem>; + + fn into_iter(self) -> Self::IntoIter { + self.items.iter_mut() + } +} + +impl Extend for ThirdPartyNoticePackage { + fn extend>(&mut self, iter: T) { + self.items.extend(iter); + } +}