3. Cross-platform fundamentals

Cross-platform fundamentals 学习卡片

CAE automation scripts rarely stay on the machine where they were written. A Python driver that launches abaqus or ccx on a Linux cluster may need to run unchanged on a Windows workstation, and a batch file that works in cmd.exe will fail immediately in bash. The friction is not in the solver — it is in the operating-system layer beneath it. This chapter isolates the four areas where that friction concentrates: shell semantics, path handling, environment variables, and process execution. Understanding these differences is what separates a script that works on one machine from a tool that works everywhere.

Shells: Two Different Programming Models

POSIX shells (sh, bash, dash, zsh) and Windows shells (cmd.exe, PowerShell) are not dialects of the same language. They differ in quoting, variable expansion, exit-code propagation, and control flow.

In POSIX shells, single quotes suppress all expansion and double quotes allow variable and command substitution. Windows cmd.exe has no equivalent distinction: %VAR% expands inside double quotes, and there is no single-quote syntax at all. PowerShell is a third model — it passes objects rather than strings, and its quoting rules follow .NET conventions. A command that is correct in one shell is frequently a syntax error in another.

# POSIX: single quotes are literal, double quotes expand
echo '$HOME'      # prints: $HOME
echo "$HOME"      # prints: /home/user

:: cmd.exe: no literal-quoting equivalent
echo %USERPROFILE%   :: always expands

# PowerShell: backtick is the escape character
Write-Output "Path: $env:USERPROFILE"

Exit codes are another divergence. POSIX uses $? and treats 0 as success. cmd.exe uses %ERRORLEVEL%, and PowerShell exposes $LASTEXITCODE for native executables but $? for cmdlets. A wrapper that checks the wrong variable will silently treat a failed solver run as successful.

Path Handling: Separators, Roots, and Case

Windows uses backslash (\) as the separator and drive letters (C:\) as roots. POSIX uses forward slash (/) with a single root. Windows paths are case-insensitive; POSIX paths are case-sensitive. Windows also reserves characters that are legal on POSIX (< > : " | ? *), and enforces a legacy 260-character limit unless long paths are enabled.

The practical rule for cross-platform scripts is to never hard-code a separator. Use the language's path library instead of string concatenation:

# Python — correct
from pathlib import Path
inp = Path("cases") / "bracket" / "mesh.inp"

# Python — fragile
inp = "cases/bracket/mesh.inp"   # breaks if later joined with a Windows root

When a path must cross a shell boundary, quote it. Spaces in C:\Program Files\... or /opt/CAE Tools/... will otherwise be split into separate arguments. On Windows, a trailing backslash inside a quoted argument can escape the closing quote — a classic source of "unterminated string" errors when passing directories to solvers.

Never build a command by concatenating an unquoted path. Both shells will split on whitespace, and Windows will additionally interpret &, |, and ^ as control operators.

Environment Variables: Naming and Scope

Environment variables carry license servers, solver roots, and MPI configuration into child processes. The naming convention differs: POSIX uses UPPER_CASE with $VAR or ${VAR} expansion; Windows uses %VAR% in cmd.exe and $env:VAR in PowerShell. Windows variable names are case-insensitive; POSIX names are case-sensitive, so $Path and $PATH are different variables.

Scope also differs. POSIX exports propagate to children only when export is used. Windows distinguishes process, user, and machine scope, and changes made with setx do not affect the current session. A script that sets a license variable and immediately launches a solver may find the variable missing if it was set at the wrong scope.

# POSIX
export LM_LICENSE_FILE=27000@licsrv
abaqus job=bracket cpus=4 interactive

:: cmd.exe
set LM_LICENSE_FILE=27000@licsrv
abaqus job=bracket cpus=4 interactive

# PowerShell
$env:LM_LICENSE_FILE = "27000@licsrv"
abaqus job=bracket cpus=4 interactive

Process Execution: Spawning, Waiting, and Signals

Launching a solver is where the differences become operational. POSIX uses fork/exec and signals; Windows uses CreateProcess and has no POSIX signals. Termination, therefore, is not portable: kill -TERM has no direct Windows equivalent, and taskkill /F has no POSIX equivalent.

Argument passing is another trap. POSIX passes an argument vector directly; Windows passes a single command-line string that the child parses. This means quoting rules differ, and an argument containing spaces may arrive intact on Linux but split on Windows. Python's subprocess with a list of arguments handles this correctly on both platforms; passing a single string with shell=True does not.

import subprocess, sys

cmd = [solver, "job=bracket", "cpus=4", "interactive"]
proc = subprocess.run(cmd, capture_output=True, text=True)

if proc.returncode != 0:
    print("Solver failed:", proc.stderr, file=sys.stderr)
    sys.exit(proc.returncode)

Note the use of a list, the explicit returncode check, and the propagation of that code to the parent. This pattern behaves identically on Linux, macOS, and Windows, which is precisely the goal.

Cross-Platform Execution: POSIX vs Windows POSIX (bash / sh) Shell 'literal' "expands $VAR" exit code: $? Paths /opt/cae/cases/bracket.inp case-sensitive, single root Environment export LM_LICENSE_FILE=... $VAR, case-sensitive Process fork/exec, argv vector kill -TERM, SIGCHLD Windows (cmd / PowerShell) Shell %VAR% always expands exit code: %ERRORLEVEL% Paths C:\CAE\cases\bracket.inp case-insensitive, drive roots Environment set / setx / $env:VAR process/user/machine scope Process CreateProcess, command string taskkill /F, no signals portable layer pathlib subprocess

A Portable Wrapper Pattern

The following pattern is the minimum viable cross-platform launcher. It resolves the solver from an environment variable, builds the argument list, runs the process, and reports the exit code. It is deliberately short so the four concerns above remain visible.

import os, subprocess, sys
from pathlib import Path

solver = os.environ.get("CAE_SOLVER", "abaqus")
case = Path(os.environ.get("CAE_CASE_DIR", ".")) / "bracket"

cmd = [solver, f"job={case.name}", "cpus=4", "interactive"]
proc = subprocess.run(cmd, cwd=case.parent, capture_output=True, text=True)

print(proc.stdout)
if proc.returncode != 0:
    print(proc.stderr, file=sys.stderr)
    sys.exit(proc.returncode)

Three details make this portable. First, os.environ.get supplies a default so the script runs even when the variable is unset. Second, Path handles separators and roots. Third, subprocess.run with a list avoids shell quoting entirely. The same file runs under bash, cmd.exe, and PowerShell without modification.

Verification Checklist

ConcernCheckFailure symptom
Shell quotingRun under bash and cmd.exeSyntax error or literal $VAR in output
Path separatorTest with a path containing spacesArgument split, "file not found"
Env scopeEcho variable in child processEmpty value, license checkout fails
Exit codeForce a solver errorScript reports success on failure
TerminationInterrupt a running jobOrphaned solver process

Run each check on both target platforms before trusting a script in production. The cost is minutes; the alternative is a failed overnight solve discovered the next morning.

When a script must invoke a shell built-in rather than an executable, isolate that call in a small platform-specific function and keep the rest of the code shell-agnostic. This confines the non-portable surface to a few lines.

The four areas covered here — shell semantics, paths, environment variables, and process execution — account for the majority of cross-platform failures in CAE automation. Treating them as a single design concern, rather than patching them after a failure, is what makes a script genuinely portable.