zeno · PORTFOLIO · v0.0.1 [05] BLOG
~/blog/cross-platform-config zeno@portfolio
zeno@portfolio:~/blog $ cat cross-platform-config
· 4 min read tags: #dotfiles #bash #configuration #dev-env

# Cross Platform Config Manager

A small dotfiles system that handles Linux/macOS differences via symlinks — idempotent, organised by specificity.

Introduction

This is a short write-up on dotfiles management for developers working across multiple machines (different operating systems). Dotfiles are configuration files that customize your development environment, and when building a dotfiles system, we need to handle OS differences and ensure consistency across machines.

Dotfiles Management

Generally, dotfiles management systems require two properties to be useful:

Consistency: Your configurations should work the same way across different machines.

Portability: Your setup should work across different operating systems without manual tweaking.

A dotfiles system requires both properties. A simple approach would be to copy and paste config files from an external backup (i.e. a github repository), but this can be tiring, and maintaining the dotfiles themselves can become annoying. As such, symlinking copies configuration files to their target locations, and allows us to modify dotfiles directly from the dotfile folder, simplifying git versioning.

File Structure

We can organize configs by specificity. We have two main ways to differentiate a config folder/file. General dotfiles are configurations that are OS-agnostic, meaning that they may work on both platforms. For example, my neovim configurations do not have any OS-specific features, thus my linux machine and macos machine both share the same configurations. OS-specific configurations represent configuration files unique to that platform.

The script processes OS-specific configs first, then general configs. This allows general configs to override OS-specific ones where conflicts exist.

# Process OS-specific dotfiles first
create_config_symlinks "$DOTFILES_DIR"
create_home_symlinks "$DOTFILES_DIR"

# Then process general dotfiles
create_config_symlinks "$GENERAL_DIR"
create_home_symlinks "$GENERAL_DIR"

The directory structure:

~/Documents/dotFiles/
├── general/
│   └── package_A_config/
├── linux/
│   └── package_B_config/
└── mac/
    └── package_C_config/

OS Detection

The script detects the operating system:

if [[ "$OSTYPE" == "linux-gnu"* ]]; then
  OS_DIR="linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
  OS_DIR="mac"
else
  OS_DIR="mac"
fi

This allows the same script to work on different platforms.

The script creates symbolic links from target locations to the repository. Before creating each symlink, it removes any existing file:

if [ -e "$CONFIG_DIR/$folder_name" ]; then
  rm -rf "$CONFIG_DIR/$folder_name"
fi
ln -sf "$folder" "$CONFIG_DIR/$folder_name"

Configuration Types

The script handles two types of configurations:

Config directories: Applications store configs in ~/.config/. These are handled by create_config_symlinks().

Dotfiles: Traditional dotfiles like .bashrc and .vimrc go in the home directory. These are handled by create_home_symlinks().

Config directories are entire folders, while dotfiles are individual files.

Functions

create_config_symlinks() {
  local source_dir=$1
  if [ ! -d "$source_dir" ]; then
    return
  fi

  for folder in $(find "$source_dir" -maxdepth 1 -type d -not -path "$source_dir"); do
    folder_name=$(basename "$folder")
    if [[ "$folder_name" != .* ]]; then
      if [ -e "$CONFIG_DIR/$folder_name" ]; then
        rm -rf "$CONFIG_DIR/$folder_name"
      fi
      ln -sf "$folder" "$CONFIG_DIR/$folder_name"
    fi
  done
}

create_home_symlinks() {
  local source_dir=$1
  if [ ! -d "$source_dir" ]; then
    return
  fi

  for file in $(find "$source_dir" -maxdepth 1 -type f -name ".*"); do
    file_name=$(basename "$file")
    if [ -e "$HOME/$file_name" ]; then
      rm -f "$HOME/$file_name"
    fi
    ln -sf "$file" "$HOME/$file_name"
  done
}

Deployment Process

The deployment follows these steps:

  1. Detect OS and set directory paths
  2. Create ~/.config if it doesn’t exist
  3. Check directory existence
  4. Process OS-specific configs first
  5. Process general configs to override where needed

The script is idempotent, meaning that it can be run multiple times safely. Symbolic links maintain a live connection to your repository, so changes are immediately reflected.

Full Script

#!/bin/bash

# Detect operating system and set the source directory accordingly
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
  OS_DIR="linux"
  echo "Linux detected, using linux configuration."
elif [[ "$OSTYPE" == "darwin"* ]]; then
  OS_DIR="mac"
  echo "macOS detected, using mac configuration."
else
  echo "Unknown operating system. Defaulting to mac configuration."
  OS_DIR="mac"
fi

# Define source directories
DOTFILES_DIR="$HOME/Documents/dotFiles/$OS_DIR"
GENERAL_DIR="$HOME/Documents/dotFiles/general"
CONFIG_DIR="$HOME/.config"

# Create .config directory if it doesn't exist
mkdir -p "$CONFIG_DIR"

# Check if OS-specific dotfiles directory exists
if [ ! -d "$DOTFILES_DIR" ]; then
  echo "Warning: OS-specific dotfiles directory $DOTFILES_DIR does not exist."
fi

# Check if general dotfiles directory exists
if [ ! -d "$GENERAL_DIR" ]; then
  echo "Warning: General dotfiles directory $GENERAL_DIR does not exist."
fi

# Function to create symlinks for config folders
create_config_symlinks() {
  local source_dir=$1

  if [ ! -d "$source_dir" ]; then
    return
  fi

  echo "Creating symlinks for config folders from $source_dir..."

  for folder in $(find "$source_dir" -maxdepth 1 -type d -not -path "$source_dir"); do
    folder_name=$(basename "$folder")

    if [[ "$folder_name" != .* ]]; then
      if [ -e "$CONFIG_DIR/$folder_name" ]; then
        echo "Removing existing $CONFIG_DIR/$folder_name"
        rm -rf "$CONFIG_DIR/$folder_name"
      fi

      echo "Creating symlink: $CONFIG_DIR/$folder_name -> $folder"
      ln -sf "$folder" "$CONFIG_DIR/$folder_name"
    fi
  done
}

# Function to create symlinks for home directory files (dotfiles)
create_home_symlinks() {
  local source_dir=$1

  if [ ! -d "$source_dir" ]; then
    return
  fi

  echo "Creating symlinks for home directory files from $source_dir..."

  for file in $(find "$source_dir" -maxdepth 1 -type f -name ".*"); do
    file_name=$(basename "$file")

    if [ -e "$HOME/$file_name" ]; then
      echo "Removing existing $HOME/$file_name"
      rm -f "$HOME/$file_name"
    fi

    echo "Creating symlink: $HOME/$file_name -> $file"
    ln -sf "$file" "$HOME/$file_name"
  done
}

# Process OS-specific dotfiles first
create_config_symlinks "$DOTFILES_DIR"
create_home_symlinks "$DOTFILES_DIR"

# Then process general dotfiles
create_config_symlinks "$GENERAL_DIR"
create_home_symlinks "$GENERAL_DIR"

echo "Symlinks created successfully!"
~/blog/cross-platform-config EOF
skip to content