Merge pull request #1019 from qitechgmbh/615-test-nixos-builds-in-ci-cd

Rework github pipeline
This commit is contained in:
Christian authored and GitHub committed 2026-01-05 07:17:47 +01:00
commit 735f27770a
24 files changed
+520 -5964

No files matched your search

+52 -29
View File
@@ -3,50 +3,73 @@ name: Electron
on:
push:
branches:
- "**"
- master
pull_request:
branches:
- "*"
- master
defaults:
run:
working-directory: electron
jobs:
build-electron:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: electron
steps:
- name: Setup Node.js
- &checkout
name: Checkout
uses: actions/checkout@v4
- &setup-node
name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18" # or '20' for latest LTS
node-version: 20
cache: "npm"
cache-dependency-path: |
electron/package-lock.json
- uses: actions/checkout@v4
- &install
name: Install dependencies
run: npm ci
- name: Cache NPM dependencies
uses: actions/cache@v3
with:
path: |
~/.npm
node_modules/
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Install
run: npm i
- name: Build
- name: Compile
run: tsc
- name: Format
run: npm run format
- name: Build
run: npm run build
- name: Lint
run: npm run lint
test:
runs-on: ubuntu-latest
needs: build
steps:
- *checkout
- *setup-node
- *install
- name: Test
run: npm run test
- name: Package
run: npm run package
format:
runs-on: ubuntu-latest
steps:
- *checkout
- *setup-node
- *install
- name: Format
run: npm run format
lint:
runs-on: ubuntu-latest
steps:
- *checkout
- *setup-node
- *install
- name: Lint
run: npm run lint
+96
View File
@@ -0,0 +1,96 @@
name: Nix CI
on:
push:
branches:
- master
pull_request:
branches:
- master
env:
NIX_PATH: nixpkgs=channel:nixos-25.05
jobs:
electron:
runs-on: ubuntu-latest
steps:
- &checkout
name: Check out repository
uses: actions/checkout@v4
- &nix-install
uses: nixbuild/nix-quick-install-action@v34
with:
nix_conf: |
keep-env-derivations = true
keep-outputs = true
- uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-electron-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './electron/package-lock.json') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
purge: true
purge-prefixes: nix-electron-${{ runner.os }}-
- name: Build Electron in Nix
run: nix build ".#packages.x86_64-linux.electron"
rust:
runs-on: ubuntu-latest
steps:
- *checkout
- *nix-install
- uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-server-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock') }}
restore-prefixes-first-match: nix-${{ runner.os }}-
purge: true
purge-prefixes: nix-server-${{ runner.os }}-
- name: Build Server in Nix
run: nix build ".#packages.x86_64-linux.server"
build:
runs-on: ubuntu-latest
needs: [electron, rust]
steps:
- *checkout
- *nix-install
- name: Use both Electron and Server Nix Store
uses: nix-community/cache-nix-action@v6
with:
primary-key: nix-${{ runner.os }}-${{ hashFiles('**/*.nix', '**/flake.lock', './Cargo.lock', './electron/package-lock.json') }}
# if no hit on the primary key, merge individual caches
restore-prefixes-all-matches: |
nix-electron-${{ runner.os }}-
nix-server-${{ runner.os }}-
- name: Build Flake and System Configuration
run: nix build ".#nixosConfigurations.nixos.config.system.build.toplevel" --impure
format:
runs-on: ubuntu-latest
steps:
- *checkout
- *nix-install
- name: Format
run: nix run 'nixpkgs#nixfmt-classic' $(find . nixos -name '*.nix')
check:
runs-on: ubuntu-latest
steps:
- *checkout
- *nix-install
- name: Build Flake and Configuration.nix
run: nix flake check "." --impure
+46 -10
View File
@@ -3,9 +3,10 @@ name: Rust
on:
push:
branches:
- "**"
- master
pull_request:
branches: "*"
branches:
- master
env:
CARGO_TERM_COLOR: always
@@ -16,14 +17,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Install libudev development package
- &udev-install
name: Install libudev development package
run: sudo apt-get update && sudo apt-get install -y libudev-dev
- uses: actions/checkout@v4
- name: Set Rust version
run: rustup override set ${{ env.RUST_VERSION }}
- name: Install Formatter
run: rustup component add rustfmt
- name: Cache Rust dependencies
- &checkout
uses: actions/checkout@v4
- &setup-rust
name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
profile: minimal
override: true
components: rustfmt
- &cargo-cache
name: Cache Rust dependencies
uses: actions/cache@v3
with:
path: |
@@ -41,11 +52,36 @@ jobs:
RUSTFLAGS: "-D warnings"
run: cargo build --verbose
format:
runs-on: ubuntu-latest
steps:
- *checkout
- *setup-rust
- name: Format
run: cargo fmt --check --verbose
test:
runs-on: ubuntu-latest
steps:
- *udev-install
- *checkout
- *setup-rust
- *cargo-cache
- name: Run tests
run: cargo test --verbose
mock:
runs-on: ubuntu-latest
steps:
- *udev-install
- *checkout
- *setup-rust
- *cargo-cache
- name: Mock Machine
run: cargo build --features development-build --features mock-machine
run: cargo build --features "development-build,mock-machine"
+1
View File
@@ -88,6 +88,7 @@ typings/
# Vite
.vite/
dist/
dist-electron/
# Electron-Forge
out/
-49
View File
@@ -1,49 +0,0 @@
import type { ForgeConfig } from "@electron-forge/shared-types";
import { VitePlugin } from "@electron-forge/plugin-vite";
import { FusesPlugin } from "@electron-forge/plugin-fuses";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
const config: ForgeConfig = {
packagerConfig: {
executableName: "electron-shadcn",
asar: true,
icon: "src/assets/icon",
},
rebuildConfig: {},
plugins: [
new VitePlugin({
// `build` can specify multiple entry builds, which can be Main process, Preload scripts, Worker process, etc.
// If you are familiar with Vite configuration, it will look really familiar.
build: [
{
// `entry` is just an alias for `build.lib.entry` in the corresponding file of `config`.
entry: "src/main.ts",
config: "vite.main.config.ts",
},
{
entry: "src/preload.ts",
config: "vite.preload.config.ts",
},
],
renderer: [
{
name: "main_window",
config: "vite.renderer.config.ts",
},
],
}),
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};
export default config;
-35
View File
@@ -1,35 +0,0 @@
export {}; // Make this a module
declare global {
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Vite
// plugin that tells the Electron app where to look for the Vite-bundled app code (depending on
// whether you're running in development or production).
const MAIN_WINDOW_VITE_DEV_SERVER_URL: string;
const MAIN_WINDOW_VITE_NAME: string;
namespace NodeJS {
interface Process {
// Used for hot reload after preload scripts.
viteDevServers: Record<string, import("vite").ViteDevServer>;
}
}
type VitePluginConfig = ConstructorParameters<
typeof import("@electron-forge/plugin-vite").VitePlugin
>[0];
interface VitePluginRuntimeKeys {
VITE_DEV_SERVER_URL: `${string}_VITE_DEV_SERVER_URL`;
VITE_NAME: `${string}_VITE_NAME`;
}
}
declare module "vite" {
interface ConfigEnv<
K extends keyof VitePluginConfig = keyof VitePluginConfig,
> {
root: string;
forgeConfig: VitePluginConfig;
forgeConfigSelf: VitePluginConfig[K][number];
}
}
+26 -5286
View File
File diff suppressed because it is too large. Load diff
+4 -13
View File
@@ -6,10 +6,8 @@
"main": ".vite/build/main.js",
"private": true,
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"start": "vite",
"build": "vite build",
"lint": "eslint .",
"lint:write": "eslint . --fix",
"format": "prettier --check .",
@@ -22,15 +20,6 @@
},
"author": "",
"devDependencies": {
"@electron-forge/cli": "^7.6.1",
"@electron-forge/maker-deb": "^7.8.0",
"@electron-forge/maker-rpm": "^7.8.3",
"@electron-forge/maker-squirrel": "^7.6.1",
"@electron-forge/maker-zip": "^7.6.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.6.1",
"@electron-forge/plugin-fuses": "^7.10.2",
"@electron-forge/plugin-vite": "^7.6.1",
"@electron-forge/shared-types": "^7.4.0",
"@electron/fuses": "^1.8.0",
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.18.0",
@@ -62,6 +51,8 @@
"typescript": "^5.7.3",
"typescript-eslint": "^8.24.1",
"vite": "^6.4.1",
"vite-plugin-electron": "^0.29.0",
"vite-plugin-electron-renderer": "^0.14.6",
"vitest": "^3.0.6"
},
"dependencies": {
+11 -9
View File
@@ -1,13 +1,14 @@
import { app, BrowserWindow } from "electron";
import registerListeners from "./helpers/ipc/listeners-register";
// "electron-squirrel-startup" seems broken when packaging with vite
//import started from "electron-squirrel-startup";
import path from "path";
import {
installExtension,
REACT_DEVELOPER_TOOLS,
} from "electron-devtools-installer";
const ARG1 = process.argv[1] ?? "./main.js";
const DIR = path.dirname(ARG1);
// Set consistent app ID for Windows taskbar and GNOME dock integration
app.setAppUserModelId("de.qitech.control-electron");
@@ -29,7 +30,9 @@ if (!gotTheLock) {
}
function createWindow() {
const preload = path.join(__dirname, "preload.js");
const preload = path.join(DIR, "preload.js");
console.log("Setting preload.js to", preload);
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
@@ -43,7 +46,7 @@ function createWindow() {
preload: preload,
},
// Add icon path for better integration
icon: path.join(__dirname, "../assets/icon.png"),
icon: path.join(DIR, "icon.png"),
// Set window class explicitly for Linux/GNOME integration
title: "QiTech Control",
});
@@ -53,12 +56,11 @@ function createWindow() {
registerListeners(mainWindow);
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
if (process.env.VITE_DEV_SERVER_URL) {
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(
path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`),
);
const index = path.join(DIR, "index.html");
mainWindow.loadFile(index);
}
}
+1
View File
@@ -1,3 +1,4 @@
import exposeContexts from "./helpers/ipc/context-exposer";
console.log("preload.js loaded");
exposeContexts();
-119
View File
@@ -1,119 +0,0 @@
import path from "path";
import { builtinModules } from "node:module";
import type { AddressInfo } from "node:net";
import type { ConfigEnv, Plugin, UserConfig } from "vite";
import pkg from "./package.json";
export const builtins = [
"electron",
...builtinModules.map((m) => [m, `node:${m}`]).flat(),
];
export const external = [
...builtins,
...Object.keys(
"dependencies" in pkg ? (pkg.dependencies as Record<string, unknown>) : {},
),
];
export function getBuildConfig(env: ConfigEnv<"build">): UserConfig {
const { root, mode, command } = env;
return {
root,
mode,
publicDir: path.resolve(__dirname, "public"),
build: {
// Prevent multiple builds from interfering with each other.
emptyOutDir: false,
// 🚧 Multiple builds may conflict.
outDir: ".vite/build",
watch: command === "serve" ? {} : null,
minify: command === "build",
},
resolve: {
preserveSymlinks: true,
alias: {
"@": path.resolve(__dirname, "./src"),
"@root": path.resolve(__dirname, ".."),
},
},
clearScreen: false,
};
}
export function getDefineKeys(names: string[]) {
const define: { [name: string]: VitePluginRuntimeKeys } = {};
return names.reduce((acc, name) => {
const NAME = name.toUpperCase();
const keys: VitePluginRuntimeKeys = {
VITE_DEV_SERVER_URL: `${NAME}_VITE_DEV_SERVER_URL`,
VITE_NAME: `${NAME}_VITE_NAME`,
};
return { ...acc, [name]: keys };
}, define);
}
export function getBuildDefine(env: ConfigEnv<"build">) {
const { command, forgeConfig } = env;
const names = forgeConfig.renderer
.filter(({ name }) => name != null)
.map(({ name }) => name!);
const defineKeys = getDefineKeys(names);
const define = Object.entries(defineKeys).reduce(
(acc, [name, keys]) => {
const { VITE_DEV_SERVER_URL, VITE_NAME } = keys;
const def = {
[VITE_DEV_SERVER_URL]:
command === "serve"
? JSON.stringify(process.env[VITE_DEV_SERVER_URL])
: undefined,
[VITE_NAME]: JSON.stringify(name),
};
return { ...acc, ...def };
},
{} as Record<string, any>,
);
return define;
}
export function pluginExposeRenderer(name: string): Plugin {
const { VITE_DEV_SERVER_URL } = getDefineKeys([name])[name];
return {
name: "@electron-forge/plugin-vite:expose-renderer",
configureServer(server) {
process.viteDevServers ??= {};
// Expose server for preload scripts hot reload.
process.viteDevServers[name] = server;
server.httpServer?.once("listening", () => {
const addressInfo = server.httpServer!.address() as AddressInfo;
// Expose env constant for main process use.
process.env[VITE_DEV_SERVER_URL] =
`http://localhost:${addressInfo?.port}`;
});
},
};
}
export function pluginHotRestart(command: "reload" | "restart"): Plugin {
return {
name: "@electron-forge/plugin-vite:hot-restart",
closeBundle() {
if (command === "reload") {
for (const server of Object.values(process.viteDevServers)) {
// Preload scripts hot reload.
server.ws.send({ type: "full-reload" });
}
} else {
// Main process hot restart.
// https://github.com/electron/forge/blob/v7.2.0/packages/api/core/src/api/start.ts#L216-L223
process.stdin.emit("data", "rs");
}
},
};
}
+37
View File
@@ -0,0 +1,37 @@
import react from "@vitejs/plugin-react";
import electron from "vite-plugin-electron/simple";
import path from "path";
// https://vitejs.dev/config
export default {
publicDir: path.resolve(__dirname, "public"),
resolve: {
preserveSymlinks: true,
alias: {
"@": path.resolve(__dirname, "./src"),
"@root": path.resolve(__dirname, ".."),
},
},
clearScreen: false,
plugins: [
electron({
main: {
entry: "src/main.ts",
onstart: ({ startup }) => {
startup(["./dist-electron/main.js"]);
},
},
preload: {
// Shortcut of `build.rollupOptions.input`
input: "src/preload.ts",
},
// Optional: Use Node.js API in the Renderer process
renderer: {},
}),
react({
babel: {
plugins: [["babel-plugin-react-compiler"]],
},
}),
],
};
-33
View File
@@ -1,33 +0,0 @@
import type { ConfigEnv, UserConfig } from "vite";
import { defineConfig, mergeConfig } from "vite";
import {
getBuildConfig,
getBuildDefine,
external,
pluginHotRestart,
} from "./vite.base.config";
// https://vitejs.dev/config
export default defineConfig((env) => {
const forgeEnv = env as ConfigEnv<"build">;
const { forgeConfigSelf } = forgeEnv;
const config: UserConfig = {
build: {
lib: {
entry: forgeConfigSelf.entry!,
fileName: () => "[name].js",
formats: ["cjs"],
},
rollupOptions: {
external,
},
},
plugins: [pluginHotRestart("restart")],
resolve: {
// Load the Node.js entry.
mainFields: ["module", "jsnext:main", "jsnext"],
},
};
return mergeConfig(getBuildConfig(forgeEnv), config);
});
-29
View File
@@ -1,29 +0,0 @@
import type { ConfigEnv, UserConfig } from "vite";
import { defineConfig, mergeConfig } from "vite";
import { getBuildConfig, external, pluginHotRestart } from "./vite.base.config";
// https://vitejs.dev/config
export default defineConfig((env) => {
const forgeEnv = env as ConfigEnv<"build">;
const { forgeConfigSelf } = forgeEnv;
const config: UserConfig = {
build: {
rollupOptions: {
external,
// Preload scripts may contain Web assets, so use the `build.rollupOptions.input` instead `build.lib.entry`.
input: forgeConfigSelf.entry!,
output: {
format: "cjs",
// It should not be split chunks.
inlineDynamicImports: true,
entryFileNames: "[name].js",
chunkFileNames: "[name].js",
assetFileNames: "[name].[ext]",
},
},
},
plugins: [pluginHotRestart("reload")],
};
return mergeConfig(getBuildConfig(forgeEnv), config);
});
-26
View File
@@ -1,26 +0,0 @@
import react from "@vitejs/plugin-react";
import type { ConfigEnv, UserConfig } from "vite";
import { defineConfig, mergeConfig } from "vite";
import { pluginExposeRenderer, getBuildConfig } from "./vite.base.config";
// https://vitejs.dev/config
export default defineConfig((env) => {
const forgeEnv = env as ConfigEnv<"renderer">;
const { forgeConfigSelf } = forgeEnv;
const name = forgeConfigSelf.name ?? "";
return mergeConfig(getBuildConfig(env), {
base: "./",
build: {
outDir: `.vite/renderer/${name}`,
},
plugins: [
pluginExposeRenderer(name),
react({
babel: {
plugins: [["babel-plugin-react-compiler"]],
},
}),
],
}) as UserConfig;
});
+56 -62
View File
@@ -5,9 +5,7 @@
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
# Crane for Rust builds with dependency caching
crane = {
url = "github:ipetkov/crane";
};
crane = { url = "github:ipetkov/crane"; };
home-manager = {
url = "github:nix-community/home-manager";
@@ -21,24 +19,21 @@
};
# Add flake-utils which was missing
flake-utils = {
url = "github:numtide/flake-utils";
};
flake-utils = { url = "github:numtide/flake-utils"; };
};
outputs = { self, nixpkgs, crane, flake-utils, qitech-control, home-manager, ... }:
outputs =
{ self, nixpkgs, crane, flake-utils, qitech-control, home-manager, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [
# Add our own overlay for QiTech packages
# Add our own overlay for QiTech packages with commit hash support
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
nodejs = final.nodejs_22;
craneLib = crane.mkLib pkgs;
};
electron = final.callPackage ./nixos/packages/electron.nix { };
};
})
];
@@ -46,8 +41,6 @@
pkgs = import nixpkgs { inherit system overlays; };
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
craneLib = crane.mkLib pkgs;
# Use Rust 1.86 stable from nixpkgs
rust = pkgs.rustc;
in {
@@ -57,7 +50,7 @@
default = self.packages.${system}.server;
};
devShells.default = craneLib.devShell {
devShells.default = pkgs.mkShell {
packages = with pkgs; [
pkg-config
libudev-zero
@@ -65,8 +58,11 @@
nodejs_22
nodePackages.npm
lldb
electron
];
ELECTON_SKIP_BINARY_DOWNLOAD = 1;
hardeningDisable = [ "fortify" ];
shellHook = ''
@@ -75,55 +71,53 @@
echo "Node version: $(${pkgs.nodejs_22}/bin/node --version)"
'';
};
}
) // {
nixosModules.qitech = import ./nixos/modules/qitech.nix;
nixosModules.default = self.nixosModules.qitech;
}) // {
nixosModules.qitech = import ./nixos/modules/qitech.nix;
nixosModules.default = self.nixosModules.qitech;
# Define nixosConfigurations outside of eachDefaultSystem
nixosConfigurations =
let
system = builtins.currentSystem;
pkgs = import nixpkgs { inherit system; };
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
in {
# Replace "nixos" with your actual hostname
nixos = nixpkgs.lib.nixosSystem {
system = system;
specialArgs = {
gitInfo = gitInfo; # Pass gitInfo to modules
# Define nixosConfigurations outside of eachDefaultSystem
nixosConfigurations = let
system = builtins.currentSystem;
pkgs = import nixpkgs { inherit system; };
gitInfo = import ./nixos/gitInfo.nix { inherit pkgs; };
in {
# Replace "nixos" with your actual hostname
nixos = nixpkgs.lib.nixosSystem {
system = system;
specialArgs = {
gitInfo = gitInfo; # Pass gitInfo to modules
};
modules = [
# Apply the overlays to the system
{
nixpkgs.overlays = [
# Add our own overlay for QiTech packages with commit hash support
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
craneLib = crane.mkLib final;
};
electron =
final.callPackage ./nixos/packages/electron.nix { };
};
})
];
}
./nixos/os/configuration.nix
# QiTech Control module
self.nixosModules.qitech
# Home Manager module
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.qitech = import ./nixos/os/home.nix;
}
];
};
modules = [
# Apply the overlays to the system
{ nixpkgs.overlays = [
# Add our own overlay for QiTech packages with commit hash support
(final: prev: {
qitechPackages = {
server = final.callPackage ./nixos/packages/server.nix {
craneLib = crane.mkLib final;
};
electron = final.callPackage ./nixos/packages/electron.nix {
nodejs = final.nodejs_22;
};
};
})
];
}
./nixos/os/configuration.nix
# QiTech Control module
self.nixosModules.qitech
# Home Manager module
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.qitech = import ./nixos/os/home.nix;
}
];
};
};
};
}
+2 -3
View File
@@ -1,5 +1,4 @@
{ pkgs ? import <nixpkgs> {}
}:
{ pkgs ? import <nixpkgs> { } }:
let
# Use the flake's packages when available
@@ -8,7 +7,7 @@ let
in {
server = flake.packages.${system}.server;
electron = flake.packages.${system}.electron;
# Use the flake's development shell for consistency
shell = flake.devShells.${system}.default;
}
+2 -3
View File
@@ -2,7 +2,7 @@
let
# Generate Git info in the store
gitInfoDerivation = pkgs.runCommand "git-info" {} ''
gitInfoDerivation = pkgs.runCommand "git-info" { } ''
gitTimestamp="${builtins.getEnv "GIT_TIMESTAMP"}"
gitCommit="${builtins.getEnv "GIT_COMMIT"}"
gitUrl="${builtins.getEnv "GIT_URL"}"
@@ -21,5 +21,4 @@ let
}
EOF
'';
in
builtins.fromJSON (builtins.readFile "${gitInfoDerivation}/git-info.json")
in builtins.fromJSON (builtins.readFile "${gitInfoDerivation}/git-info.json")
+57 -60
View File
@@ -2,30 +2,30 @@
with lib;
let
cfg = config.services.qitech;
let cfg = config.services.qitech;
in {
options.services.qitech = {
enable = mkEnableOption "QiTech Control";
openFirewall = mkOption {
type = types.bool;
default = false;
description = "Whether to open ports in the firewall for the QiTech server";
description =
"Whether to open ports in the firewall for the QiTech server";
};
user = mkOption {
type = types.str;
default = "qitech-service";
description = "User account under which the service runs";
};
group = mkOption {
type = types.str;
default = "qitech-service";
description = "Group under which the service runs";
};
package = mkOption {
type = types.package;
default = pkgs.qitech-control-server or null;
@@ -34,12 +34,11 @@ in {
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.package != null;
message = "No QiTech server package available. Please add it to your pkgs or explicitly set services.qitech.package.";
}
];
assertions = [{
assertion = cfg.package != null;
message =
"No QiTech server package available. Please add it to your pkgs or explicitly set services.qitech.package.";
}];
users.users.${cfg.user} = {
isSystemUser = true;
@@ -47,72 +46,72 @@ in {
description = "QiTech service user";
extraGroups = [ "realtime" "plugdev" "dialout" "uucp" ];
};
users.groups.${cfg.group} = {};
users.groups.${cfg.group} = { };
# Install the Electron app system-wide
environment.systemPackages = [
pkgs.qitechPackages.electron
];
environment.systemPackages = [ pkgs.qitechPackages.electron ];
# Configure udev rules for EtherCAT device access
services.udev.extraRules = ''
# Allow access to network devices for ethercrab
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="*", TAG+="uaccess", TAG+="udev-acl", GROUP="${cfg.group}"
# USB device access if needed
SUBSYSTEM=="usb", ATTRS{idVendor}=="*", ATTRS{idProduct}=="*", MODE="0660", GROUP="${cfg.group}"
'';
# Configure the systemd service
systemd.services.qitech-control-server = {
description = "QiTech Control Server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/server";
Restart = "always";
RestartSec = "10s";
# Capabilities
CapabilityBoundingSet = "CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
AmbientCapabilities = "CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = "${cfg.package}/bin/server";
Restart = "always";
RestartSec = "10s";
# Hardening options
NoNewPrivileges = true;
ProtectSystem = "strict";
# Capabilities
CapabilityBoundingSet =
"CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
AmbientCapabilities =
"CAP_NET_RAW CAP_IPC_LOCK CAP_NET_ADMIN CAP_SYS_NICE CAP_DAC_OVERRIDE";
# Open only /proc/irq explicitly
ReadWritePaths = [ "/proc/irq" ];
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false;
# Hardening options
NoNewPrivileges = true;
ProtectSystem = "strict";
# Must disable this to allow /proc/irq writes
ProtectKernelTunables = false;
ProtectControlGroups = true;
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_PACKET";
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
# Open only /proc/irq explicitly
ReadWritePaths = [ "/proc/irq" ];
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = false;
# Logging
StandardOutput = "journal";
StandardError = "journal";
SyslogIdentifier = "qitech-control-server";
};
# Must disable this to allow /proc/irq writes
ProtectKernelTunables = false;
ProtectControlGroups = true;
RestrictAddressFamilies =
"AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_PACKET";
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
# Logging
StandardOutput = "journal";
StandardError = "journal";
SyslogIdentifier = "qitech-control-server";
};
environment = {
RUST_BACKTRACE = "full";
RUST_LOG = "info";
};
};
# Add real-time privileges
security.pam.loginLimits = [
{
@@ -134,12 +133,10 @@ serviceConfig = {
value = "-20";
}
];
# Open firewall if requested
networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ 3001 ];
};
networking.firewall = mkIf cfg.openFirewall { allowedTCPPorts = [ 3001 ]; };
# Desktop integration
xdg.mime.enable = true;
xdg.icons.enable = true;
+10
View File
@@ -0,0 +1,10 @@
{ config, pkgs, ... }:
{
fileSystems."/" = {
device = "/dev/null";
fsType = "tmpfs";
};
swapDevices = [ ];
boot.initrd.kernelModules = [ ];
}
+21 -26
View File
@@ -1,25 +1,23 @@
{ config, pkgs, ... }:
let
gitInfo = import ../gitInfo.nix { inherit pkgs; };
in
{
imports =
[ # Include the results of the hardware scan.
let gitInfo = import ../gitInfo.nix { inherit pkgs; };
in {
imports = [
(if builtins.pathExists "/etc/nixos/hardware-configuration.nix" then
/etc/nixos/hardware-configuration.nix
];
else
./ci-hardware-configuration.nix)
];
# Bootloader.
boot.loader.systemd-boot = {
enable = true;
consoleMode = "max"; # Use the highest available resolution
consoleMode = "max"; # Use the highest available resolution
};
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages_6_13;
boot.kernelModules = [ "i915" ];
boot.kernelParams = [
# Realtime Preemption
"preempt=full"
@@ -45,20 +43,20 @@ in
# Should ONLY be used in completely trusted environments
# - Improves performance by 7-43%
"mitigation=off"
"intel_pstate=performance" # Intel CPU-specific performance mode (if applicable)
"intel_pstate=performance" # Intel CPU-specific performance mode (if applicable)
# Memory Management
"transparent_hugepage=always" # Use larger memory pages for memory intense applications
"nmi_watchdog=0" # Disable NMI watchdog for reduced CPU overhead and realtime execution
"nmi_watchdog=0" # Disable NMI watchdog for reduced CPU overhead and realtime execution
# High-throughput ethernet parameters
"pcie_aspm=off" # Disable PCIe power management for NICs
"intel_iommu=off" # Disable IOMMU (performance gain)
"pcie_aspm=off" # Disable PCIe power management for NICs
"intel_iommu=off" # Disable IOMMU (performance gain)
# Reliability
"panic=10" # Auto-reboot 10 seconds after kernel panic
"oops=panic" # Treat kernel oops as panic for auto-recovery
"usbcore.autosuspend=-1" # Possibly fixes dre disconnect issue?
"panic=10" # Auto-reboot 10 seconds after kernel panic
"oops=panic" # Treat kernel oops as panic for auto-recovery
"usbcore.autosuspend=-1" # Possibly fixes dre disconnect issue?
"isolcpus=2,3" # Isolate cpus 2 and 3 from scheduler for better latency, 2 runs ethercatthread and 3 runs server control-loop
"nohz_full=2,3" # In this mode, the periodic scheduler tick is stopped when only one task is running, reducing kernel interruptions on those CPUs.
@@ -68,10 +66,10 @@ in
# Add these system settings for a more comprehensive kiosk setup
boot.kernel.sysctl = {
"kernel.panic_on_oops" = 1; # Reboot on kernel oops
"kernel.panic" = 10; # Reboot after 10 seconds on panic
"vm.swappiness" = 10; # Reduce swap usage
"kernel.sysrq" = 1; # Enable SysRq for emergency control
"kernel.panic_on_oops" = 1; # Reboot on kernel oops
"kernel.panic" = 10; # Reboot after 10 seconds on panic
"vm.swappiness" = 10; # Reduce swap usage
"kernel.sysrq" = 1; # Enable SysRq for emergency control
};
nix = {
@@ -79,13 +77,11 @@ in
extraOptions = ''
experimental-features = nix-command flakes
'';
settings = {
sandbox = false;
};
settings = { sandbox = false; };
};
# Create a realtime group
users.groups.realtime = {};
users.groups.realtime = { };
# Configure real-time privileges
security.pam.loginLimits = [
@@ -209,7 +205,6 @@ in
extraPackages = with pkgs; [ mesa ];
};
services.libinput.enable = true;
services.libinput.touchpad.tapping = true;
services.touchegg.enable = true;
+54 -55
View File
@@ -1,9 +1,7 @@
{ config, pkgs, lib, ... }: {
home.stateVersion = "24.11";
home.packages = with pkgs; [
pkgs.qitechPackages.electron
];
home.packages = with pkgs; [ pkgs.qitechPackages.electron ];
# Add autostart entry for the QiTech electron app
xdg.configFile."autostart/de.qitech.control-electron.desktop".text = ''
@@ -18,53 +16,53 @@
X-GNOME-Autostart-enabled=true
X-GNOME-Autostart-Phase=Applications
'';
dconf.settings = {
# Set GNOME wallpaper
"org/gnome/desktop/background" = {
picture-uri = "https://i.postimg.cc/Z5XJtNMW/qitech-industries-wallpaper.jpg";
picture-uri-dark = "https://i.postimg.cc/Z5XJtNMW/qitech-industries-wallpaper.jpg";
picture-uri =
"https://i.postimg.cc/Z5XJtNMW/qitech-industries-wallpaper.jpg";
picture-uri-dark =
"https://i.postimg.cc/Z5XJtNMW/qitech-industries-wallpaper.jpg";
picture-options = "zoom";
};
# Enable on-screen keyboard
"org/gnome/desktop/a11y/applications" = {
screen-keyboard-enabled = true;
};
"org/gnome/desktop/a11y/applications" = { screen-keyboard-enabled = true; };
# Configure on-screen keyboard (optional)
"org/gnome/desktop/a11y" = {
always-show-universal-access-status = true;
};
"org/gnome/desktop/a11y" = { always-show-universal-access-status = true; };
# Disable screen blanking/timeout
"org/gnome/desktop/session" = {
idle-delay = lib.gvariant.mkUint32 0; # Use uint32 format to ensure proper type
idle-delay =
lib.gvariant.mkUint32 0; # Use uint32 format to ensure proper type
};
# Disable automatic suspend and screen dimming
"org/gnome/settings-daemon/plugins/power" = {
sleep-inactive-ac-type = "nothing";
sleep-inactive-battery-type = "nothing";
power-button-action = "nothing";
idle-dim = false;
sleep-inactive-ac-timeout = lib.gvariant.mkUint32 0;
sleep-inactive-battery-timeout = lib.gvariant.mkUint32 0;
ambient-enabled = false;
};
# Disable screen lock
"org/gnome/desktop/screensaver" = {
lock-enabled = false;
idle-activation-enabled = false;
lock-delay = lib.gvariant.mkUint32 0;
};
# Disable automatic suspend and screen dimming
"org/gnome/settings-daemon/plugins/power" = {
sleep-inactive-ac-type = "nothing";
sleep-inactive-battery-type = "nothing";
power-button-action = "nothing";
idle-dim = false;
sleep-inactive-ac-timeout = lib.gvariant.mkUint32 0;
sleep-inactive-battery-timeout = lib.gvariant.mkUint32 0;
ambient-enabled = false;
};
# Disable screen lock
"org/gnome/desktop/screensaver" = {
lock-enabled = false;
idle-activation-enabled = false;
lock-delay = lib.gvariant.mkUint32 0;
};
# Interface settings with hot-corners disabled
"org/gnome/desktop/interface" = {
enable-animations = true;
show-battery-percentage = true;
enable-hot-corners = false; # Prevent activities from opening with hot corners
enable-hot-corners =
false; # Prevent activities from opening with hot corners
};
# Show dock on all monitors and always visible
@@ -74,48 +72,49 @@
dock-fixed = true;
autohide = false;
intellihide = false;
multi-monitor = true; # Show dock on all monitors
isolate-monitors = false; # Don't isolate workspaces per monitor
isolate-workspaces = false; # Don't isolate dock content per workspace
multi-monitor = true; # Show dock on all monitors
isolate-monitors = false; # Don't isolate workspaces per monitor
isolate-workspaces = false; # Don't isolate dock content per workspace
};
# Configure display settings
"org/gnome/mutter" = {
workspaces-only-on-primary = false; # Show workspaces on all monitors
dynamic-workspaces = false; # Disable dynamic workspaces
workspaces-only-on-primary = false; # Show workspaces on all monitors
dynamic-workspaces = false; # Disable dynamic workspaces
};
"org/gnome/desktop/wm/preferences" = {
num-workspaces = 1; # Set a fixed number of workspaces (default: 4)
num-workspaces = 1; # Set a fixed number of workspaces (default: 4)
};
"org/gnome/desktop/lockdown" = {
disable-lock-screen = true; # Disable lock screen
disable-log-out = false; # Enable logout option (To be able to reboot manually)
disable-user-switching = true; # Disable user switching
disable-screensaver = true; # Disable screensaver
user-adminstration-disabled = true; # Disable user administration
disable-lock-screen = true; # Disable lock screen
disable-log-out =
false; # Enable logout option (To be able to reboot manually)
disable-user-switching = true; # Disable user switching
disable-screensaver = true; # Disable screensaver
user-adminstration-disabled = true; # Disable user administration
};
"org/gnome/shell" = {
always-show-log-out = true;
enabled-extensions = [
"dash-to-dock@micxgx.gmail.com"
"no-overview@fthx"
];
enabled-extensions =
[ "dash-to-dock@micxgx.gmail.com" "no-overview@fthx" ];
favorite-apps = [
"de.qitech.control-electron.desktop" # The desktop entry from the QiTech app
"de.qitech.control-electron.desktop" # The desktop entry from the QiTech app
"org.gnome.Settings.desktop"
];
disable-user-extensions = false;
disable-extension-version-validation = true;
welcome-dialog-last-shown-version = "999.999.999"; # Prevents welcome screen
looking-glass-history = [];
welcome-dialog-last-shown-version =
"999.999.999"; # Prevents welcome screen
looking-glass-history = [ ];
};
# Disable automatic Activities overview on startup
"org/gnome/shell/extensions/ding" = {
start-corner = "top-left"; # This can help with preventing Activities from opening
start-corner =
"top-left"; # This can help with preventing Activities from opening
};
};
}
+24 -87
View File
@@ -1,104 +1,41 @@
{ lib
, stdenv
, makeWrapper
, nodejs
, electron
, nodePackages
, git
, cacert
, fetchFromGitHub
}:
{ lib, buildNpmPackage, electron }:
stdenv.mkDerivation rec {
buildNpmPackage rec {
pname = "qitech-control-electron";
version = "1.0.0";
srcs = [ ../../electron ../../docs ];
sourceRoot = "electron";
nativeBuildInputs = [
nodejs
nodePackages.npm
git
makeWrapper
cacert
];
ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
# Environment variables for the build
SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
GIT_SSL_CAINFO = "${cacert}/etc/ssl/certs/ca-bundle.crt";
npm_config_update_notifier = false;
npm_config_cache = "$TMPDIR/npm-cache";
makeCacheWritable = true;
npmDepsHash = "sha256-gk/YC3+gc5DVvPS4ght9WmFcuc2FAX/UB+lIuQn436U=";
npmFlags = [ "--no-audit" "--no-fund" ];
# Let Electron Forge see more output
ELECTRON_ENABLE_LOGGING = "true";
DEBUG = "*";
installPhase = ''
runHook preInstall
buildPhase = ''
export HOME=$TMPDIR
mkdir -p $out/share/qitech-control-electron
cp -rv dist/* $out/share/qitech-control-electron
cp -rv dist-electron/* $out/share/qitech-control-electron
cp -v src/assets/icon.png $out/share/qitech-control-electron
# Setup git
mkdir -p $HOME
${git}/bin/git config --global --add safe.directory '*'
${git}/bin/git config --global user.email "nixbuild@localhost"
${git}/bin/git config --global user.name "Nix Builder"
if [ -f src/assets/icon.png ]; then
mkdir -p $out/share/icons/hicolor/256x256/apps $out/share/pixmaps
# Install dependencies
echo "Installing dependencies..."
npm ci --no-audit --no-fund || npm install --no-audit --no-fund --legacy-peer-deps
# First try the 'make' command which does a full build + package
echo "Creating distributable..."
NODE_ENV=production npm run make || {
echo "Make failed, trying package instead..."
NODE_ENV=production npm run package
}
# List the output directories to see what we got
echo "Created artifacts:"
find out -type d | sort
find out -type f -name "*.js" | sort
'';
installPhase = ''
mkdir -p $out/share/qitech-control-electron $out/bin $out/share/applications $out/share/icons/hicolor/256x256/apps
# First check for a full distribution in the 'out/make' directory
if [ -d "out/make" ]; then
echo "Using distribution from 'out/make'"
find out/make -type d -name "*linux*" -exec cp -r {} $out/share/qitech-control-electron \; || true
fi
# If no distribution was found, use the packaged version
if [ ! "$(ls -A $out/share/qitech-control-electron)" ]; then
echo "Using packaged app from 'out'"
find out -maxdepth 1 -type d -name "*linux-x64" -exec cp -r {}/* $out/share/qitech-control-electron \; || true
fi
# If still nothing, try the Electron Forge template approach
if [ ! "$(ls -A $out/share/qitech-control-electron)" ]; then
echo "No packaged app found, using source + .vite directory"
cp -r . $out/share/qitech-control-electron/
fi
# Copy the icon
if [ -f "src/assets/icon.png" ]; then
echo "Copying icon from src/assets/icon.png"
cp src/assets/icon.png $out/share/icons/hicolor/256x256/apps/de.qitech.control-electron.png
# Also create a symlink with the application name for compatibility
mkdir -p $out/share/pixmaps
ln -sf $out/share/icons/hicolor/256x256/apps/de.qitech.control-electron.png $out/share/pixmaps/qitech-control-electron.png
ln -sf $out/share/icons/hicolor/256x256/apps/de.qitech.control-electron.png \
$out/share/pixmaps/qitech-control-electron.png
fi
# Create the executable wrapper with proper app ID and niceness
# electron needs a little bit of niceness, otherwise it will lag
# wrapper
mkdir -p $out/bin
cat > $out/bin/qitech-control-electron << EOF
#!/bin/sh
cd $out/share/qitech-control-electron
# enable gpu and ignore driver blacklists for electron
# gpu drivers may be blacklisted because of the usage of PREEMPT_RT Kernel
exec nice -n -20 ${electron}/bin/electron "$out/share/qitech-control-electron" \
appdir="$out/share/qitech-control-electron"
cd "\$appdir"
exec nice -n -20 ${electron}/bin/electron "\$appdir/main.js" \
--no-sandbox \
--class=de.qitech.control-electron \
--name="QiTech Control" \
@@ -108,11 +45,12 @@ installPhase = ''
--ignore-gpu-blocklist \
--enable-gpu-rasterization \
--disable-software-rasterizer \
"\$@"
"$@"
EOF
chmod +x $out/bin/qitech-control-electron
# Create desktop entry with consistent application ID
# desktop entry
mkdir -p $out/share/applications
cat > $out/share/applications/de.qitech.control-electron.desktop << EOF
[Desktop Entry]
Type=Application
@@ -127,7 +65,6 @@ installPhase = ''
EOF
'';
meta = with lib; {
description = "QiTech Control Electron";
homepage = "https://qitech.de";
+20 -30
View File
@@ -1,38 +1,28 @@
{ lib
, pkgs
, pkg-config
, libudev-zero
, libpcap
, craneLib
}:
{ lib, pkgs, pkg-config, libudev-zero, libpcap, craneLib }:
let
# Bind variables so they can be inherited inside inner calls
pname = "server";
version = "1.0.0";
strictDeps = true;
nativeBuildInputs = [ pkg-config ];
buildInputs = [ libpcap libudev-zero ];
commonArgs = {
pname = "server";
version = "1.0.0";
strictDeps = true;
# Cleaned source
src = craneLib.cleanCargoSource ../..;
nativeBuildInputs = [ pkg-config ];
buildInputs = [ libpcap libudev-zero ];
# Safe fallback for CARGO_BUILD_JOBS in impure builds
cargoJobs = if (builtins.tryEval (builtins.getEnv "CARGO_BUILD_JOBS")).success
then builtins.getEnv "CARGO_BUILD_JOBS"
else "2";
in
src = craneLib.cleanCargoSource ../..;
craneLib.buildPackage {
inherit pname version src strictDeps nativeBuildInputs buildInputs;
CARGO_BUILD_JOBS =
if (builtins.tryEval (builtins.getEnv "CARGO_BUILD_JOBS")).success then
builtins.getEnv "CARGO_BUILD_JOBS"
else
"2";
# Use the safe cargoJobs variable
CARGO_BUILD_JOBS = cargoJobs;
# Build cargo dependencies once for caching
cargoArtifacts = craneLib.buildDepsOnly {
inherit src strictDeps nativeBuildInputs buildInputs pname version;
cargoExtraArgs =
"--features tracing-journald,io-uring --no-default-features";
};
cargoExtraArgs = "-p server --features tracing-journald,io-uring --no-default-features";
}
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
in craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;
})