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
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,25 @@ jobs:
run: |-
go version
go test -ldflags '-extldflags "-static"' -tags static,system_libgit2 -v ./...

cross-compile:
name: Cross Compile (windows/amd64)
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4.3.1

- name: Install Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod

- name: Build and vet
env:
GOOS: windows
GOARCH: amd64
CGO_ENABLED: "0"
run: |-
go build ./...
go vet ./...
4 changes: 2 additions & 2 deletions cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ package cmd
import (
"context"
"fmt"
"os"
"strings"
"syscall"

"golang.org/x/crypto/ssh/terminal"

Expand Down Expand Up @@ -84,7 +84,7 @@ func runLogin(ctx context.Context, registry string) error {
}
} else if loginConfig.PasswordStdin && loginConfig.Password == "" {
fmt.Print("Enter password: ")
password, err := terminal.ReadPassword(syscall.Stdin)
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return err
}
Expand Down
6 changes: 1 addition & 5 deletions pkg/backend/build/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"io"
"os"
"path/filepath"
"syscall"
"time"

sha256 "github.com/minio/sha256-simd"
Expand Down Expand Up @@ -417,10 +416,7 @@ func getFileMetadata(path string) (modelspec.FileMetadata, error) {
}

// UID and GID (Unix-specific).
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
metadata.Uid = stat.Uid
metadata.Gid = stat.Gid
}
metadata.Uid, metadata.Gid = fileOwner(info)

return metadata, nil
}
32 changes: 8 additions & 24 deletions pkg/backend/build/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"testing"
"time"

Expand Down Expand Up @@ -499,18 +497,10 @@ func TestGetFileMetadata(t *testing.T) {
assert.Equal(t, byte(0), metadata.Typeflag, "Typeflag should be 0 for regular file")
assert.WithinDuration(t, fileInfo.ModTime(), metadata.ModTime, time.Second)

// Check UID/GID only on Unix-like systems
if stat, ok := fileInfo.Sys().(*syscall.Stat_t); ok {
assert.Equal(t, stat.Uid, metadata.Uid, "UID mismatch")
assert.Equal(t, stat.Gid, metadata.Gid, "GID mismatch")
} else if runtime.GOOS != "windows" {
// If not windows and not syscall.Stat_t, something is unexpected
t.Logf("Warning: Could not get syscall.Stat_t on non-Windows OS (%s)", runtime.GOOS)
} else {
// On Windows, expect 0 as syscall.Stat_t assertion fails
assert.Equal(t, uint32(0), metadata.Uid, "UID should be 0 on Windows")
assert.Equal(t, uint32(0), metadata.Gid, "GID should be 0 on Windows")
}
// UID and GID are only available on Unix-like systems and are 0 elsewhere.
wantUID, wantGID := expectedFileOwner(t, fileInfo)
assert.Equal(t, wantUID, metadata.Uid, "UID mismatch")
assert.Equal(t, wantGID, metadata.Gid, "GID mismatch")
})

// --- Test Case 2: Directory ---
Expand All @@ -528,15 +518,9 @@ func TestGetFileMetadata(t *testing.T) {
assert.Equal(t, byte(5), metadata.Typeflag, "Typeflag should be 5 for directory")
assert.WithinDuration(t, dirInfo.ModTime(), metadata.ModTime, time.Second)

// Check UID/GID only on Unix-like systems
if stat, ok := dirInfo.Sys().(*syscall.Stat_t); ok {
assert.Equal(t, stat.Uid, metadata.Uid, "UID mismatch")
assert.Equal(t, stat.Gid, metadata.Gid, "GID mismatch")
} else if runtime.GOOS != "windows" {
t.Logf("Warning: Could not get syscall.Stat_t on non-Windows OS (%s)", runtime.GOOS)
} else {
assert.Equal(t, uint32(0), metadata.Uid, "UID should be 0 on Windows")
assert.Equal(t, uint32(0), metadata.Gid, "GID should be 0 on Windows")
}
// UID and GID are only available on Unix-like systems and are 0 elsewhere.
wantUID, wantGID := expectedFileOwner(t, dirInfo)
assert.Equal(t, wantUID, metadata.Uid, "UID mismatch")
assert.Equal(t, wantGID, metadata.Gid, "GID mismatch")
})
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//go:build !unix

/*
* Copyright 2025 The ModelPack Authors
* Copyright 2026 The ModelPack Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,6 +16,11 @@
* limitations under the License.
*/

package version
package build

import "os"

const platform = "darwin"
// fileOwner returns zero IDs because the platform has no numeric file owners.
func fileOwner(info os.FileInfo) (uid, gid uint32) {
return 0, 0
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//go:build !unix

/*
* Copyright 2025 The ModelPack Authors
* Copyright 2026 The ModelPack Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,6 +16,17 @@
* limitations under the License.
*/

package version
package build

import (
"os"
"testing"
)

// expectedFileOwner returns zero IDs because the platform has no numeric file
// owners.
func expectedFileOwner(t *testing.T, info os.FileInfo) (uid, gid uint32) {
t.Helper()

const platform = "linux"
return 0, 0
}
33 changes: 33 additions & 0 deletions pkg/backend/build/owner_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build unix

/*
* Copyright 2026 The ModelPack Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package build

import (
"os"
"syscall"
)

// fileOwner returns the numeric user and group IDs that own the file.
func fileOwner(info os.FileInfo) (uid, gid uint32) {
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
return stat.Uid, stat.Gid
}

return 0, 0
}
37 changes: 37 additions & 0 deletions pkg/backend/build/owner_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//go:build unix

/*
* Copyright 2026 The ModelPack Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package build

import (
"os"
"syscall"
"testing"

"github.com/stretchr/testify/require"
)

// expectedFileOwner returns the numeric owner IDs reported by the OS.
func expectedFileOwner(t *testing.T, info os.FileInfo) (uid, gid uint32) {
t.Helper()

stat, ok := info.Sys().(*syscall.Stat_t)
require.True(t, ok, "expected *syscall.Stat_t, got %T", info.Sys())

return stat.Uid, stat.Gid
}
24 changes: 5 additions & 19 deletions pkg/diskspace/check.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux || darwin || windows

/*
* Copyright 2025 The CNAI Authors
*
Expand All @@ -14,17 +16,12 @@
* limitations under the License.
*/

//go:build linux || darwin

package diskspace

import (
"fmt"
"math"
"os"
"path/filepath"

"golang.org/x/sys/unix"
)

const (
Expand All @@ -42,7 +39,7 @@ func Check(dir string, requiredBytes int64) error {
return nil
}

// Ensure the directory exists for statfs; walk up to find an existing parent.
// Ensure the directory exists for the disk space query; walk up to find an existing parent.
checkDir := dir
for {
if _, err := os.Stat(checkDir); err == nil {
Expand All @@ -56,22 +53,11 @@ func Check(dir string, requiredBytes int64) error {
checkDir = parent
}

var stat unix.Statfs_t
if err := unix.Statfs(checkDir, &stat); err != nil {
availableBytes, err := availableSpace(checkDir)
if err != nil {
return fmt.Errorf("failed to check disk space for %s: %w", dir, err)
}

// Available space for non-root users.
// Guard against overflow: on Linux Bavail is uint64, and values exceeding
// math.MaxInt64 would wrap negative when cast to int64. Cap at MaxInt64.
bavail := stat.Bavail
bsize := uint64(stat.Bsize)
var availableBytes int64
if bavail > 0 && bsize > uint64(math.MaxInt64)/bavail {
availableBytes = math.MaxInt64
} else {
availableBytes = int64(bavail * bsize)
}
requiredWithMargin := int64(float64(requiredBytes) * safetyMargin)

if availableBytes < requiredWithMargin {
Expand Down
7 changes: 3 additions & 4 deletions pkg/diskspace/check_other.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build !linux && !darwin && !windows

/*
* Copyright 2025 The CNAI Authors
*
Expand All @@ -14,16 +16,13 @@
* limitations under the License.
*/

//go:build !linux && !darwin

package diskspace

import "github.com/sirupsen/logrus"

// Check is a no-op stub for platforms that do not yet have a disk-space
// implementation. It always returns nil so callers can compile and run on
// Windows and other non-Unix systems, at the cost of losing the pre-check
// warning. Native Windows support is tracked upstream.
// those systems, at the cost of losing the pre-check warning.
func Check(dir string, requiredBytes int64) error {
logrus.Debugf("diskspace: pre-check skipped on this platform (dir=%s, required=%d)", dir, requiredBytes)
return nil
Expand Down
4 changes: 2 additions & 2 deletions pkg/diskspace/check_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//go:build linux || darwin || windows

/*
* Copyright 2025 The CNAI Authors
*
Expand All @@ -14,8 +16,6 @@
* limitations under the License.
*/

//go:build linux || darwin

package diskspace

import (
Expand Down
44 changes: 44 additions & 0 deletions pkg/diskspace/space_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//go:build linux || darwin

/*
* Copyright 2026 The ModelPack Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package diskspace

import (
"math"

"golang.org/x/sys/unix"
)

// availableSpace returns the number of bytes available to non-root users on
// the filesystem containing dir.
func availableSpace(dir string) (int64, error) {
var stat unix.Statfs_t
if err := unix.Statfs(dir, &stat); err != nil {
return 0, err
}

// Guard against overflow: on Linux Bavail is uint64, and values exceeding
// math.MaxInt64 would wrap negative when cast to int64. Cap at MaxInt64.
bavail := stat.Bavail
bsize := uint64(stat.Bsize)
if bavail > 0 && bsize > uint64(math.MaxInt64)/bavail {
return math.MaxInt64, nil
}

return int64(bavail * bsize), nil
}
Loading
Loading