Compare commits

...

5 Commits

12 changed files with 783 additions and 33 deletions

View File

@@ -66,6 +66,45 @@
registry.address.pikachu.self = "10.23.10.102";
modules.services.steamcmd = {
enable = true;
dataDir = "/pikachu/data/steamcmd";
apps.valheim = {
enable = true;
appId = 896660;
installDir = "/pikachu/data/servers/valheim/main";
workingDirectory = "/pikachu/data/servers/valheim/main";
dataDirs = [ "/pikachu/media/games/servers/valheim/main" ];
packages = [ pkgs.steam-run ];
environment = {
SteamAppId = "892970";
SERVER_PASSWORD = "foobarbaz";
};
command = [
"${pkgs.bash}/bin/bash"
"-c"
''
${pkgs.coreutils}/bin/install -d -m 0770 /pikachu/media/games/servers/valheim/main
export LD_LIBRARY_PATH="$PWD/linux64:$LD_LIBRARY_PATH"
exec steam-run ./valheim_server.x86_64 \
-nographics \
-batchmode \
-name main \
-port 2456 \
-world main \
-savedir /pikachu/media/games/servers/valheim/main \
-password "$SERVER_PASSWORD" \
-public 0
''
];
udpPorts = [
2456
2457
2458
];
};
};
guests.actualbudget = {
enable = true;
vlan = 20;

View File

@@ -0,0 +1,325 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.modules.services.steamcmd;
appType = lib.types.submodule (
{ config, name, ... }:
{
options = {
enable = lib.mkEnableOption "this SteamCMD app service";
appId = lib.mkOption {
type = lib.types.ints.positive;
example = 896660;
description = "Steam app ID for the dedicated server.";
};
installDir = lib.mkOption {
type = lib.types.str;
example = "/data/servers/valheim/main";
description = "Directory where SteamCMD installs the server files.";
};
workingDirectory = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/data/servers/valheim/main";
description = "Directory where the game service starts.";
};
command = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [
"/data/servers/valheim/main/valheim_server.x86_64"
"-name"
"main"
];
description = "Command that starts the game server.";
};
user = lib.mkOption {
type = lib.types.str;
default = cfg.user;
defaultText = lib.literalExpression "config.modules.services.steamcmd.user";
description = "User account that runs the game service.";
};
group = lib.mkOption {
type = lib.types.str;
default = cfg.group;
defaultText = lib.literalExpression "config.modules.services.steamcmd.group";
description = "Group account that owns writable server files.";
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = "Environment variables for the game service.";
};
environmentFiles = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = "Environment files loaded by the game service.";
};
dataDirs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "/media/games/servers/valheim/main" ];
description = "Additional writable directories created before the game service starts.";
};
updateOnStart = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Runs SteamCMD before each game service start.";
};
validateUpdate = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Passes validate to SteamCMD app_update.";
};
packages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = "Packages added to the game service path.";
};
tcpPorts = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ ];
description = "TCP ports opened for the game service.";
};
udpPorts = lib.mkOption {
type = lib.types.listOf lib.types.port;
default = [ ];
description = "UDP ports opened for the game service.";
};
restart = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enables the scheduled restart timer.";
};
onCalendar = lib.mkOption {
type = lib.types.str;
default = "daily";
example = "*-*-* 05:00:00";
description = "Calendar expression for the restart timer.";
};
randomizedDelaySec = lib.mkOption {
type = lib.types.str;
default = "15m";
description = "Maximum randomized delay for the restart timer.";
};
command = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "systemctl restart valheim.service";
description = "Command run by the restart timer.";
};
};
};
config = {
workingDirectory = lib.mkDefault config.installDir;
};
}
);
enabledApps = lib.filterAttrs (_: app: app.enable) cfg.apps;
prepareScript = name: app:
pkgs.writeShellScript "steamcmd-${name}-prepare" ''
set -eu
${pkgs.coreutils}/bin/install -d \
-o ${lib.escapeShellArg cfg.user} \
-g ${lib.escapeShellArg cfg.group} \
-m 0770 \
${lib.escapeShellArg cfg.dataDir}
${pkgs.coreutils}/bin/install -d \
-o ${lib.escapeShellArg app.user} \
-g ${lib.escapeShellArg app.group} \
-m 0770 \
${lib.escapeShellArg app.installDir} \
${lib.escapeShellArgs app.dataDirs}
'';
updateScript = name: app:
pkgs.writeShellScript "steamcmd-${name}-update" ''
set -eu
export HOME=${lib.escapeShellArg cfg.dataDir}
exec ${lib.getExe cfg.package} \
+force_install_dir ${lib.escapeShellArg app.installDir} \
+login anonymous \
+app_update ${toString app.appId} ${lib.optionalString app.validateUpdate "validate"} \
+quit
'';
startScript = name: app:
pkgs.writeShellScript "steamcmd-${name}-start" ''
set -eu
cd ${lib.escapeShellArg app.workingDirectory}
exec ${lib.escapeShellArgs app.command}
'';
restartCommand = name: app:
if app.restart.command != null then
app.restart.command
else
"systemctl restart ${lib.escapeShellArg name}.service";
in
{
options.modules.services.steamcmd = {
enable = lib.mkEnableOption "SteamCMD service management";
package = lib.mkPackageOption pkgs "steamcmd" { };
user = lib.mkOption {
type = lib.types.str;
default = "steamcmd";
description = "User account that runs SteamCMD.";
};
group = lib.mkOption {
type = lib.types.str;
default = "steamcmd";
description = "Group account that owns SteamCMD files.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/data/steamcmd";
description = "Directory where SteamCMD stores metadata and cache files.";
};
apps = lib.mkOption {
type = lib.types.attrsOf appType;
default = { };
description = "SteamCMD-managed game services keyed by service name.";
};
};
config = lib.mkMerge [
{
registry."stable-uid".${config.networking.hostName}.steamcmd = 20003;
}
(lib.mkIf cfg.enable {
assertions = [
{
assertion = enabledApps != { };
message = "modules.services.steamcmd.apps must contain at least one enabled app.";
}
];
users.groups.${cfg.group} = { };
users.users.${cfg.user} = {
isSystemUser = true;
uid = config.registry."stable-uid".${config.networking.hostName}.steamcmd;
group = cfg.group;
home = cfg.dataDir;
};
systemd.tmpfiles.rules =
[
"d ${cfg.dataDir} 0770 ${cfg.user} ${cfg.group} - -"
"z ${cfg.dataDir} 0770 ${cfg.user} ${cfg.group} - -"
]
++ lib.flatten (
lib.mapAttrsToList (_: app: [
"d ${app.installDir} 0770 ${cfg.user} ${cfg.group} - -"
"z ${app.installDir} 0770 ${cfg.user} ${cfg.group} - -"
]) enabledApps
);
systemd.services = lib.mkMerge (
lib.mapAttrsToList (name: app: {
"steamcmd-${name}-update" = {
description = "Update ${name} with SteamCMD";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
User = app.user;
Group = app.group;
WorkingDirectory = "-${cfg.dataDir}";
ExecStartPre = "+${prepareScript name app}";
ExecStart = updateScript name app;
};
};
${name} = {
description = "${name} game server";
after = [
"network-online.target"
"steamcmd-${name}-update.service"
];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
path = app.packages;
environment = app.environment;
serviceConfig = {
Type = "simple";
User = app.user;
Group = app.group;
WorkingDirectory = "-${app.workingDirectory}";
EnvironmentFile = app.environmentFiles;
ExecStartPre = [
"+${prepareScript name app}"
] ++ lib.optional app.updateOnStart (updateScript name app);
ExecStart = startScript name app;
Restart = "on-failure";
};
};
"steamcmd-${name}-restart" = lib.mkIf app.restart.enable {
description = "Restart ${name}";
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "steamcmd-${name}-restart" ''
set -eu
exec ${restartCommand name app}
'';
};
};
}) enabledApps
);
systemd.timers = lib.mkMerge (
lib.mapAttrsToList (name: app: {
"steamcmd-${name}-restart" = lib.mkIf app.restart.enable {
description = "Restart ${name} on a schedule";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = app.restart.onCalendar;
RandomizedDelaySec = app.restart.randomizedDelaySec;
Persistent = true;
};
};
}) enabledApps
);
networking.firewall.allowedTCPPorts = lib.flatten (lib.mapAttrsToList (_: app: app.tcpPorts) enabledApps);
networking.firewall.allowedUDPPorts = lib.flatten (lib.mapAttrsToList (_: app: app.udpPorts) enabledApps);
})
];
}

View File

@@ -5,31 +5,51 @@ end
set host $argv[1]
set branch $argv[2]
set branch_source explicit
if test -z "$branch"
set branch_source current
set branch (git branch --show-current)
end
if test -z "$branch"
echo "deploy-to: could not determine the current branch" >&2
return 2
end
git rev-parse --show-toplevel >/dev/null 2>&1
or begin
echo "deploy-to: run this from a git checkout" >&2
return 2
end
set dirty_lines (git status --porcelain)
if test (count $dirty_lines) -gt 0
echo "deploy-to: refusing to deploy with a dirty local worktree" >&2
set untracked_lines (git status --porcelain --untracked-files=normal | string match -r '^\?\? ')
if test (count $untracked_lines) -gt 0
echo "deploy-to: refusing to deploy untracked files" >&2
echo "deploy-to: git add new files that Nix must see, or remove them" >&2
return 1
end
git fetch origin
or return $status
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
if test "$branch_source" = explicit
echo "deploy-to: origin/$branch does not exist" >&2
return 1
end
set branch_source upstream
set upstream_ref (git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)
set branch (string replace -r '^origin/' '' -- $upstream_ref)
end
if test -z "$branch"; or not git rev-parse --verify --quiet refs/remotes/origin/$branch >/dev/null
set branch_source default
set origin_head (git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null)
set branch (string replace -r '^origin/' '' -- $origin_head)
end
if test -z "$branch"
echo "deploy-to: could not determine a deploy branch" >&2
return 2
end
set remote_ref refs/remotes/origin/$branch
git rev-parse --verify --quiet $remote_ref >/dev/null
or begin
@@ -37,17 +57,37 @@ or begin
return 1
end
set local_head (git rev-parse HEAD)
set remote_head (git rev-parse $remote_ref)
if test "$local_head" != "$remote_head"
echo "deploy-to: local HEAD differs from origin/$branch" >&2
echo "deploy-to: push or reset the branch before deploying" >&2
return 1
set has_local_changes false
if not git diff --quiet $remote_ref --
set has_local_changes true
end
set escaped_branch (string escape -- $branch)
set escaped_host (string escape -- $host)
set remote_command "cd ~/dev && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
set remote_command "cd ~/dev && git fetch origin && git switch -- $escaped_branch && git reset --hard origin/$escaped_branch && git clean -fd"
set patch_file ""
set remote_patch_file ""
if test "$has_local_changes" = true
set patch_file (mktemp)
set remote_patch_file "/tmp/deploy-to-$host.patch"
git diff --binary $remote_ref -- >$patch_file
or begin
rm -f $patch_file
return $status
end
scp $patch_file $host:$remote_patch_file
set scp_status $status
rm -f $patch_file
if test $scp_status -ne 0
return $scp_status
end
set escaped_remote_patch_file (string escape -- $remote_patch_file)
set remote_command "$remote_command && git apply --index $escaped_remote_patch_file && rm -f $escaped_remote_patch_file"
end
set remote_command "$remote_command && sudo nixos-rebuild switch --flake .#$escaped_host --print-build-logs"
ssh -t $host $remote_command

View File

@@ -0,0 +1,93 @@
# Add a SteamCMD server
## 1. Configure the generic SteamCMD module
1. Create or update `configs/nixos/modules/services/steamcmd.nix`.
2. Define `modules.services.steamcmd.enable`.
3. Create the `steamcmd` service user.
4. Configure the shared SteamCMD data directory.
5. Add one option for each Steam app service.
6. Run SteamCMD before each app service starts.
7. Update the Steam app on each service restart.
8. Add a restart timer with default settings.
9. Add an option that overrides the restart command.
10. Open declared in-machine firewall ports.
## 2. Configure the game service module
1. Create or update `configs/nixos/modules/services/<game>.nix`.
2. Define `modules.services.<game>.enable`.
3. Add the Steam app ID for the dedicated server.
4. Add runtime packages that the server binary needs.
5. Add `serverName` for the service identity.
6. Use `serverName` as the default world or save name.
7. Add a game-specific world or save option when the game supports it.
8. Add guest-internal paths for server files and durable state.
9. Add options for required ports.
10. Add options for required secret files or values.
11. Map the game options to `modules.services.steamcmd`.
12. Configure the game service command.
13. Pass secrets to the service without embedding secret values in Nix.
14. Open the required in-machine firewall ports.
## 3. Configure the guest module
1. Create or update `configs/nixos/guests/<game>.nix`.
2. Define `guests.<game>` options for host-level wiring.
3. Add `serverName`.
4. Pass `serverName` to `modules.services.<game>` inside `interior`.
5. Add host path options for SteamCMD data, server files, and durable state.
6. Use names such as `steamcmdPath`, `serverPath`, and `statePath`.
7. Mount each host path into the guest.
8. Mount sops-backed secret files read-only into the guest.
9. Pass guest paths for secret files to `modules.services.<game>`.
10. Set guest resource limits.
11. Keep VLAN placement and address wiring in the guest or host configuration.
## 4. Add host storage
1. Choose a host path for SteamCMD data.
2. Choose a host path for downloaded server files.
3. Choose a host path for world data, saves, and generated config.
4. Configure those paths through `guests.<game>`.
## 5. Add secrets
1. Add a sops secret for each durable server secret.
2. Configure the guest option that names each sops secret.
3. Do not put passwords or tokens in Nix strings.
4. Do not pass passwords or tokens as process arguments.
## 6. Configure networking
1. Keep game port knowledge in `modules.services.<game>`.
2. Open game ports from `modules.services.<game>`.
3. Configure guest VLAN placement from the guest or host configuration.
4. Add a DHCP reservation before you commit a stable guest address.
5. Forward public ports to the guest address when the server needs public access.
## 7. Enable the guest on the target host
1. Set `guests.<game>.enable = true`.
2. Set `guests.<game>.serverName` to the server identity.
3. Set the host paths for SteamCMD data, server files, and durable state.
4. Set the secret names for the server.
5. Set the guest network options required by the host.
## 8. Deploy the host
1. Run `deploy-to <host>` from a clean checkout.
2. Check `container@<game>.service` on the host.
3. Enter the guest with `nixos-container root-login <game>`.
4. Check the SteamCMD update service.
5. Check the game service with `systemctl status <unit>`.
6. Follow logs with `journalctl -u <unit> -f`.
## 9. Validate the server
1. Confirm the guest address with `machinectl status <game>`.
2. Connect from the LAN before you add public forwarding.
3. Restart the game service.
4. Confirm that the restart updates the Steam app.
5. Confirm that the restart timer uses the expected command.
6. Confirm that durable state lands in the mounted state path.

View File

@@ -3,13 +3,19 @@ load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library")
rust_library(
name = "ariadne_lib",
crate_name = "ariadne",
srcs = ["src/lib.rs"],
srcs = [
"lib.rs",
"pipeline.rs",
],
)
rust_binary(
name = "ariadne",
crate_name = "ariadne_cli",
srcs = ["src/main.rs"],
deps = [":ariadne_lib"],
srcs = ["main.rs"],
deps = [
":ariadne_lib",
"//third_party/crates:clap",
],
)

View File

@@ -3,6 +3,13 @@ name = "ariadne"
version = "0.1.0"
edition = "2024"
[lib]
path = "lib.rs"
[[bin]]
name = "ariadne"
path = "main.rs"
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,18 +1,53 @@
# Ariadne
Ariadne analyzes Clew output for a local Ren'Py game install.
Keep project-owned source under `src/ariadne/`.
Keep game-derived files local and ignored under `src/ariadne/output/`.
Ariadne analyzes Clew-produced data for local Ren'Py projects.
Clew owns extraction, preparation, and Ren'Py AST export.
Ariadne consumes Clew's `clew-renpy-ast-v1` JSON from `output/ast/`.
Ariadne maps that data into internal analysis state.
Ariadne consumes Clew output and builds analysis artifacts from it.
Keep project-owned source under `src/ariadne/`; keep generated or game-derived files out of Git.
## Bazel shape
## CLI
Run the current placeholder CLI with:
Run the default target, `all`:
```sh
bazel run //src/ariadne:ariadne
```
By default, Ariadne writes output under `output/`.
Use `--output-dir` to choose a different output directory:
```sh
bazel run //src/ariadne:ariadne -- --output-dir scratch/ariadne-output
```
Run through a specific target stage and its prerequisites:
```sh
bazel run //src/ariadne:ariadne -- parse
bazel run //src/ariadne:ariadne -- add-jump-edges
```
Available target stages:
- `all`
- `parse`
- `add-implicit-edges`
- `add-init-edges`
- `add-jump-edges`
Relative `--output-dir` paths resolve from the workspace root when Ariadne runs through `bazel run`.
## Pipeline skeleton
Ariadne currently has pipeline scaffolding only.
Each executable stage prepares an output directory at:
```text
<output-dir>/graph/<stage>/
```
If a stage output directory already exists and is not empty, Ariadne fails unless `--force` is set.
With `--force`, Ariadne removes the existing stage output directory before recreating it.
Stage implementations are placeholders and do not write graph artifacts yet.

59
src/ariadne/lib.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::fs;
use std::io;
use std::path::PathBuf;
use crate::pipeline::Stage;
pub mod pipeline;
const OUTPUT_GRAPH_DIR: &str = "graph";
pub type Result<T> = io::Result<T>;
#[derive(Debug, Clone)]
pub struct Config {
pub output_dir: PathBuf,
pub force: bool,
pub target_stage: pipeline::Stage,
}
pub fn run(config: &Config) -> Result<()> {
for stage in config.target_stage.with_prerequisites() {
run_stage(config, stage)?;
}
Ok(())
}
fn get_output_dir(config: &Config, stage: Stage) -> Result<PathBuf> {
let output_dir = config.output_dir.join(OUTPUT_GRAPH_DIR).join(stage.name());
if output_dir.exists() && !output_dir.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
format!("output path is not a directory: {path}", path = output_dir.display()),
));
}
if output_dir.is_dir() && fs::read_dir(&output_dir)?.next().transpose()?.is_some() {
if !config.force {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!(
"output directory already exists and is not empty: {path}",
path = output_dir.display()
),
));
}
fs::remove_dir_all(&output_dir)?;
}
fs::create_dir_all(&output_dir)?;
Ok(output_dir)
}
fn run_stage(config: &Config, stage: Stage) -> Result<()> {
let output_dir = get_output_dir(config, stage)?;
pipeline::run_stage(stage, &output_dir)
}

51
src/ariadne/main.rs Normal file
View File

@@ -0,0 +1,51 @@
use std::path::PathBuf;
use clap::Parser;
#[derive(Debug, Parser)]
struct Args {
#[arg(long, default_value = "output")]
output_dir: PathBuf,
#[arg(long)]
force: bool,
#[arg(default_value = "all", value_parser = parse_stage)]
target_stage: ariadne::pipeline::Stage,
}
impl From<Args> for ariadne::Config {
fn from(args: Args) -> Self {
ariadne::Config {
output_dir: resolve_workspace_path(args.output_dir),
force: args.force,
target_stage: args.target_stage,
}
}
}
fn parse_stage(value: &str) -> Result<ariadne::pipeline::Stage, String> {
ariadne::pipeline::Stage::ALL
.iter()
.copied()
.find(|stage| stage.name() == value)
.ok_or_else(|| format!("unknown stage: {value}"))
}
// TODO: Move this into a shared Rust utility crate.
fn resolve_workspace_path(path: PathBuf) -> PathBuf {
if path.is_absolute() {
return path;
}
if let Some(workspace) = std::env::var_os("BUILD_WORKSPACE_DIRECTORY") {
return PathBuf::from(workspace).join(path);
}
path
}
fn main() -> ariadne::Result<()> {
let config: ariadne::Config = Args::parse().into();
ariadne::run(&config)
}

101
src/ariadne/pipeline.rs Normal file
View File

@@ -0,0 +1,101 @@
use std::collections::BTreeSet;
use std::path::Path;
use crate::Result;
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)]
pub enum Stage {
#[default]
All,
Parse,
AddImplicitEdges,
AddInitEdges,
AddJumpEdges,
}
impl Stage {
pub const ALL: &'static [Stage] = &[
Stage::All,
Stage::Parse,
Stage::AddImplicitEdges,
Stage::AddInitEdges,
Stage::AddJumpEdges,
];
pub fn name(self) -> &'static str {
match self {
Stage::All => "all",
Stage::Parse => "parse",
Stage::AddImplicitEdges => "add-implicit-edges",
Stage::AddInitEdges => "add-init-edges",
Stage::AddJumpEdges => "add-jump-edges",
}
}
pub fn prerequisites(self) -> &'static [Stage] {
match self {
Stage::All => &[
Stage::AddImplicitEdges,
Stage::AddInitEdges,
Stage::AddJumpEdges,
],
Stage::Parse => &[],
Stage::AddImplicitEdges => &[Stage::Parse],
Stage::AddInitEdges => &[Stage::Parse],
Stage::AddJumpEdges => &[Stage::Parse],
}
}
pub fn with_prerequisites(self) -> Vec<Stage> {
self.with_prerequisites_impl(BTreeSet::new())
}
fn with_prerequisites_impl(self, seen: BTreeSet<Stage>) -> Vec<Stage> {
if seen.contains(&self) {
return Vec::new();
}
let mut seen = seen;
seen.insert(self);
let mut stages = Vec::new();
for prerequisite in self.prerequisites() {
let prerequisite_stages = prerequisite.with_prerequisites_impl(seen.clone());
seen.extend(prerequisite_stages.iter().copied());
stages.extend(prerequisite_stages);
}
if self != Stage::All {
stages.push(self);
}
stages
}
}
pub fn run_stage(stage: Stage, output_dir: &Path) -> Result<()> {
match stage {
Stage::All => Ok(()),
Stage::Parse => run_parse(output_dir),
Stage::AddImplicitEdges => run_add_implicit_edges(output_dir),
Stage::AddInitEdges => run_add_init_edges(output_dir),
Stage::AddJumpEdges => run_add_jump_edges(output_dir),
}
}
fn run_parse(_output_dir: &Path) -> Result<()> {
Ok(())
}
fn run_add_implicit_edges(_output_dir: &Path) -> Result<()> {
Ok(())
}
fn run_add_init_edges(_output_dir: &Path) -> Result<()> {
Ok(())
}
fn run_add_jump_edges(_output_dir: &Path) -> Result<()> {
Ok(())
}

View File

@@ -1,3 +0,0 @@
pub fn run() {
println!("Ariadne");
}

View File

@@ -1,3 +0,0 @@
fn main() {
ariadne::run();
}