Revisiting C: Building a Valheim Server Management Tool
I recently decided to dive back into C to refresh my understanding of its fundamentals. To make it practical and fun, I created a command-line tool for managing a Valheim dedicated server. This project turned out to be a great way to work with core C concepts like memory management, file I/O, and process execution.
In this article, I'll walk you through the process of building this tool, from project structure to the final executable. It's a complete, working example that you can adapt for your own projects.
Project Goals
The main goal was to create a simple command-line interface (CLI) to handle the most common server management tasks. The script needed to:
- Start the server.
- Update the server using SteamCMD.
- Backup the world files.
- Check the server's running status.
The tool remotely manages the server by executing commands over SSH.
Project Structure
A well-organized C project is much easier to navigate and maintain. Here is the directory layout I settled on:
valheim-server-management/ ├── Makefile ├── .env ├── src/ │ ├── main.c │ ├── ui.c │ ├── config.c │ └── commands.c ├── include/ │ └── valserver.h └── obj/ │ ├── main.o │ ├── ui.o │ ├── config.o │ └── commands.o └── valserver
src/
: Contains all the C source files, with each file handling a specific part of the logic (main entrypoint, commands, configuration, UI).include/
: Holds the main header file,valserver.h
, which contains shared declarations.obj/
: Stores the compiled object files, keeping the root directory clean.Makefile
: The build script that compiles the source code into the finalvalserver
executable..env
: The configuration file for server credentials and paths.
Configuration Management
Hardcoding credentials and paths is a bad practice. I used a .env
file for configuration, which is a common convention.
Here's an example .env
file that needs to be created in the root directory:
SERVER_IP="0.0.0.0" SERVER_PORT="22" SERVER_USER="username" SERVER_VALHEIM_DIR="/home/steam/valheim" SERVER_MAP_PATH="/home/steam/.config/unity3d/IronGate/Valheim/worlds_local" LOCAL_BACKUP_PATH="~/yourpath/valheim-backups"
To load these variables into the C program, I wrote the config.c
file. It reads the .env
file, parses the key-value pairs, and stores them in global variables.
Here is the code for src/config.c
:
/**
* @file config.c
* @brief Handles configuration loading and validation.
*
* This file is responsible for managing the application's configuration.
* It defines the global configuration variables, loads settings from the .env file,
* and ensures that all necessary variables are set before the application runs.
*/
#include "valserver.h"
// --- Global Variable Definitions ---
// These are the actual definitions for the global variables declared with 'extern' in valserver.h.
// Memory will be allocated for them here. They are initialized to NULL, meaning they don't point to anything yet.
char* server_ip = NULL;
char* server_port = NULL;
char* server_user = NULL;
char* server_valheim_dir = NULL;
char* server_map_path = NULL;
char* local_backup_path = NULL;
// This array holds the names of the required configuration variables.
// It's used by check_config() to systematically verify that each variable has been loaded.
const char* config_vars[] = {
"SERVER_IP", "SERVER_PORT", "SERVER_USER",
"SERVER_VALHEIM_DIR", "SERVER_MAP_PATH", "LOCAL_BACKUP_PATH"
};
/**
* @brief Removes leading/trailing quotes from a string.
* @param str The string to trim.
*
* This is a helper function used by load_env() to clean up values read from
* the .env file. It modifies the string in-place.
* 'static' means this function is only visible within this file (config.c).
*/
static void trim_quotes(char *str) {
int len = strlen(str); // Get the length of the string.
if (len < 2) return; // If the string is too short to have quotes, do nothing.
// Check if the string starts and ends with double or single quotes.
if ((str[0] == '"' && str[len - 1] == '"') || (str[0] == '\'' && str[len - 1] == '\'')) {
// Move the entire string one character to the left, overwriting the opening quote.
memmove(str, str + 1, len - 2);
// Add a new null terminator to shorten the string by two characters (removing both quotes).
str[len - 2] = '\0';
}
}
/**
* @brief Loads configuration variables from the .env file.
*
* This function opens and reads the .env file line by line, parsing out
* key-value pairs. It then allocates memory for the values and stores them
* in the global configuration variables.
*/
void load_env() {
// Attempt to open the .env file in read mode ("r").
FILE *fp = fopen(ENV_FILE, "r");
// If the file cannot be opened, fp will be NULL.
if (!fp) {
fprintf(stderr, "%sWarning: %s not found. Please create it and try again.%s\n", RED, ENV_FILE, NC);
return; // Exit the function if the file doesn't exist.
}
printf("%sLoading configuration from %s%s\n", GREEN, ENV_FILE, NC);
char line[MAX_LINE_LEN]; // A buffer to hold one line from the file.
// Read the file line by line until the end.
while (fgets(line, sizeof(line), fp)) {
// Ignore lines that are comments (start with '#') or are empty.
if (line[0] == '#' || line[0] == '\n' || line[0] == '\r') {
continue;
}
// Find the position of the '=' character, which separates the key from the value.
char *separator = strchr(line, '=');
if (!separator) {
continue; // If there's no '=', it's not a valid line, so skip it.
}
*separator = '\0'; // Replace '=' with a null terminator to split the line into two strings.
char *key = line; // The key is now the part of the line before the '\0'.
char *value = separator + 1; // The part after.
// Remove any newline or carriage return characters from the end of the value.
value[strcspn(value, "\n\r")] = 0;
// Remove quotes from the value, e.g., "my_value" -> my_value.
trim_quotes(value);
// Compare the key with known configuration variable names and store the value.
// strdup() allocates new memory for the value and copies it. This is important!
if (strcmp(key, "SERVER_IP") == 0) server_ip = strdup(value);
else if (strcmp(key, "SERVER_PORT") == 0) server_port = strdup(value);
else if (strcmp(key, "SERVER_USER") == 0) server_user = strdup(value);
else if (strcmp(key, "SERVER_VALHEIM_DIR") == 0) server_valheim_dir = strdup(value);
else if (strcmp(key, "SERVER_MAP_PATH") == 0) server_map_path = strdup(value);
else if (strcmp(key, "LOCAL_BACKUP_PATH") == 0) local_backup_path = strdup(value);
}
fclose(fp); // Close the file to free up system resources.
}
/**
* @brief Checks if all required configuration variables have been set.
* @return 1 if the configuration is valid, 0 otherwise.
*
* This function iterates through the expected configuration variables and
* checks if they have been loaded (i.e., are not NULL). If any variable is
* missing, it prints an error and returns 0.
*/
int check_config() {
int missing = 0; // A flag to track if any variable is missing.
// An array of pointers to the actual configuration variables.
char** config_values[] = {&server_ip, &server_port, &server_user, &server_valheim_dir, &server_map_path, &local_backup_path};
// Loop through all the configuration variables.
for (size_t i = 0; i < sizeof(config_vars)/sizeof(config_vars[0]); i++) {
// Check if the pointer is NULL or points to an empty string.
if (*config_values[i] == NULL || strlen(*config_values[i]) == 0) {
// If it's missing, print an error message.
fprintf(stderr, "%sError: %s is not set in %s%s\n", RED, config_vars[i], ENV_FILE, NC);
missing = 1; // Set the flag.
}
}
return !missing; // Return true (1) if nothing was missing, false (0) otherwise.
}
Notice the use of strdup()
. Since the line
buffer is overwritten for each line in the file, we need to allocate new memory for each value and copy it. This is a classic C detail that's easy to forget!
The Header File: valserver.h
The valserver.h
header file is the glue that holds the project together. It contains all the shared declarations, constants, and function prototypes. This allows different .c
files to communicate with each other.
/**
* @file valserver.h
* @brief Header file for the Valheim server management tool.
*
* This file contains all the declarations for constants, global variables,
* and functions used across the entire valserver application. By including
* this single file, other source files can access a consistent set of
* definitions and function prototypes.
*/
#ifndef VALSERVER_H
#define VALSERVER_H
// These are standard C library headers that provide essential functions.
#include <stdio.h> // For standard input/output functions like printf, fprintf, fopen, etc.
#include <stdlib.h> // For memory allocation (malloc, free), process control (exit), and other utilities.
#include <string.h> // For string manipulation functions like strcmp, strcpy, strlen, etc.
#include <unistd.h> // For POSIX operating system API, including sleep().
#include <sys/stat.h> // For file status functions, though not directly used, it's good for file management tasks.
#include <time.h> // For time-related functions to generate date-stamped backups.
// --- Constant Definitions ---
// Defines the name of the environment file that holds the configuration.
#define ENV_FILE ".env"
// Defines the maximum length for a line read from a file.
#define MAX_LINE_LEN 256
// Defines the maximum length for a command string to be executed.
#define MAX_CMD_LEN 1024
// --- ANSI Color Code Definitions ---
// These macros are used to print colored text to the terminal for better readability.
#define GREEN "\033[0;32m" // Green text
#define RED "\033[0;31m" // Red text
#define YELLOW "\033[1;33m" // Yellow text
#define NC "\033[0m" // No Color (resets terminal color to default)
// --- Global Configuration Variables ---
// The 'extern' keyword tells the compiler that these variables are defined in another
// source file (in this case, src/config.c). This allows multiple files to share them.
extern char* server_ip; // IP address of the remote server.
extern char* server_port; // SSH port of the remote server.
extern char* server_user; // Username for SSH login.
extern char* server_valheim_dir; // Directory where the Valheim server is installed on the remote machine.
extern char* server_map_path; // Path to the Valheim world files on the remote server.
extern char* local_backup_path; // Path where backups will be stored locally.
// --- Function Prototypes ---
// Prototypes declare the functions that will be defined in the .c files.
// This allows files to call functions defined in other files.
// Functions from src/config.c
void load_env(); // Loads configuration from the .env file.
int check_config(); // Checks if all required configuration variables are set.
// Functions from src/ui.c
void display_help(); // Displays the help message with all available commands.
// Functions from src/commands.c
void start_server(); // Starts the Valheim server on the remote machine.
void update_server(); // Updates the Valheim server using steamcmd.
void backup_server(); // Backs up the server's world files.
void check_status(); // Checks the current status of the server.
#endif // VALSERVER_H
The extern
keyword is important here. It tells the compiler that the global variables are defined elsewhere (config.c
), so it doesn't try to create new ones.
The Main Entrypoint: main.c
The main.c
file is the starting point of the application. Its job is to:
- Load the configuration.
- Validate the configuration.
- Parse the command-line arguments.
- Execute the requested command.
- Free up allocated memory before exiting.
/**
* @file main.c
* @brief The main entry point for the valserver application.
*
* This file contains the main() function, which is the starting point of the
* program. It orchestrates the overall flow: loading the configuration,
* parsing user commands, and calling the appropriate functions.
*/
#include "valserver.h"
/**
* @brief Parses the command-line argument and executes the corresponding command.
* @param command The command string provided by the user.
*
* This function uses a series of if-else if statements to compare the user's
* command with the list of available commands and calls the appropriate function.
*/
void run_command(const char* command) {
// strcmp() compares two strings. It returns 0 if they are identical.
if (strcmp(command, "start") == 0) {
printf("%sStarting Valheim server...%s\n", GREEN, NC);
start_server(); // Call the function to start the server.
} else if (strcmp(command, "update") == 0) {
printf("%sUpdating Valheim server...%s\n", GREEN, NC);
update_server(); // Call the function to update the server.
} else if (strcmp(command, "backup") == 0) {
printf("%sBacking up world files...%s\n", GREEN, NC);
backup_server(); // Call the function to back up the server.
} else if (strcmp(command, "status") == 0) {
printf("%sChecking server status...%s\n", GREEN, NC);
check_status(); // Call the function to check the server status.
} else if (strcmp(command, "help") == 0) {
display_help(); // Call the function to display the help message.
} else {
// If the command is not recognized, print an error and show the help message.
fprintf(stderr, "%sUnknown command: %s%s\n", RED, command, NC);
display_help();
exit(1); // Exit the program with a non-zero status code to indicate an error.
}
}
/**
* @brief The main function and entry point of the program.
* @param argc The number of command-line arguments.
* @param argv An array of strings containing the command-line arguments.
* @return 0 on successful execution, 1 on error.
*/
int main(int argc, char *argv[]) {
// First, load the configuration from the .env file.
load_env();
// Next, check if all required configuration variables were loaded successfully.
if (!check_config()) {
// If the configuration is invalid, print an error and exit.
fprintf(stderr, "%sPlease fix your %s file and try again.%s\n", RED, ENV_FILE, NC);
return 1; // Return 1 to indicate an error.
}
// argc is the argument count. If it's less than 2, it means the user
// didn't provide a command (the program name itself is the first argument).
if (argc < 2) {
display_help(); // Show the help message.
return 0; // Exit successfully.
}
// The first argument (argv[0]) is the program name, e.g., "./valserver".
// The second argument (argv[1]) is the command, e.g., "start".
run_command(argv[1]);
// --- Memory Cleanup ---
// The configuration values were allocated using strdup() (which uses malloc()).
// It's crucial to free this memory before the program exits to prevent memory leaks.
free(server_ip);
free(server_port);
free(server_user);
free(server_valheim_dir);
free(server_map_path);
free(local_backup_path);
// Return 0 to indicate that the program finished successfully.
return 0;
}
Proper memory cleanup is critical in C. Since strdup()
uses malloc()
internally, I have to call free()
on each configuration variable before the program ends to prevent memory leaks.
Implementing the Server Commands
The core logic lives in src/commands.c
. This file implements the functions that construct and execute shell commands on the remote server via SSH.
/**
* @file commands.c
* @brief Core functions for managing the Valheim server.
*
* This file implements the main functionalities of the application, such as
* starting, updating, backing up, and checking the status of the Valheim server.
* These functions often construct and execute shell commands on the remote server via SSH.
*/
#include "valserver.h"
/**
* @brief Checks if the Valheim server process is running on the remote server.
* @param pid_buf A buffer to store the PID if the process is found.
* @param pid_buf_size The size of the PID buffer.
* @return 1 if the server is running, 0 if not, -1 on error.
*
* This 'static' function is only visible within this file. It uses 'pidof' on the
* remote server, which is a reliable way to get the Process ID (PID) of a
* specific executable.
*/
static int is_server_running(char* pid_buf, size_t pid_buf_size) {
char cmd[MAX_CMD_LEN]; // Buffer to hold the SSH command.
// snprintf is used to safely construct the command string. It prevents buffer overflows.
// The command runs 'pidof' on the remote server to find the PID of the Valheim process.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"pidof valheim_server.x86_64\"",
server_port, server_user, server_ip);
// popen() executes a command and opens a pipe to read its output.
FILE *pipe = popen(cmd, "r");
if (!pipe) {
perror("popen"); // If popen fails, print the system error and return -1.
return -1;
}
// fgets() reads the output from the command. If pidof found a process, it will output a PID.
// If a PID is read and it's longer than a single character (i.e., not just a newline),
// the server is considered to be running.
if (fgets(pid_buf, pid_buf_size, pipe) != NULL && strlen(pid_buf) > 1) {
pid_buf[strcspn(pid_buf, "\n")] = 0; // Remove the trailing newline from the PID.
pclose(pipe); // Close the pipe.
return 1; // Return 1 indicating the server is running.
}
pclose(pipe); // Close the pipe if no PID was found.
return 0; // Return 0 indicating the server is not running.
}
/**
* @brief Starts the Valheim server.
*
* This function first checks if the server is already running. If not, it constructs
* and executes an SSH command to start the server in the background using 'nohup'.
*/
void start_server() {
char cmd[MAX_CMD_LEN];
char pid[64]; // Buffer to hold the PID if the server is running.
printf("%sChecking if Valheim server is already running...%s\n", YELLOW, NC);
// Call our helper function to check the server status.
int status = is_server_running(pid, sizeof(pid));
if (status == 1) { // If the server is running...
printf("%sValheim server is already running with PID: %s%s\n", RED, pid, NC);
printf("%sUse the status command to see server details:%s ./valserver status\n", YELLOW, NC);
return; // Exit the function.
} else if (status == -1) { // If there was an error...
fprintf(stderr, "%sError checking server status.%s\n", RED, NC);
return; // Exit the function.
}
// If the server is not running, proceed to start it.
printf("%sStarting Valheim server in background...%s\n", GREEN, NC);
// Construct the SSH command to start the server.
// 'nohup ... &' ensures the server keeps running even after the SSH session ends.
// Output is redirected to a log file.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"cd %s && nohup ./start_server.sh > valheim_server.log 2>&1 &\"",
server_port, server_user, server_ip, server_valheim_dir);
system(cmd); // system() executes the command.
printf("%sValheim server started.%s\n", GREEN, NC);
sleep(5); // Wait for 5 seconds to give the server time to start up.
printf("%sVerifying server startup...%s\n", YELLOW, NC);
// Construct another command to verify that the process has indeed started.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"pidof valheim_server.x86_64 && echo -e '%sServer started successfully!%s' || echo -e '%sWARNING: Server may not have started properly.%s'\"",
server_port, server_user, server_ip, GREEN, NC, RED, NC);
system(cmd);
}
/**
* @brief Updates the Valheim server using steamcmd.
*
* This function constructs and executes a single SSH command to run steamcmd on the
* remote server and update the Valheim dedicated server files.
*/
void update_server() {
char cmd[MAX_CMD_LEN];
// This command logs in anonymously to Steam, sets the install directory, and runs the update.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"steamcmd +force_install_dir %s +login anonymous +app_update 896660 validate +exit\"",
server_port, server_user, server_ip, server_valheim_dir);
system(cmd); // Execute the update command.
printf("Valheim server update completed!\n");
}
/**
* @brief Backs up the world files from the remote server.
*
* This function creates a date-stamped local directory and then uses 'scp'
* to copy the world save files from the remote server to the local machine.
*/
void backup_server() {
char date_folder[256];
char cmd[MAX_CMD_LEN];
time_t t = time(NULL); // Get the current time.
struct tm *tm = localtime(&t); // Convert it to a local time structure.
// Format the time into a "YearMonthDay" string for the folder name.
strftime(date_folder, sizeof(date_folder), "%y%m%d", tm);
char local_path[512];
// Create the full local path for the backup.
snprintf(local_path, sizeof(local_path), "%s/%s", local_backup_path, date_folder);
char mkdir_cmd[512];
// Create the local directory. The '-p' flag ensures that parent directories are created if they don't exist.
snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p %s", local_path);
system(mkdir_cmd);
// An array of the filenames to back up.
const char* files_to_backup[] = {"Dedicated.fwl", "Dedicated.fwl.old", "Dedicated.db", "Dedicated.db.old"};
// Loop through each file and copy it.
for (int i = 0; i < 4; i++) {
// Construct the 'scp' (secure copy) command.
snprintf(cmd, sizeof(cmd), "scp -P %s %s@%s:%s/%s %s/",
server_port, server_user, server_ip, server_map_path, files_to_backup[i], local_path);
printf("Executing: %s\n", cmd);
system(cmd); // Execute the copy command.
}
printf("Backup completed to %s\n", local_path);
}
/**
* @brief Checks the status of the Valheim server.
*
* This function checks if the server is running and, if it is, displays
* detailed information like the process info, server uptime, and listening ports.
*/
void check_status() {
char cmd[MAX_CMD_LEN];
char pid[64];
printf("Checking Valheim server status...\n");
int status = is_server_running(pid, sizeof(pid));
if (status == 1) { // If the server is running...
printf("%s✓ Valheim server is running with PID: %s%s\n", GREEN, pid, NC);
printf("%sRaw Process Info:%s\n", YELLOW, NC);
fflush(stdout); // Force printing of the above line before the system() call.
// Construct a 'ps' command to get detailed process information for the specific PID.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"ps -p %s -o pid,user,%%cpu,%%mem,vsz,rss,stat,start,time,command | grep %s\"",
server_port, server_user, server_ip, pid, pid);
system(cmd);
printf("%sServer uptime:%s ", YELLOW, NC);
fflush(stdout);
// Get the remote server's uptime.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"uptime\"", server_port, server_user, server_ip);
system(cmd);
printf("%sChecking server ports:%s\n", YELLOW, NC);
fflush(stdout);
// Check for listening ports using 'netstat'.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"netstat -tuln | grep :2456\"", server_port, server_user, server_ip);
system(cmd);
} else if (status == 0) { // If the server is not running...
printf("%s✗ Valheim server is not running!%s\n", RED, NC);
printf("You can start it by running: ./valserver start\n");
printf("%sChecking for any valheim processes:%s\n", YELLOW, NC);
fflush(stdout);
// Run 'ps' just in case there are other related processes hanging around.
snprintf(cmd, sizeof(cmd), "ssh -p %s %s@%s \"ps aux | grep -i valheim | grep -v grep || echo 'No valheim processes found'\"",
server_port, server_user, server_ip);
system(cmd);
} else { // status == -1, an error occurred.
fprintf(stderr, "%sError checking server status.%s\n", RED, NC);
}
}
I used snprintf
to build the command strings safely, which prevents buffer overflows. The is_server_running
function uses popen
to execute a command and read its output, which is a neat way to capture the PID of the remote process.
The User Interface
For a CLI tool, a good help message is essential. The src/ui.c
file is dedicated to this.
/**
* @file ui.c
* @brief User-interface related functions.
*
* This file contains functions that are responsible for displaying
* information to the user, such as help messages and command usage.
*/
#include "valserver.h"
/**
* @brief Displays the help message.
*
* This function prints a formatted list of all available commands
* and their descriptions to the console. It uses the color macros
* defined in valserver.h to make the output more readable.
*/
void display_help() {
// Print the main usage line in yellow.
printf("%sUsage: ./valserver [command]%s\n", YELLOW, NC);
// Print a blank line for spacing.
printf("\n");
// Print the "Commands:" header in green.
printf("%sCommands:%s\n", GREEN, NC);
// Print each command, with the command name in yellow and the description in the default color.
printf(" %sstart%s Start the Valheim server\n", YELLOW, NC);
printf(" %supdate%s Update the Valheim server\n", YELLOW, NC);
printf(" %sbackup%s Backup the world files\n", YELLOW, NC);
printf(" %sstatus%s Check server status/health\n", YELLOW, NC);
printf(" %shelp%s Display this help message\n", YELLOW, NC);
// Print a final blank line for spacing.
printf("\n");
}
I added some simple ANSI color codes (defined in valserver.h
) to make the output easier to read.
Building with make
The Makefile
is the script that automates the compilation process. It defines how to build the final executable from the source files. This is a huge time-saver for any C project with more than one file.
# Makefile
# --- Compiler and Flags ---
# CC: C Compiler to be used. 'gcc' is the GNU C Compiler.
CC = gcc
# CFLAGS: Compiler Flags. These are options passed to the compiler.
# -Wall: Enables most of the compiler's warning messages.
# -Wextra: Enables even more warning messages not covered by -Wall.
# -std=c11: Specifies that the code should conform to the C11 standard.
# -Iinclude: Tells the compiler to look for header files in the 'include' directory.
CFLAGS = -Wall -Wextra -std=c11 -Iinclude
# LDFLAGS: Linker Flags. These are options passed to the linker.
# (Currently empty, but could be used for linking libraries, e.g., -lm for the math library).
LDFLAGS =
# --- Project Configuration ---
# TARGET: The name of the final executable file to be created.
TARGET = valserver
# SRC_DIR: The directory where the source (.c) files are located.
SRC_DIR = src
# OBJ_DIR: The directory where the compiled object (.o) files will be stored.
# Using a separate directory for object files keeps the project tidy.
OBJ_DIR = obj
# SRC: A list of all C source files.
# The 'wildcard' function finds all files in SRC_DIR that end with '.c'.
SRC = $(wildcard $(SRC_DIR)/*.c)
# OBJ: A list of all object files that correspond to the source files.
# The 'patsubst' (pattern substitution) function takes the list of source files
# and replaces each '.c' extension with '.o' and prepends the object directory path.
# For example, 'src/main.c' becomes 'obj/main.o'.
OBJ = $(patsubst $(SRC_DIR)/%.c,$(OBJ_DIR)/%.o,$(SRC))
# --- Rules ---
# .PHONY: Declares targets that are not actual files.
# 'all' and 'clean' are recipes to be executed, not files to be built.
# This prevents 'make' from getting confused if a file named 'clean' exists.
.PHONY: all clean
# The 'all' rule is the default goal. When you run 'make', this is the rule that runs.
# It depends on the $(TARGET) file, so 'make' will first try to build the target executable.
all: $(TARGET)
# This is the 'linking' rule. It builds the final executable ($(TARGET)).
# It depends on all the object files ($(OBJ)). This rule will only run after
# all the object files have been successfully created.
# '$^' is an automatic variable that means "all prerequisites" (i.e., all the .o files).
$(TARGET): $(OBJ)
$(CC) $(CFLAGS) -o $(TARGET) $^ $(LDFLAGS)
# This is the 'compiling' rule. It's a pattern rule that tells 'make' how to
# create an object file (.o) from a source file (.c).
# For each source file, 'make' will run this recipe.
# '$<' is an automatic variable that means "the first prerequisite" (the .c file).
# '$@' is an automatic variable that means "the target" (the .o file).
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
# The '@' symbol suppresses the command from being printed to the console.
@mkdir -p $(OBJ_DIR)
# This command creates the object directory if it doesn't exist.
$(CC) $(CFLAGS) -c -o $@ $<
# The '-c' flag tells gcc to compile the source file into an object file but not to link it yet.
# The 'clean' rule is used to remove generated files.
# When you run 'make clean', this recipe is executed.
clean:
# 'rm -rf' forcefully removes the object directory and the target executable.
# This is useful for clearing out old compiled files before a fresh build.
rm -rf $(OBJ_DIR) $(TARGET)
How to Use It
With all the pieces in place, using the tool is simple:
-
Create the
.env
file in the root directory and fill it with your server details. -
Compile the code:
make clean && make
-
Run the commands:
Usage: ./valserver [command] Commands: start Start the Valheim server update Update the Valheim server backup Backup the world files status Check server status/health help Display this help message
Conclusion
Building this Valheim server manager was a fantastic refresher on C programming. It forced me to think about memory allocation, pointers, file handling, and how to structure a multi-file project. It's a practical example of how C is still a powerful choice for building efficient system utilities.
The code works on macOS and Linux. Feel free to adapt it for your own server management needs or use it as a starting point for your own C projects!