Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/cidata/cidata.TEMPLATE.d/lima.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ LIMA_CIDATA_SHELL={{ .Shell }}
LIMA_CIDATA_HOSTHOME_MOUNTPOINT={{ .HostHomeMountPoint }}
LIMA_CIDATA_MOUNTS={{ len .Mounts }}
{{- range $i, $val := .Mounts}}
LIMA_CIDATA_MOUNTS_{{$i}}_TAG={{$val.Tag}}
LIMA_CIDATA_MOUNTS_{{$i}}_MOUNTPOINT={{$val.MountPoint}}
LIMA_CIDATA_MOUNTS_{{$i}}_TYPE={{$val.Type}}
LIMA_CIDATA_MOUNTS_{{$i}}_OPTIONS={{$val.Options}}
LIMA_CIDATA_MOUNTS_{{$i}}_LOCATION={{$val.Location}}
LIMA_CIDATA_MOUNTS_{{$i}}_WRITABLE={{if $val.Writable}}1{{else}}0{{end}}
{{- end}}
LIMA_CIDATA_MOUNTTYPE={{ .MountType }}
LIMA_CIDATA_DISKS={{ len .Disks }}
Expand Down
13 changes: 12 additions & 1 deletion pkg/cidata/cidata.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,18 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
options += ",failok"
}
}
args.Mounts = append(args.Mounts, Mount{Tag: tag, MountPoint: *f.MountPoint, Type: fstype, Options: options})
writable := false
if f.Writable != nil {
writable = *f.Writable
}
args.Mounts = append(args.Mounts, Mount{
Tag: tag,
MountPoint: *f.MountPoint,
Type: fstype,
Options: options,
Location: f.Location,
Writable: writable,
})
if f.Location == hostHome {
args.HostHomeMountPoint = *f.MountPoint
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/cidata/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ type Mount struct {
MountPoint string // abs path, accessible by the User
Type string
Options string
Location string
Writable bool
}
type BootCmds struct {
Lines []string
Expand Down
113 changes: 113 additions & 0 deletions pkg/driver/wsl2/boot.Linux/03-wsl-mounts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/bin/sh

# SPDX-FileCopyrightText: Copyright The Lima Authors
# SPDX-License-Identifier: Apache-2.0

[ "${LIMA_CIDATA_MOUNTS:-0}" -gt 0 ] || exit 0
[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0

INFO() {
echo "LIMA $(date -Iseconds)| mounts: $*"
}

WARNING() {
echo "LIMA $(date -Iseconds)| mounts: WARNING: $*" >&2
}

# shellcheck disable=SC1003
BACKSLASH='\'

for i in $(seq 0 $((LIMA_CIDATA_MOUNTS - 1))); do
location=""
mountpoint=""
writable=0
eval "location=\${LIMA_CIDATA_MOUNTS_${i}_LOCATION}"
eval "mountpoint=\${LIMA_CIDATA_MOUNTS_${i}_MOUNTPOINT}"
eval "writable=\${LIMA_CIDATA_MOUNTS_${i}_WRITABLE}"

if [ -z "$location" ] || [ -z "$mountpoint" ]; then
continue
fi

# Step 1: Translate Windows Host Path to WSL path
wsl_path=$(wslpath -u "$location" 2>/dev/null)
if [ -z "$wsl_path" ]; then
cleaned_loc="$location"
while [ ${#cleaned_loc} -gt 2 ] && { [ "${cleaned_loc#"${cleaned_loc%?}"}" = "/" ] || [ "${cleaned_loc#"${cleaned_loc%?}"}" = "$BACKSLASH" ]; }; do
cleaned_loc="${cleaned_loc%?}"
done
case "$cleaned_loc" in
[A-Za-z]:*)
drive=$(printf "%s" "$cleaned_loc" | cut -c1 | tr '[:upper:]' '[:lower:]')
rest=$(printf "%s" "$cleaned_loc" | cut -c3- | tr "$BACKSLASH" '/')
wsl_path="/mnt/${drive}${rest}"
;;
*)
wsl_path="$location"
;;
esac
fi

# If the mountPoint is the same as the WSL path, it is already natively accessible via WSL automount
if [ "$wsl_path" = "$mountpoint" ]; then
if [ "$writable" = "0" ]; then
# If the user requested read-only, we must bind-mount it onto itself and remount as read-only
# to enforce writable:false without affecting the parent native mount.
INFO "Enforcing read-only on native automount at $mountpoint"
if ! mount --bind "$mountpoint" "$mountpoint" 2>/dev/null || ! mount -o remount,ro,bind "$mountpoint" 2>/dev/null; then
WARNING "Path $location is natively read-write; cannot enforce writable:false on $mountpoint"
fi
else
INFO "Path $location is already available at $mountpoint via WSL automount"
fi
continue
fi

# Step 2: Idempotent Mount Check
if mountpoint -q "$mountpoint"; then
INFO "$mountpoint is already mounted, skipping"
else
mkdir -p "$mountpoint"
INFO "Mounting $wsl_path to $mountpoint (writable: $writable)"
if mount --bind "$wsl_path" "$mountpoint"; then
if [ "$writable" = "0" ]; then
if ! mount -o remount,ro,bind "$mountpoint"; then
WARNING "Failed to remount $mountpoint as read-only. Unmounting for safety."
umount "$mountpoint"
# Cleanup leaf directory and create symlink fallback
# Note: intermediate parent directories created by mkdir -p are left behind (harmless in practice)
rmdir "$mountpoint" 2>/dev/null || true
if [ ! -e "$mountpoint" ]; then
ln -sfn "$wsl_path" "$mountpoint"
WARNING "Fell back to symlink for $mountpoint, but note that symlinks CANNOT enforce read-only status. Target is writable!"
fi
fi
fi
INFO "Successfully bind-mounted $wsl_path to $mountpoint"
else
WARNING "Failed to bind mount $wsl_path to $mountpoint. Falling back to symlink. Note: symlink fallback is a degraded mode — tools relying on mount-point detection (df, container engines doing bind-mount-of-bind-mount) will not treat a symlinked path as a mount boundary."
# Cleanup leaf directory and create symlink
# Note: intermediate parent directories created by mkdir -p are left behind (harmless in practice)
rmdir "$mountpoint" 2>/dev/null || true
if [ ! -e "$mountpoint" ]; then
ln -sfn "$wsl_path" "$mountpoint"
INFO "Successfully symlinked $wsl_path to $mountpoint"
else
WARNING "Cannot create symlink at $mountpoint: file or non-empty directory exists"
fi
fi
fi
done

# Step 3: Merge [automount] options = "metadata" into /etc/wsl.conf
if grep -q '^\[automount\]' /etc/wsl.conf 2>/dev/null; then
# section exists — patch or insert the options= line within it
if awk '/^\[automount\]/{f=1;next} /^\[/{f=0} f&&/^options/{print; exit}' /etc/wsl.conf | grep -q .; then
sed -i '/^\[automount\]/,/^\[/ s|^options.*|options = "metadata"|' /etc/wsl.conf
else
sed -i '/^\[automount\]/a options = "metadata"' /etc/wsl.conf
fi
else
printf '\n[automount]\noptions = "metadata"\n' >>/etc/wsl.conf
fi
INFO "wsl.conf automount metadata options verified/merged"
29 changes: 29 additions & 0 deletions pkg/ioutilx/ioutilx.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,32 @@ func WindowsSubsystemPathForLinux(ctx context.Context, orig, distro string) (str
}
return strings.TrimSpace(string(out)), nil
}

// TranslateWindowsToWSLPath translates a Windows path (e.g., C:\Users\...) to a WSL-compliant path (e.g., /mnt/c/Users/...)
func TranslateWindowsToWSLPath(orig string) (string, error) {
cleaned := orig
// Trim trailing slashes/backslashes, but keep drive root if length <= 2
for len(cleaned) > 2 && (cleaned[len(cleaned)-1] == '/' || cleaned[len(cleaned)-1] == '\\') {
cleaned = cleaned[:len(cleaned)-1]
}

// UNC paths check
if strings.HasPrefix(cleaned, `\\`) || strings.HasPrefix(orig, `//`) {
return orig, fmt.Errorf("UNC paths are not supported for WSL translation: %q", orig)
}

// Recognizable drive letter paths like C: or C:\path or C:/path
if len(cleaned) >= 2 && cleaned[1] == ':' && ((cleaned[0] >= 'A' && cleaned[0] <= 'Z') || (cleaned[0] >= 'a' && cleaned[0] <= 'z')) {
drive := strings.ToLower(string(cleaned[0]))
if len(cleaned) == 2 {
return "/mnt/" + drive, nil
}
// Must be followed by a slash or backslash
if cleaned[2] == '/' || cleaned[2] == '\\' {
rest := strings.ReplaceAll(cleaned[2:], "\\", "/")
return "/mnt/" + drive + rest, nil
}
}

return orig, fmt.Errorf("not an absolute Windows path with drive letter: %q", orig)
}
87 changes: 87 additions & 0 deletions pkg/ioutilx/ioutilx_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package ioutilx

import (
"testing"

"gotest.tools/v3/assert"
)

func TestTranslateWindowsToWSLPath(t *testing.T) {
tests := []struct {
name string
input string
expected string
expectedErr string
}{
{
name: "Standard Windows path",
input: `C:\Users\example`,
expected: "/mnt/c/Users/example",
},
{
name: "Windows path with forward slashes and trailing slash",
input: `C:/Users/example/`,
expected: "/mnt/c/Users/example",
},
{
name: "Windows path with trailing backslash",
input: `C:\Users\example\`,
expected: "/mnt/c/Users/example",
},
{
name: "Drive root only with trailing slash",
input: `C:\`,
expected: "/mnt/c",
},
{
name: "Drive letter only",
input: `c:`,
expected: "/mnt/c",
},
{
name: "UNC path starts with double backslash",
input: `\\server\share\path`,
expected: `\\server\share\path`,
expectedErr: "UNC paths are not supported for WSL translation",
},
{
name: "UNC path starts with double slash",
input: `//server/share/path`,
expected: `//server/share/path`,
expectedErr: "UNC paths are not supported for WSL translation",
},
{
name: "Relative path",
input: `.\relative\path`,
expected: `.\relative\path`,
expectedErr: "not an absolute Windows path with drive letter",
},
{
name: "Relative path no dot",
input: `relative\path`,
expected: `relative\path`,
expectedErr: "not an absolute Windows path with drive letter",
},
{
name: "Alternative drive letter",
input: `D:\Projects`,
expected: "/mnt/d/Projects",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := TranslateWindowsToWSLPath(tt.input)
if tt.expectedErr != "" {
assert.ErrorContains(t, err, tt.expectedErr)
assert.Equal(t, actual, tt.expected)
} else {
assert.NilError(t, err)
assert.Equal(t, actual, tt.expected)
}
})
}
}
20 changes: 15 additions & 5 deletions pkg/limayaml/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,14 +710,24 @@ func FillDefault(ctx context.Context, y, d, o *limatype.LimaYAML, filePath strin
}
if mount.MountPoint == nil {
mountLocation := mount.Location
var err error
if runtime.GOOS == "windows" {
var err error
mountLocation, err = ioutilx.WindowsSubsystemPath(ctx, mountLocation)
if err != nil {
logrus.WithError(err).Warnf("Couldn't convert location %#q into mount target", mount.Location)
if y.VMType != nil && *y.VMType == limatype.WSL2 {
mountLocation, err = ioutilx.TranslateWindowsToWSLPath(mountLocation)
if err != nil {
logrus.WithError(err).Warnf("Couldn't convert location %#q into WSL mount target", mount.Location)
mountLocation = ""
}
} else {
mountLocation, err = ioutilx.WindowsSubsystemPath(ctx, mountLocation)
if err != nil {
logrus.WithError(err).Warnf("Couldn't convert location %#q into mount target", mount.Location)
}
}
}
mount.MountPoint = ptr.Of(mountLocation)
if mountLocation != "" {
mount.MountPoint = ptr.Of(mountLocation)
}
} else {
if out, err := executeGuestTemplate(*mount.MountPoint, instDir, y.User, y.Param); err == nil {
mount.MountPoint = ptr.Of(out.String())
Expand Down
4 changes: 4 additions & 0 deletions pkg/limayaml/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ func Validate(y *limatype.LimaYAML, warn bool) error {
}

for i, f := range y.Mounts {
if f.MountPoint == nil {
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].mountPoint` must be specified explicitly (failed to automatically determine default mount point)", i))
continue
}
if !filepath.IsAbs(f.Location) && !strings.HasPrefix(f.Location, "~") {
errs = errors.Join(errs, fmt.Errorf("field `mounts[%d].location` must be an absolute path, got %#q",
i, f.Location))
Expand Down