diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..2fa2a2f --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,37 @@ +name: Nix build + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + kbuildx: + name: kbuildx (${{ matrix.system }}) + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-latest + system: x86_64-linux + - runner: ubuntu-24.04-arm + system: aarch64-linux + - runner: macos-15 + system: aarch64-darwin + runs-on: ${{ matrix.runner }} + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Build kbuildx + run: nix build ".#packages.${{ matrix.system }}.kbuildx" --print-build-logs + + - name: Verify packaged CLI + run: ./result/bin/kbuildx --version diff --git a/flake.lock b/flake.lock index faa925e..c8fca5d 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,119 @@ { "nodes": { + "bsdkrun": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils", + "libkrun-pvh": "libkrun-pvh", + "nixpkgs": [ + "nixpkgs" + ], + "solo5": "solo5" + }, + "locked": { + "lastModified": 1786696945, + "narHash": "sha256-NOsdxSiNQTHPW2QqFM8WIojm2DFaU/dV3ojujpYfw+8=", + "owner": "tsirysndr", + "repo": "bsdkrun", + "rev": "a4dfebef6959ad55307a459192953f4e2c7c2397", + "type": "github" + }, + "original": { + "owner": "tsirysndr", + "repo": "bsdkrun", + "type": "github" + } + }, + "crane": { + "locked": { + "lastModified": 1785541369, + "narHash": "sha256-6VCYI5xashcnAR1lambqt9FGh0F6kYhg1P2Z0VDlr48=", + "owner": "ipetkov", + "repo": "crane", + "rev": "508ff4829aefddee8ea62291e86b144b58365724", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "crane_2": { + "locked": { + "lastModified": 1785782307, + "narHash": "sha256-MPaRdVkf6zZP5fCPxYCi8Dr4pZzgmXzg8T9nVEbp3Mw=", + "owner": "ipetkov", + "repo": "crane", + "rev": "2c71e194474d13de031d729b729c968ddbe3507f", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "libkrun-pvh": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1785699516, + "narHash": "sha256-nW0ZBqNuuOO8qY/cqkfvYScTi++1ozvUjwdRioZgACo=", + "owner": "tsirysndr", + "repo": "libkrun", + "rev": "c3cea52a141e72a6211e5b0abc394df82f78d12a", + "type": "github" + }, + "original": { + "owner": "tsirysndr", + "ref": "feat/pvh-boot", + "repo": "libkrun", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1753939845, - "narHash": "sha256-K2ViRJfdVGE8tpJejs8Qpvvejks1+A4GQej/lBk5y7I=", + "lastModified": 1785571196, + "narHash": "sha256-KoTsyMQqnXQZq8deCEnu4QkyldkwH/bpMMhUcfMdGIw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1786599213, + "narHash": "sha256-yNJd40f11EzXBjSByCB7IPpeFFAdeoSKKM67dGkfFoU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "94def634a20494ee057c76998843c015909d6311", + "rev": "0e251e24a4f24e036a084b6b4b2d2491af4167f4", "type": "github" }, "original": { @@ -18,7 +125,41 @@ }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "bsdkrun": "bsdkrun", + "crane": "crane_2", + "nixpkgs": "nixpkgs_2" + } + }, + "solo5": { + "flake": false, + "locked": { + "lastModified": 1786144622, + "narHash": "sha256-VhQZh6B6Wq2hqTJwBQKJoLwd33tvHeWeef4RamZ7zZ0=", + "owner": "tsirysndr", + "repo": "solo5", + "rev": "994556ca8b727f905a5040cfff42ff0e4daeddd3", + "type": "github" + }, + "original": { + "owner": "tsirysndr", + "ref": "hvf-macos-aarch64", + "repo": "solo5", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index 5bc6e79..c4197b6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,34 +1,75 @@ { - description = "Dev environment with Linux build dependencies"; + description = "Linux kernel builder and development environment"; inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + crane = { + url = "github:ipetkov/crane"; + }; + bsdkrun = { + url = "github:tsirysndr/bsdkrun"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, ... }: - + outputs = { nixpkgs, crane, bsdkrun, ... }: let - system = "x86_64-linux"; - pkgs = import nixpkgs { - inherit system; - }; + systems = [ + "aarch64-darwin" + "x86_64-linux" + "aarch64-linux" + ]; + forAllSystems = function: + builtins.listToAttrs (map (system: { + name = system; + value = function system; + }) systems); in { - devShells.${system}.default = pkgs.mkShell { - buildInputs = with pkgs; [ - deno - curl - git - gcc - flex - bison - ncurses.dev - openssl.dev - bc - elfutils.dev - pahole - pkg-config - perl - ]; - }; + packages = forAllSystems (system: + let + pkgs = import nixpkgs { inherit system; }; + craneLib = crane.mkLib pkgs; + commonArgs = { + pname = "kbuildx"; + version = "0.1.0"; + src = craneLib.cleanCargoSource ./kbuildx; + strictDeps = true; + nativeBuildInputs = [ pkgs.makeWrapper ]; + }; + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + kbuildx = craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; + postInstall = '' + wrapProgram $out/bin/kbuildx \ + --prefix PATH : ${pkgs.lib.makeBinPath [ bsdkrun.packages.${system}.default ]} + ''; + }); + in { + inherit kbuildx; + default = kbuildx; + }); + + devShells = forAllSystems (system: + let + pkgs = import nixpkgs { inherit system; }; + in { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + deno + curl + git + gcc + flex + bison + ncurses.dev + openssl.dev + bc + elfutils.dev + pahole + pkg-config + perl + ]; + }; + }); }; } diff --git a/kbuildx/README.md b/kbuildx/README.md index a6e507d..555d083 100644 --- a/kbuildx/README.md +++ b/kbuildx/README.md @@ -118,7 +118,7 @@ The editor starts with the currently selected kernel version, CPU count, and mem ### Live build logs -Build stdout and stderr are read concurrently as raw chunks and rendered as they arrive, including carriage-return progress updates that do not end with a newline. This preserves the live compilation experience of non-interactive mode. The log view follows the newest output automatically. Scrolling upward pauses following; press `End` to return to real-time output. +Build stdout and stderr share a real pseudo-terminal and are read as raw chunks, so nested tools see the same terminal environment as normal CLI mode. Output is rendered as it arrives, including carriage-return progress updates that do not end with a newline. The log view follows the newest output automatically. Scrolling upward pauses following; press `End` to return to real-time output. Press `l` to expand the logs across the terminal and press `l` again to return to the home screen. Log scrolling, auto-follow, and build controls remain available in fullscreen mode. diff --git a/kbuildx/src/tui.rs b/kbuildx/src/tui.rs index b2ed95a..48255f6 100644 --- a/kbuildx/src/tui.rs +++ b/kbuildx/src/tui.rs @@ -1,5 +1,6 @@ use std::{ collections::BTreeMap, + fs::File, io::{self, Read}, process::{Child, Command, Stdio}, sync::mpsc::{self, Receiver}, @@ -215,7 +216,8 @@ impl App { } else { command.args(&self.build_args); } - command.stdout(Stdio::piped()).stderr(Stdio::piped()); + let (output, child_stdout, child_stderr) = output_pty()?; + command.stdout(child_stdout).stderr(child_stderr); #[cfg(unix)] { use std::os::unix::process::CommandExt; @@ -225,15 +227,11 @@ impl App { command.arg("--set-config").arg(format!("{name}={value}")); } - let mut child = command.spawn().context("starting kernel build")?; - let stdout = child.stdout.take().context("capturing build stdout")?; - let stderr = child.stderr.take().context("capturing build stderr")?; + let child = command.spawn().context("starting kernel build")?; + drop(command); let (tx, rx) = mpsc::channel(); let mut readers = Vec::new(); - for reader in [ - Box::new(stdout) as Box, - Box::new(stderr) as Box, - ] { + for reader in [Box::new(output) as Box] { let tx = tx.clone(); readers.push(thread::spawn(move || { let mut reader = reader; @@ -528,6 +526,39 @@ impl App { } } +#[cfg(unix)] +fn output_pty() -> Result<(File, Stdio, Stdio)> { + use std::os::fd::FromRawFd; + + let mut master = -1; + let mut slave = -1; + // SAFETY: openpty initializes both descriptors on success. Each descriptor + // is immediately wrapped in exactly one File, transferring ownership. + if unsafe { + libc::openpty( + &mut master, + &mut slave, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + } != 0 + { + return Err(io::Error::last_os_error()).context("opening build pseudo-terminal"); + } + // SAFETY: openpty returned owned, valid file descriptors. + let master = unsafe { File::from_raw_fd(master) }; + // SAFETY: openpty returned owned, valid file descriptors. + let slave = unsafe { File::from_raw_fd(slave) }; + let stderr = slave.try_clone().context("cloning build pseudo-terminal")?; + Ok((master, Stdio::from(slave), Stdio::from(stderr))) +} + +#[cfg(not(unix))] +fn output_pty() -> Result<(File, Stdio, Stdio)> { + anyhow::bail!("the TUI build log pseudo-terminal is unsupported on this platform") +} + fn shell_split(input: &str) -> std::result::Result, &'static str> { let mut args = Vec::new(); let mut current = String::new(); @@ -1040,7 +1071,9 @@ fn status_style(status: &str) -> Style { mod tests { use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - use super::{App, Modal, aligned_logo_lines, fuzzy_indices, shell_split, version_key}; + use super::{ + App, Modal, aligned_logo_lines, fuzzy_indices, output_pty, shell_split, version_key, + }; #[test] fn fuzzy_search_prioritizes_close_matches() { @@ -1103,6 +1136,30 @@ mod tests { assert_eq!(app.partial_log, "next"); } + #[cfg(unix)] + #[test] + fn build_output_uses_a_real_terminal() { + use std::io::Read; + use std::process::Command; + + let (mut output, stdout, stderr) = output_pty().unwrap(); + let status = Command::new("sh") + .args([ + "-c", + "test -t 1 && test -t 2 && printf 'stdout\\nstderr\\n' >&2", + ]) + .stdout(stdout) + .stderr(stderr) + .status() + .unwrap(); + let mut captured = String::new(); + output.read_to_string(&mut captured).unwrap(); + + assert!(status.success(), "child failed; captured: {captured:?}"); + assert!(captured.contains("stdout"), "captured: {captured:?}"); + assert!(captured.contains("stderr"), "captured: {captured:?}"); + } + #[test] fn build_options_modal_applies_complete_argument_list() { let mut app = App::new();