{ description = "komorebi for Windows"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; crane.url = "github:ipetkov/crane"; rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; treefmt-nix.url = "github:numtide/treefmt-nix"; treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; git-hooks-nix.url = "github:cachix/git-hooks.nix"; git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs"; asc = { url = "github:LGUG2Z/komorebi-application-specific-configuration"; flake = false; }; # the other platforms' flakes (komorebi-for-kde/flake.nix, komorebi-for-mac/flake.nix) are not inputs: their # flake.nix files are imported below and called with these inputs, which are pinned to exactly the # revisions of their flake.lock (so nothing here pins the content of those directories) # inputs of komorebi-for-kde, pinned to the revisions in komorebi-for-kde/flake.lock (keep them equal to it) kfde-nixpkgs.url = "github:NixOS/nixpkgs/f05535a0efcb2134c478d1e88434fadf486d42b5"; kfde-nixpkgs-lib.url = "github:nix-community/nixpkgs.lib/f7cd230690d9fc982d06129f9189b101fddcfa3b"; kfde-flake-compat = { url = "github:NixOS/flake-compat/5edf11c44bc78a0d334f6334cdaf7d60d732daab"; flake = false; }; kfde-flake-parts.url = "github:hercules-ci/flake-parts/024633cd702b10285db5cb19b40ad48d2399ba60"; kfde-flake-parts.inputs.nixpkgs-lib.follows = "kfde-nixpkgs-lib"; kfde-crane.url = "github:ipetkov/crane/e675a36b7377bebe41d548388a2501cbb16d380d"; kfde-rust-overlay.url = "github:oxalica/rust-overlay/19ecdd08213836b36c4296a3a4f4af5a3d5308db"; kfde-rust-overlay.inputs.nixpkgs.follows = "kfde-nixpkgs"; kfde-treefmt-nix.url = "github:numtide/treefmt-nix/0d6d739a833ba9a9c93034862e4a7b061a062a1c"; kfde-treefmt-nix.inputs.nixpkgs.follows = "kfde-nixpkgs"; kfde-git-hooks-nix.url = "github:cachix/git-hooks.nix/1ba1dbfe28738c9a71c266be6efa5b89e75491a9"; kfde-git-hooks-nix.inputs.nixpkgs.follows = "kfde-nixpkgs"; kfde-git-hooks-nix.inputs.flake-compat.follows = "kfde-flake-compat"; # inputs of komorebi-for-mac, pinned to the revisions in komorebi-for-mac/flake.lock (keep them equal to it) kfm-nixpkgs.url = "github:NixOS/nixpkgs/16c7794d0a28b5a37904d55bcca36003b9109aaa"; kfm-nixpkgs-lib.url = "github:nix-community/nixpkgs.lib/2075416fcb47225d9b68ac469a5c4801a9c4dd85"; kfm-flake-compat = { url = "github:NixOS/flake-compat/5edf11c44bc78a0d334f6334cdaf7d60d732daab"; flake = false; }; kfm-flake-parts.url = "github:hercules-ci/flake-parts/a34fae9c08a15ad73f295041fec82323541400a9"; kfm-flake-parts.inputs.nixpkgs-lib.follows = "kfm-nixpkgs-lib"; kfm-crane.url = "github:ipetkov/crane/7d59256814085fd9666a2ae3e774dc5ee216b630"; kfm-rust-overlay.url = "github:oxalica/rust-overlay/1d3f83babdd21e16bf5cfe0f1efcb4f49ee1bc2c"; kfm-rust-overlay.inputs.nixpkgs.follows = "kfm-nixpkgs"; kfm-treefmt-nix.url = "github:numtide/treefmt-nix/d56486eb9493ad9c4777c65932618e9c2d0468fc"; kfm-treefmt-nix.inputs.nixpkgs.follows = "kfm-nixpkgs"; kfm-git-hooks-nix.url = "github:cachix/git-hooks.nix/f0927703b7b1c8d97511c4116eb9b4ec6645a0fa"; kfm-git-hooks-nix.inputs.nixpkgs.follows = "kfm-nixpkgs"; kfm-git-hooks-nix.inputs.flake-compat.follows = "kfm-flake-compat"; kfm-gitignore = { url = "github:hercules-ci/gitignore.nix/637db329424fd7e46cf4185293b9cc8c88c95394"; inputs.nixpkgs.follows = "kfm-nixpkgs"; }; kfm-git-hooks-nix.inputs.gitignore.follows = "kfm-gitignore"; }; outputs = inputs@{ self, nixpkgs, flake-parts, crane, rust-overlay, ... }: let windowsSdkVersion = "10.0.26100"; windowsCrtVersion = "14.44.17.14"; mkWindowsSdk = pkgs: pkgs.stdenvNoCC.mkDerivation { name = "windows-sdk-${windowsSdkVersion}-crt-${windowsCrtVersion}"; nativeBuildInputs = [ pkgs.xwin pkgs.cacert ]; outputHashAlgo = "sha256"; outputHashMode = "recursive"; # hash of `xwin splat --disable-symlinks`: without the flag xwin makes # case-alias symlinks on a case-sensitive filesystem and none on a # case-insensitive one, so the hash depends on the host outputHash = "sha256-YjmEfoxtcmQXvUc+WtaElnyP/4osNvtSI7X6MDaNTjg="; buildCommand = '' export HOME=$(mktemp -d) # keep the cache on the same filesystem as $out, as xwin moves # files from the cache into the output and rename(2) fails with # EXDEV when the build dir and the store are separate mounts mkdir -p $out xwin --accept-license \ --cache-dir $out/.xwin-cache \ --sdk-version ${windowsSdkVersion} \ --crt-version ${windowsCrtVersion} \ splat --disable-symlinks --output $out rm -rf $out/.xwin-cache ''; }; # the linker asks for `kernel32.lib` of a file that is `Kernel32.lib`, so # on a case-sensitive host add a lowercase alias for every name with a # capital in it, and the spellings the headers include each other by (the # SDK itself is made without symlinks) caseFolded = { pkgs, windowsSdk }: if pkgs.stdenv.hostPlatform.isDarwin then windowsSdk else pkgs.runCommandLocal "${windowsSdk.name}-case-folded" { } '' cp -rs ${windowsSdk} $out chmod -R u+w $out cd $out find . -name '*[A-Z]*' | while read -r path; do directory=$(dirname "$path") name=$(basename "$path") lower=$(echo "$name" | tr 'A-Z' 'a-z') if [ ! -e "$directory/$lower" ] && [ ! -L "$directory/$lower" ]; then ln -s "$name" "$directory/$lower" fi done # headers include each other with spellings that differ from the # file name (`DriverSpecs.h` for `driverspecs.h`), which xwin # covers with symlinks unless told not to grep -Rhoi -E '^[[:space:]]*#[[:space:]]*include[[:space:]]*[<"][^>"]+[>"]' \ crt/include sdk/include 2>/dev/null \ | sed -E 's/.*[<"]([^>"]+)[>"]/\1/; s#\\#/#g; s#.*/##' \ | sort -u > $TMPDIR/spellings find crt/include sdk/include \( -type f -o -type l \) \ | awk -v spellings=$TMPDIR/spellings ' BEGIN { while ((getline line < spellings) > 0) known[tolower(line)] = known[tolower(line)] "\n" line } { n = split($0, parts, "/"); base = parts[n] dir = substr($0, 1, length($0) - length(base)) m = split(known[tolower(base)], names, "\n") for (i = 1; i <= m; i++) if (names[i] != "" && names[i] != base) print dir "\t" base "\t" names[i] }' \ | while IFS=$'\t' read -r directory name spelling; do if [ ! -e "$directory$spelling" ] && [ ! -L "$directory$spelling" ]; then ln -s "$name" "$directory$spelling" fi done ''; mkMsvcEnv = { pkgs, windowsSdk }: let sdk = caseFolded { inherit pkgs windowsSdk; }; clangVersion = pkgs.lib.versions.major pkgs.llvmPackages.clang.version; in { # linker for the windows target CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER = "lld-link"; # c/c++ compiler CC_x86_64_pc_windows_msvc = "clang-cl"; CXX_x86_64_pc_windows_msvc = "clang-cl"; AR_x86_64_pc_windows_msvc = "llvm-lib"; # IMPORTANT: libclang include path MUST come first to avoid header conflicts CFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [ "--target=x86_64-pc-windows-msvc" "-Wno-unused-command-line-argument" "-fuse-ld=lld-link" "/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include" "/imsvc${sdk}/crt/include" "/imsvc${sdk}/sdk/include/ucrt" "/imsvc${sdk}/sdk/include/um" "/imsvc${sdk}/sdk/include/shared" ]; CXXFLAGS_x86_64_pc_windows_msvc = builtins.concatStringsSep " " [ "--target=x86_64-pc-windows-msvc" "-Wno-unused-command-line-argument" "-fuse-ld=lld-link" "/imsvc${pkgs.llvmPackages.libclang.lib}/lib/clang/${clangVersion}/include" "/imsvc${sdk}/crt/include" "/imsvc${sdk}/sdk/include/ucrt" "/imsvc${sdk}/sdk/include/um" "/imsvc${sdk}/sdk/include/shared" ]; # target-specific rust flags with linker flavor and library search paths CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS = builtins.concatStringsSep " " [ "-Clinker-flavor=lld-link" "-Lnative=${sdk}/crt/lib/x86_64" "-Lnative=${sdk}/sdk/lib/um/x86_64" "-Lnative=${sdk}/sdk/lib/ucrt/x86_64" ]; # cargo target CARGO_BUILD_TARGET = "x86_64-pc-windows-msvc"; }; mkKomorebiPackages = { pkgs, windowsSdk, # the commit stamped into the binaries; a build for the e2e guest # gives a constant one, so that a new commit does not rebuild it commit ? self.rev or (pkgs.lib.removeSuffix "-dirty" (self.dirtyRev or "unknown")), }: let # toolchain with windows msvc target toolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { targets = [ "x86_64-pc-windows-msvc" ]; }; craneLib = (crane.mkLib pkgs).overrideToolchain toolchain; version = "0.1.0"; msvcEnv = mkMsvcEnv { inherit pkgs windowsSdk; }; src = pkgs.lib.cleanSourceWith { src = ./.; filter = path: type: # the other platforms' trees have their own workspaces and are built by their own outputs !(pkgs.lib.hasInfix "/komorebi-for-kde" path || pkgs.lib.hasInfix "/komorebi-for-mac" path) && ( (craneLib.filterCargoSources path type) || (pkgs.lib.hasInfix "/docs/" path) || (builtins.match ".*/docs/.*" path != null) || (builtins.match ".*/komorebic/fixtures/.*" path != null) ); }; commonArgs = { inherit src version; strictDeps = true; # komorebic/build.rs downloads applications.json if it is missing, which # is not possible in the sandbox, so provide the pinned copy instead preBuild = "mkdir -p komorebic && cp ${inputs.asc}/applications.json komorebic/applications.json"; COMMIT_HASH = commit; # build inputs for cross-compilation nativeBuildInputs = [ pkgs.llvmPackages.clang-unwrapped pkgs.llvmPackages.lld pkgs.llvmPackages.llvm # komorebic build script uses reqwest, which runs on the build host pkgs.pkg-config pkgs.openssl pkgs.openssl.dev ]; # point the host-side openssl-sys build at the nix openssl OPENSSL_NO_VENDOR = "1"; OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib"; OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include"; # cross-compilation environment inherit (msvcEnv) CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER CC_x86_64_pc_windows_msvc CXX_x86_64_pc_windows_msvc AR_x86_64_pc_windows_msvc CFLAGS_x86_64_pc_windows_msvc CXXFLAGS_x86_64_pc_windows_msvc CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS CARGO_BUILD_TARGET ; }; cargoArtifacts = craneLib.buildDepsOnly commonArgs; individualCrateArgs = commonArgs // { inherit cargoArtifacts; doCheck = false; doDoc = false; }; fullBuild = craneLib.buildPackage ( individualCrateArgs // { pname = "komorebi-workspace"; } ); extractBinary = binaryName: pkgs.runCommand "komorebi-${binaryName}" { meta = fullBuild.meta // { }; } '' mkdir -p $out/bin cp ${fullBuild}/bin/${binaryName}.exe $out/bin/ ''; in { inherit craneLib src individualCrateArgs fullBuild msvcEnv ; komorebi = extractBinary "komorebi"; komorebic = extractBinary "komorebic"; komorebic-no-console = extractBinary "komorebic-no-console"; komorebi-bar = extractBinary "komorebi-bar"; komorebi-gui = extractBinary "komorebi-gui"; komorebi-shortcuts = extractBinary "komorebi-shortcuts"; komorebi-testwin = extractBinary "komorebi-testwin"; komorebi-e2e-agent = extractBinary "komorebi-e2e-agent"; }; mkPkgs = system: import nixpkgs { inherit system; overlays = [ (import rust-overlay) ]; }; in flake-parts.lib.mkFlake { inherit inputs; } { systems = [ "aarch64-darwin" "x86_64-linux" "aarch64-linux" ]; imports = [ inputs.treefmt-nix.flakeModule inputs.git-hooks-nix.flakeModule # komorebi for KDE Plasma (kfde) and komorebi for macOS (kfm), re-exported with a prefix. # Their flakes keep their own inputs and lock files (inputs..inputs), but their # outputs are made here, from the whole repository as the source: the Rust workspaces of # both depend on the crates of this tree by path, which a flake of their own directory # cannot see (their `lib.mkOutputs`). The VM tests of kfde are not re-exported as checks, # so that `nix flake check` of this flake does not boot them; they are reachable as # legacyPackages..kfde-vm. ( { lib, ... }: let # the nested flake.nix files are plain attribute sets: import them and call their # `lib.mkOutputs` with the inputs declared above (same revisions as their own locks). # `self` of a nested flake: its own outputs, with the revision and the path of this one nestedOutputs = name: dir: let nestedInputs = lib.genAttrs [ "nixpkgs" "flake-parts" "crane" "rust-overlay" "treefmt-nix" "git-hooks-nix" ] (n: inputs."${name}-${n}"); mkOutputs = ((import (self.outPath + "/${dir}/flake.nix")).outputs (nestedInputs // { inherit self; })).lib.mkOutputs; nestedSelf = outputs // lib.filterAttrs (n: _: n == "rev" || n == "dirtyRev") self // { outPath = self.outPath + "/${dir}"; inputs = nestedInputs; }; outputs = mkOutputs { self = nestedSelf; inputs = nestedInputs // { self = nestedSelf; }; repoRoot = self; platformDir = dir; }; in outputs; kfde = nestedOutputs "kfde" "komorebi-for-kde"; kfm = nestedOutputs "kfm" "komorebi-for-mac"; in { flake = { nixosModules.komorebi-for-kde = kfde.nixosModules.default; overlays.komorebi-for-kde = kfde.overlays.default; darwinModules.komorebi-for-mac = kfm.darwinModules.default; }; perSystem = { system, ... }: let prefixed = prefix: lib.mapAttrs' (name: lib.nameValuePair "${prefix}-${name}"); withoutVmTests = lib.filterAttrs (name: _: !(lib.hasPrefix "vm-" name)); in { packages = prefixed "kfde" (kfde.packages.${system} or { }) // prefixed "kfm" (kfm.packages.${system} or { }); checks = prefixed "kfde" (withoutVmTests (kfde.checks.${system} or { })) // prefixed "kfm" (kfm.checks.${system} or { }); legacyPackages.kfde-vm = (kfde.legacyPackages.${system} or { }).vm or { }; }; } ) ]; perSystem = { config, system, ... }: let pkgs = mkPkgs system; windowsSdk = mkWindowsSdk pkgs; build = mkKomorebiPackages { inherit pkgs windowsSdk; }; # the binaries the e2e guest runs: the same build, but with a # constant commit stamp, so a new commit (or a dirty tree) that does # not change a source file reuses it. The commit under test is # KOMOREBI_E2E_COMMIT, not what the binary says. e2eBuild = mkKomorebiPackages { inherit pkgs windowsSdk; commit = "e2e"; }; # OVMF without SMM and with the TPM support Windows 11 Setup asks for ovmfWithTpm = (pkgs.OVMF.override { tpmSupport = true; }).fd; e2eWindows = pkgs.writeShellApplication { name = "e2e-windows"; runtimeInputs = [ pkgs.qemu pkgs.swtpm (pkgs.python3.withPackages (p: [ p.pillow ])) ]; text = '' : "''${E2E_GOLDEN:?set E2E_GOLDEN to the golden image directory}" out=''${1:?usage: e2e-windows OUT SCENARIO...} shift # the binaries under test: this checkout's, or those of another # commit, made by e2e/platforms/qemu_windows/build-ref.sh if [ -n "''${KOMOREBI_E2E_BINS:-}" ]; then bins=$KOMOREBI_E2E_BINS if [ -f "$bins/commit" ]; then KOMOREBI_E2E_COMMIT=$(cat "$bins/commit") fi else bins=$(mktemp -d) trap 'rm -rf "$bins"' EXIT cp ${e2eBuild.komorebi}/bin/komorebi.exe "$bins" cp ${e2eBuild.komorebic}/bin/komorebic.exe "$bins" cp ${e2eBuild.komorebi-testwin}/bin/komorebi-testwin.exe "$bins" fi export OVMF_FD=${ovmfWithTpm} export KOMOREBI_E2E_COMMIT=''${KOMOREBI_E2E_COMMIT:-${self.rev or "dirty"}} export PYTHONPATH=${./e2e/lib}:${./e2e} exec python -m platforms.qemu_windows.run \ "$E2E_GOLDEN" "$bins" "$out" "$@" ''; }; # toolchain with windows target and nightly rustfmt rustToolchain = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml).override { targets = [ "x86_64-pc-windows-msvc" ]; }; nightlyRustfmt = pkgs.rust-bin.nightly.latest.rustfmt; rustToolchainWithNightlyRustfmt = pkgs.symlinkJoin { name = "rust-toolchain-with-nightly-rustfmt"; paths = [ nightlyRustfmt rustToolchain ]; }; nightlyToolchain = pkgs.rust-bin.nightly.latest.default.override { targets = [ "x86_64-pc-windows-msvc" ]; }; cargo-udeps = pkgs.writeShellScriptBin "cargo-udeps" '' export PATH="${nightlyToolchain}/bin:$PATH" exec ${pkgs.cargo-udeps}/bin/cargo-udeps "$@" ''; in { treefmt = { projectRootFile = "flake.nix"; programs = { deadnix.enable = true; just.enable = true; nixfmt.enable = true; taplo.enable = true; rustfmt = { enable = true; package = pkgs.rust-bin.nightly.latest.rustfmt; }; }; }; checks = { komorebi-workspace-clippy = build.craneLib.cargoClippy ( build.individualCrateArgs // { cargoClippyExtraArgs = "--all-targets -- -D warnings"; } ); komorebi-workspace-fmt = build.craneLib.cargoFmt { inherit (build) src; }; komorebi-workspace-toml-fmt = build.craneLib.taploFmt { src = pkgs.lib.sources.sourceFilesBySuffices build.src [ ".toml" ]; }; komorebi-workspace-deny = build.craneLib.cargoDeny { inherit (build) src; }; komorebi-workspace-nextest = build.craneLib.cargoNextest build.individualCrateArgs; # the Python of the Windows e2e guest that needs no guest: the # vendored core's tests and those of the runner e2e-tests = pkgs.runCommand "e2e-tests" { nativeBuildInputs = [ pkgs.git (pkgs.python3.withPackages (p: [ p.pillow p.pytest ])) ]; } '' cp -r ${./e2e} e2e chmod -R u+w e2e cd e2e export HOME=$TMPDIR PYTHONPATH=$PWD/lib:$PWD python -m pytest -q -p no:cacheprovider lib/tests platforms/qemu_windows/tests touch $out ''; }; packages = { inherit (build) komorebi komorebic komorebic-no-console komorebi-bar komorebi-gui komorebi-shortcuts komorebi-testwin komorebi-e2e-agent ; inherit windowsSdk; # the test window of the Windows e2e guest, built with the constant # stamp: a new commit does not rebuild what it depends on komorebi-testwin-e2e = e2eBuild.komorebi-testwin; komorebi-full = build.fullBuild; default = build.fullBuild; }; apps = { komorebi = { type = "app"; program = "${build.komorebi}/bin/komorebi.exe"; }; komorebic = { type = "app"; program = "${build.komorebic}/bin/komorebic.exe"; }; komorebic-no-console = { type = "app"; program = "${build.komorebic-no-console}/bin/komorebic-no-console.exe"; }; komorebi-bar = { type = "app"; program = "${build.komorebi-bar}/bin/komorebi-bar.exe"; }; komorebi-gui = { type = "app"; program = "${build.komorebi-gui}/bin/komorebi-gui.exe"; }; komorebi-shortcuts = { type = "app"; program = "${build.komorebi-shortcuts}/bin/komorebi-shortcuts.exe"; }; default = { type = "app"; program = "${build.fullBuild}/bin/komorebi.exe"; }; } // pkgs.lib.optionalAttrs (system == "x86_64-linux") { # the neutral scenarios against a throwaway Windows guest: # E2E_GOLDEN= nix run .#e2e-windows -- \ # OUT SCENARIO... # KOMOREBI_E2E_BINS= tests another commit # the golden image is built once with # e2e/platforms/qemu_windows/build-golden.sh e2e-windows = { type = "app"; program = pkgs.lib.getExe e2eWindows; }; }; devShells.default = pkgs.mkShell { name = "komorebi"; RUST_BACKTRACE = "full"; # komorebic's build script runs on the host and wants OpenSSL, as in # the nix build OPENSSL_NO_VENDOR = "1"; OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib"; OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include"; # the Windows e2e guest (e2e/platforms/qemu_windows): the build # scripts and the runner find OVMF here, and the vendored core # and the platform on PYTHONPATH OVMF_FD = "${ovmfWithTpm}"; shellHook = '' export PYTHONPATH="$PWD/e2e/lib:$PWD/e2e''${PYTHONPATH:+:$PYTHONPATH}" ''; # cross-compilation environment inherit (build.msvcEnv) CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER CC_x86_64_pc_windows_msvc CXX_x86_64_pc_windows_msvc AR_x86_64_pc_windows_msvc CFLAGS_x86_64_pc_windows_msvc CXXFLAGS_x86_64_pc_windows_msvc CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS CARGO_BUILD_TARGET ; packages = [ rustToolchainWithNightlyRustfmt cargo-udeps # cross-compilation tooling pkgs.llvmPackages.clang-unwrapped # provides clang-cl pkgs.llvmPackages.lld # provides lld-link pkgs.llvmPackages.llvm # provides llvm-lib # komorebic's build script (reqwest) runs on the build host pkgs.pkg-config pkgs.openssl pkgs.cargo-deny pkgs.cargo-nextest pkgs.cargo-outdated pkgs.jq pkgs.just pkgs.prettier # the Windows e2e guest pkgs.qemu pkgs.swtpm pkgs.xorriso pkgs.socat pkgs.git (pkgs.python3.withPackages (p: [ p.pillow p.pytest # the tests of e2e/lib ])) ]; }; pre-commit = { check.enable = true; settings.hooks.treefmt = { enable = true; package = config.treefmt.build.wrapper; pass_filenames = false; }; }; }; }; }