--- name: cross-platform-compatibility description: Handle cross-platform compatibility including file paths, environment detection, platform-specific dependencies, and testing across Windows, macOS, and Linux. Use when dealing with platform-specific code or OS compatibility. --- # Cross-Platform Compatibility ## Overview Comprehensive guide to writing code that works seamlessly across Windows, macOS, and Linux. Covers file path handling, environment detection, platform-specific features, and testing strategies. ## When to Use - Building applications for multiple operating systems - Handling file system operations - Managing platform-specific dependencies - Detecting operating system and architecture - Working with environment variables - Building cross-platform CLI tools - Dealing with line endings and character encodings - Managing platform-specific build processes ## Instructions ### 1. **File Path Handling** #### Node.js Path Module ```typescript // ❌ BAD: Hardcoded paths with platform-specific separators const configPath = "C:\\Users\\user\\config.json"; // Windows only const dataPath = "/home/user/data.txt"; // Unix only // ✅ GOOD: Use path module import path from "path"; import os from "os"; // Platform-independent path construction const configPath = path.join(os.homedir(), "config", "app.json"); const dataPath = path.join(process.cwd(), "data", "users.txt"); // Resolve relative paths const absolutePath = path.resolve("./config/settings.json"); // Get path components const dirname = path.dirname("/path/to/file.txt"); // '/path/to' const basename = path.basename("/path/to/file.txt"); // 'file.txt' const extname = path.extname("/path/to/file.txt"); // '.txt' // Normalize paths (handle .. and .) const normalized = path.normalize("/path/to/../file.txt"); // '/path/file.txt' ``` #### Python Path Handling ```python # ❌ BAD: Hardcoded separators config_path = 'C:\\Users\\user\\config.json' # Windows only data_path = '/home/user/data.txt' # Unix only # ✅ GOOD: Use pathlib from pathlib import Path import os # Platform-independent path construction config_path = Path.home() / 'config' / 'app.json' data_path = Path.cwd() / 'data' / 'users.txt' # Working with paths if config_path.exists(): content = config_path.read_text() # Get path components dirname = config_path.parent filename = config_path.name extension = config_path.suffix # Resolve relative paths absolute_path = Path('./config/settings.json').resolve() # Create directories output_dir = Path('output') output_dir.mkdir(parents=True, exist_ok=True) ``` #### Go Path Handling ```go package main import ( "os" "path/filepath" ) func main() { // ❌ BAD: Hardcoded paths // configPath := "C:\\Users\\user\\config.json" // ✅ GOOD: Use filepath package homeDir, _ := os.UserHomeDir() configPath := filepath.Join(homeDir, "config", "app.json") // Get path components dir := filepath.Dir(configPath) base := filepath.Base(configPath) ext := filepath.Ext(configPath) // Clean and normalize paths cleaned := filepath.Clean("path/to/../file.txt") // Convert to absolute path absPath, _ := filepath.Abs("./config/settings.json") } ``` ### 2. **Platform Detection** #### Node.js Platform Detection ```typescript // platform-utils.ts import os from "os"; export const Platform = { isWindows: process.platform === "win32", isMacOS: process.platform === "darwin", isLinux: process.platform === "linux", isUnix: process.platform !== "win32", get current(): "windows" | "macos" | "linux" | "unknown" { switch (process.platform) { case "win32": return "windows"; case "darwin": return "macos"; case "linux": return "linux"; default: return "unknown"; } }, get arch(): string { return process.arch; // 'x64', 'arm64', etc. }, get homeDir(): string { return os.homedir(); }, get tempDir(): string { return os.tmpdir(); }, }; // Usage if (Platform.isWindows) { // Windows-specific code console.log("Running on Windows"); } else if (Platform.isMacOS) { // macOS-specific code console.log("Running on macOS"); } else if (Platform.isLinux) { // Linux-specific code console.log("Running on Linux"); } // Architecture detection if (Platform.arch === "arm64") { console.log("Running on ARM architecture"); } ``` #### Python Platform Detection ```python # platform_utils.py import platform import sys class Platform: @staticmethod def is_windows(): return sys.platform.startswith('win') @staticmethod def is_macos(): return sys.platform == 'darwin' @staticmethod def is_linux(): return sys.platform.startswith('linux') @staticmethod def is_unix(): return not Platform.is_windows() @staticmethod def current(): if Platform.is_windows(): return 'windows' elif Platform.is_macos(): return 'macos' elif Platform.is_linux(): return 'linux' return 'unknown' @staticmethod def arch(): return platform.machine() # 'x86_64', 'arm64', etc. @staticmethod def version(): return platform.version() # Usage if Platform.is_windows(): # Windows-specific code print('Running on Windows') elif Platform.is_macos(): # macOS-specific code print('Running on macOS') elif Platform.is_linux(): # Linux-specific code print('Running on Linux') ``` ### 3. **Line Endings** ```typescript // line-endings.ts import os from "os"; export const LineEnding = { LF: "\n", // Unix/Linux/macOS CRLF: "\r\n", // Windows CR: "\r", // Old Mac (pre-OS X) get platform(): string { return os.EOL; // Returns platform-specific line ending }, normalize(text: string, target: string = os.EOL): string { // Normalize all line endings to target return text.replace(/\r\n|\r|\n/g, target); }, toUnix(text: string): string { return this.normalize(text, this.LF); }, toWindows(text: string): string { return this.normalize(text, this.CRLF); }, }; // Usage const fileContent = fs.readFileSync("file.txt", "utf8"); // Normalize to platform-specific line endings const normalized = LineEnding.normalize(fileContent); // Force Unix line endings (for git, etc.) const unixContent = LineEnding.toUnix(fileContent); // Write with platform-specific line endings fs.writeFileSync("output.txt", normalized); ``` ### 4. **Environment Variables** ```typescript // env-utils.ts export class EnvUtils { // Get environment variable with fallback static get(key: string, defaultValue?: string): string | undefined { return process.env[key] || defaultValue; } // Get PATH separator (: on Unix, ; on Windows) static get pathSeparator(): string { return process.platform === "win32" ? ";" : ":"; } // Split PATH into array static getPaths(): string[] { const pathVar = process.env.PATH || ""; return pathVar.split(this.pathSeparator); } // Get common paths static get home(): string { return process.env.HOME || process.env.USERPROFILE || ""; } static get user(): string { return process.env.USER || process.env.USERNAME || ""; } // Check if running in CI static get isCI(): boolean { return !!( process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI ); } } ``` ### 5. **Shell Commands** ```typescript // shell-utils.ts import { exec } from "child_process"; import { promisify } from "util"; const execAsync = promisify(exec); export class ShellUtils { // Execute command with platform-specific handling static async execute(command: string): Promise { try { const { stdout, stderr } = await execAsync(command, { shell: this.getShell(), }); if (stderr) console.error(stderr); return stdout.trim(); } catch (error) { throw new Error(`Command failed: ${error.message}`); } } // Get platform-specific shell static getShell(): string { if (process.platform === "win32") { return "cmd.exe"; } return process.env.SHELL || "/bin/sh"; } // Platform-specific commands static async listFiles(directory: string): Promise { if (process.platform === "win32") { return this.execute(`dir "${directory}"`); } return this.execute(`ls -la "${directory}"`); } static async clearScreen(): Promise { if (process.platform === "win32") { await this.execute("cls"); } else { await this.execute("clear"); } } static async openFile(filepath: string): Promise { if (process.platform === "win32") { await this.execute(`start "" "${filepath}"`); } else if (process.platform === "darwin") { await this.execute(`open "${filepath}"`); } else { await this.execute(`xdg-open "${filepath}"`); } } } ``` ### 6. **File Permissions** ```typescript // permissions.ts import fs from "fs"; import path from "path"; export class FilePermissions { // Make file executable (Unix only) static makeExecutable(filepath: string): void { if (process.platform !== "win32") { fs.chmodSync(filepath, 0o755); } } // Check if file is executable static isExecutable(filepath: string): boolean { if (process.platform === "win32") { // On Windows, check file extension const ext = path.extname(filepath).toLowerCase(); return [".exe", ".bat", ".cmd", ".com"].includes(ext); } try { fs.accessSync(filepath, fs.constants.X_OK); return true; } catch { return false; } } // Create file with specific permissions (Unix) static createWithPermissions( filepath: string, content: string, mode: number = 0o644, ): void { fs.writeFileSync(filepath, content, { mode }); } } ``` ### 7. **Process Management** ```typescript // process-utils.ts import { spawn, ChildProcess } from "child_process"; export class ProcessUtils { // Kill process by PID with platform-specific signal static kill(pid: number, signal?: string): void { if (process.platform === "win32") { // Windows doesn't support signals, use taskkill spawn("taskkill", ["/pid", pid.toString(), "/f", "/t"]); } else { process.kill(pid, signal || "SIGTERM"); } } // Spawn process with platform-specific handling static spawnCommand(command: string, args: string[] = []): ChildProcess { if (process.platform === "win32") { // Windows requires cmd.exe to run commands return spawn("cmd", ["/c", command, ...args], { stdio: "inherit", shell: true, }); } return spawn(command, args, { stdio: "inherit", shell: true, }); } // Find process by name static async findProcess(name: string): Promise { if (process.platform === "win32") { const { stdout } = await execAsync(`tasklist /FI "IMAGENAME eq ${name}"`); // Parse Windows tasklist output const pids: number[] = []; const lines = stdout.split("\n"); for (const line of lines) { const match = line.match(/\s+(\d+)\s+/); if (match) pids.push(parseInt(match[1])); } return pids; } else { const { stdout } = await execAsync(`pgrep ${name}`); return stdout.split("\n").filter(Boolean).map(Number); } } } ``` ### 8. **Platform-Specific Dependencies** ```json // package.json { "name": "my-app", "dependencies": { "common-dep": "^1.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" }, "devDependencies": { "@types/node": "^18.0.0" } } ``` ```typescript // platform-specific-module.ts export async function loadPlatformModule() { if (process.platform === "win32") { return await import("./windows/module"); } else if (process.platform === "darwin") { return await import("./macos/module"); } else { return await import("./linux/module"); } } // Graceful fallback for optional dependencies export function useFSEvents() { try { // fsevents is macOS only if (process.platform === "darwin") { const fsevents = require("fsevents"); return fsevents; } } catch (error) { console.warn("fsevents not available, using fallback"); } // Fallback to chokidar or fs.watch return require("chokidar"); } ``` ### 9. **Testing Across Platforms** #### GitHub Actions Matrix ```yaml # .github/workflows/test.yml name: Cross-Platform Tests on: [push, pull_request] jobs: test: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: [16, 18, 20] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Platform-specific tests if: runner.os == 'Windows' run: npm run test:windows - name: Platform-specific tests if: runner.os == 'macOS' run: npm run test:macos - name: Platform-specific tests if: runner.os == 'Linux' run: npm run test:linux ``` #### Platform-Specific Tests ```typescript // tests/platform.test.ts import { Platform } from "../src/platform-utils"; describe("Platform-specific tests", () => { describe("File paths", () => { it("should handle paths correctly", () => { const configPath = path.join(os.homedir(), "config.json"); if (Platform.isWindows) { expect(configPath).toMatch(/^[A-Z]:\\/); } else { expect(configPath).toMatch(/^\//); } }); }); describe.skipIf(Platform.isWindows)("Unix-only tests", () => { it("should work with symlinks", () => { // Symlink tests }); it("should handle file permissions", () => { // Permission tests }); }); describe.skipIf(!Platform.isWindows)("Windows-only tests", () => { it("should work with UNC paths", () => { // UNC path tests }); it("should handle drive letters", () => { // Drive letter tests }); }); }); ``` ### 10. **Character Encoding** ```typescript // encoding-utils.ts import iconv from "iconv-lite"; export class EncodingUtils { // Read file with specific encoding static readFile(filepath: string, encoding: string = "utf8"): string { const buffer = fs.readFileSync(filepath); if (encoding === "utf8") { // Remove BOM if present if (buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) { return buffer.slice(3).toString("utf8"); } return buffer.toString("utf8"); } return iconv.decode(buffer, encoding); } // Write file with specific encoding static writeFile( filepath: string, content: string, encoding: string = "utf8", ): void { if (encoding === "utf8") { fs.writeFileSync(filepath, content, "utf8"); } else { const buffer = iconv.encode(content, encoding); fs.writeFileSync(filepath, buffer); } } // Detect encoding static detectEncoding(filepath: string): string { const buffer = fs.readFileSync(filepath); // Check for BOM if (buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) { return "utf8"; } if (buffer[0] === 0xfe && buffer[1] === 0xff) { return "utf16be"; } if (buffer[0] === 0xff && buffer[1] === 0xfe) { return "utf16le"; } // Default to UTF-8 return "utf8"; } } ``` ### 11. **Build Configuration** ```typescript // rollup.config.js export default { input: "src/index.ts", output: [ { file: "dist/index.js", format: "cjs", }, { file: "dist/index.esm.js", format: "esm", }, ], external: [ // Mark platform-specific modules as external "fsevents", ], plugins: [ // Replace platform checks at build time for better tree-shaking replace({ "process.platform": JSON.stringify(process.platform), preventAssignment: true, }), ], }; ``` ## Best Practices ### ✅ DO - Use path.join() or path.resolve() for paths - Use os.EOL for line endings - Detect platform at runtime when needed - Test on all target platforms - Use optionalDependencies for platform-specific modules - Handle file permissions gracefully - Use shell escaping for user input - Normalize line endings in text files - Use UTF-8 encoding by default - Document platform-specific behavior - Provide fallbacks for platform-specific features - Use CI/CD to test on multiple platforms ### ❌ DON'T - Hardcode file paths with backslashes or forward slashes - Assume Unix-only features (signals, permissions, symlinks) - Ignore Windows-specific quirks (drive letters, UNC paths) - Use platform-specific commands without fallbacks - Assume case-sensitive file systems - Forget about different line endings - Use platform-specific APIs without checking - Hardcode environment variable access patterns - Ignore character encoding issues ## Common Patterns ### Pattern 1: Platform Factory ```typescript export interface PlatformHandler { openFile(path: string): Promise; getConfigPath(): string; } class WindowsHandler implements PlatformHandler { async openFile(path: string) { await exec(`start "" "${path}"`); } getConfigPath() { return path.join(process.env.APPDATA!, "myapp", "config.json"); } } class UnixHandler implements PlatformHandler { async openFile(path: string) { await exec(`xdg-open "${path}"`); } getConfigPath() { return path.join(os.homedir(), ".config", "myapp", "config.json"); } } export function createPlatformHandler(): PlatformHandler { return process.platform === "win32" ? new WindowsHandler() : new UnixHandler(); } ``` ### Pattern 2: Conditional Imports ```typescript const platformModule = await (async () => { switch (process.platform) { case "win32": return import("./platforms/windows"); case "darwin": return import("./platforms/macos"); default: return import("./platforms/linux"); } })(); ``` ## Tools & Resources - **cross-env**: Set environment variables cross-platform - **cross-spawn**: Cross-platform spawn - **rimraf**: Cross-platform rm -rf - **mkdirp**: Cross-platform mkdir -p - **cpy**: Cross-platform file copying - **del**: Cross-platform file deletion - **execa**: Better child_process - **pkg**: Package Node.js apps for all platforms