Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

870 lines
27 KiB
Go

package managedruntime
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/tutti-os/tutti/packages/agent/daemon/httpx"
"github.com/tutti-os/tutti/packages/agent/daemon/runtimecmd"
tuttitypes "github.com/tutti-os/tutti/services/tuttid/types"
)
const tuttiAppRuntimeRootEnv = "TUTTI_APP_RUNTIME_ROOT"
const tuttiAppRuntimeCacheRootEnv = "TUTTI_APP_RUNTIME_CACHE_ROOT"
const tuttiAppRuntimeCatalogEnv = "TUTTI_APP_RUNTIME_CATALOG"
const appRuntimeCatalogSchemaVersion = "tutti.app.runtimes.v2"
const appRuntimeBaselineProfile = "baseline"
const appRuntimeNodeStaticProfile = "connector-node-static"
const appRuntimeRTKSaverProfile = "rtk-saver"
const defaultTuttiAppRuntimeCatalogURL = "https://d1x7gb6wqsqmnm.cloudfront.net/tutti-app-runtimes/catalog.json"
const NodeStaticProfile = appRuntimeNodeStaticProfile
const RTKSaverProfile = appRuntimeRTKSaverProfile
const maxManagedAppRuntimeArtifactBytes int64 = 512 * 1024 * 1024
const maxManagedAppRuntimeExpandedBytes int64 = 2 * 1024 * 1024 * 1024
const managedAppRuntimeCatalogRequestAttempts = 3
const managedAppRuntimeCatalogRetryBaseDelay = 100 * time.Millisecond
const managedAppRuntimeCatalogErrorDrainBytes int64 = 32 * 1024
type Resolver interface {
Resolve(context.Context) (ResolvedRuntime, error)
}
type ProfilePreloader interface {
PreloadProfile(context.Context, string) error
}
type ProfileResolver interface {
ResolveProfile(context.Context, string) (ResolvedRuntime, error)
}
type ResolvedRuntime struct {
Root string
Python string
Node string
NPM string
RTK string
BinDirs []string
EnvOverrides []string
}
type DefaultResolver struct {
RuntimeRoot string
Environ func() []string
HTTPClient *http.Client
}
type DownloadProgress struct {
DownloadedBytes int64
TotalBytes int64
}
type downloadProgressKey struct{}
type componentDownloadProgressKey struct{}
type appRuntimeCatalog struct {
SchemaVersion string `json:"schemaVersion"`
Runtimes map[string]appRuntimeCatalogEntry `json:"runtimes"`
}
type appRuntimeCatalogEntry struct {
Version string `json:"version"`
Components map[string]appRuntimeCatalogComponent `json:"components"`
Profiles map[string][]string `json:"profiles"`
ProfileABIs map[string]string `json:"profileAbis,omitempty"`
}
type appRuntimeCatalogComponent struct {
Version string `json:"version"`
ArtifactURL string `json:"artifactUrl"`
ArtifactSHA256 string `json:"artifactSha256"`
ArtifactSizeBytes int64 `json:"artifactSizeBytes,omitempty"`
}
type managedAppRuntimeRootLock struct {
token chan struct{}
users int
}
var managedAppRuntimeDownloadLocks = struct {
sync.Mutex
entries map[string]*managedAppRuntimeRootLock
}{entries: make(map[string]*managedAppRuntimeRootLock)}
func acquireManagedAppRuntimeDownloadLock(ctx context.Context, root string) (func(), error) {
managedAppRuntimeDownloadLocks.Lock()
lock := managedAppRuntimeDownloadLocks.entries[root]
if lock == nil {
lock = &managedAppRuntimeRootLock{token: make(chan struct{}, 1)}
managedAppRuntimeDownloadLocks.entries[root] = lock
}
lock.users++
managedAppRuntimeDownloadLocks.Unlock()
releaseUser := func() {
managedAppRuntimeDownloadLocks.Lock()
lock.users--
if lock.users == 0 && managedAppRuntimeDownloadLocks.entries[root] == lock {
delete(managedAppRuntimeDownloadLocks.entries, root)
}
managedAppRuntimeDownloadLocks.Unlock()
}
select {
case lock.token <- struct{}{}:
var once sync.Once
return func() {
once.Do(func() {
<-lock.token
releaseUser()
})
}, nil
case <-ctx.Done():
releaseUser()
return nil, ctx.Err()
}
}
func (r DefaultResolver) Resolve(ctx context.Context) (ResolvedRuntime, error) {
root := r.runtimeRoot()
if err := r.ensureRuntime(ctx, root); err != nil {
return ResolvedRuntime{}, err
}
components, err := r.baselineRuntimeComponents(ctx, root)
if err != nil {
return ResolvedRuntime{}, err
}
return r.resolvedRuntimeForComponents(root, components)
}
func (r DefaultResolver) PreloadProfile(ctx context.Context, profile string) error {
return r.ensureRuntimeProfile(ctx, r.runtimeRoot(), profile)
}
func (r DefaultResolver) ResolveProfile(ctx context.Context, profile string) (ResolvedRuntime, error) {
profile = strings.TrimSpace(profile)
if profile == "" || profile == appRuntimeBaselineProfile {
return r.Resolve(ctx)
}
root := r.runtimeRoot()
if err := r.ensureRuntimeProfile(ctx, root, profile); err != nil {
return ResolvedRuntime{}, err
}
if profile == appRuntimeNodeStaticProfile {
return r.resolvedRuntimeForComponents(root, []string{"node"})
}
componentNames, err := r.runtimeProfileComponentNames(ctx, profile)
if err != nil {
return ResolvedRuntime{}, err
}
return r.resolvedRuntimeForComponents(root, componentNames)
}
func (r DefaultResolver) runtimeRoot() string {
root := strings.TrimSpace(r.RuntimeRoot)
if root == "" {
root = strings.TrimSpace(envValue(r.environ(), tuttiAppRuntimeRootEnv))
}
if root == "" {
root = DefaultRoot(r.environ())
}
return filepath.Clean(root)
}
func (r DefaultResolver) resolvedRuntime(root string) (ResolvedRuntime, error) {
return r.resolvedRuntimeForComponents(root, []string{"python", "node"})
}
func (r DefaultResolver) resolvedRuntimeForComponents(root string, components []string) (ResolvedRuntime, error) {
pythonBinDir := filepath.Join(root, "python", "bin")
nodeBinDir := filepath.Join(root, "node", "bin")
python := filepath.Join(pythonBinDir, pythonBinaryName())
node := filepath.Join(nodeBinDir, nodeBinaryName())
npm := filepath.Join(nodeBinDir, npmBinaryName())
corepack := filepath.Join(nodeBinDir, corepackBinaryName())
rtkBinDir := filepath.Join(root, "rtk", "bin")
rtk := filepath.Join(rtkBinDir, rtkBinaryName())
var (
binDirs []string
envOverrides []string
resolved ResolvedRuntime
)
for _, component := range components {
switch strings.TrimSpace(component) {
case "python":
if !isExecutableFile(python) {
return ResolvedRuntime{}, fmt.Errorf("managed app runtime python executable is unavailable at %s", python)
}
resolved.Python = python
binDirs = append(binDirs, pythonBinDir)
envOverrides = append(envOverrides, "TUTTI_APP_PYTHON="+python)
case "node":
for name, path := range map[string]string{
"node": node,
"npm": npm,
} {
if !isExecutableFile(path) {
return ResolvedRuntime{}, fmt.Errorf("managed app runtime %s executable is unavailable at %s", name, path)
}
}
if !isStandaloneCorepackWrapper(corepack) {
return ResolvedRuntime{}, fmt.Errorf("managed app runtime corepack wrapper is unavailable or incompatible at %s", corepack)
}
resolved.Node = node
resolved.NPM = npm
binDirs = append(binDirs, nodeBinDir)
envOverrides = append(envOverrides, "TUTTI_APP_NODE="+node, "TUTTI_APP_NPM="+npm)
case "rtk":
if !isExecutableFile(rtk) {
return ResolvedRuntime{}, fmt.Errorf("managed app runtime rtk executable is unavailable at %s", rtk)
}
resolved.RTK = rtk
binDirs = append(binDirs, rtkBinDir)
envOverrides = append(envOverrides, "TUTTI_APP_RTK="+rtk)
}
}
binDirs = mergeAppPathDirs(binDirs)
resolved.Root = root
resolved.BinDirs = binDirs
resolved.EnvOverrides = append(
[]string{tuttiAppRuntimeRootEnv + "=" + root},
envOverrides...,
)
resolved.EnvOverrides = append(
resolved.EnvOverrides,
"PATH="+strings.Join(append(binDirs, filepath.SplitList(envValue(r.environ(), pathEnvKey(r.environ())))...), string(os.PathListSeparator)),
)
return resolved, nil
}
func (r DefaultResolver) ensureRuntime(ctx context.Context, root string) error {
components, err := r.baselineRuntimeComponents(ctx, root)
if err != nil {
return err
}
if runtimeComponentsReady(root, components) {
return nil
}
if err := r.ensureRuntimeProfile(ctx, root, appRuntimeBaselineProfile); err != nil {
return err
}
if !runtimeComponentsReady(root, components) {
return fmt.Errorf("managed app runtime artifact does not contain required baseline components: %s", strings.Join(components, ", "))
}
return nil
}
func (r DefaultResolver) baselineRuntimeComponents(ctx context.Context, root string) ([]string, error) {
if RootReady(root) {
return []string{"python", "node"}, nil
}
components, err := r.runtimeProfileComponentNames(ctx, appRuntimeBaselineProfile)
if err == nil {
return components, nil
}
// A fully materialized legacy runtime can still be resolved without a
// catalog. Keep that offline behavior for existing Unix installations while
// allowing platform catalogs (notably Windows) to define a node-only
// baseline.
return nil, err
}
func runtimeComponentsReady(root string, components []string) bool {
if len(components) == 0 {
return false
}
for _, component := range components {
if !appRuntimeComponentReady(root, component) {
return false
}
}
return true
}
func (r DefaultResolver) ensureRuntimeProfile(ctx context.Context, root string, profile string) error {
profile = strings.TrimSpace(profile)
if profile == "" {
return fmt.Errorf("managed app runtime profile is required")
}
release, err := acquireManagedAppRuntimeDownloadLock(ctx, root)
if err != nil {
return fmt.Errorf("wait for managed app runtime lock: %w", err)
}
defer release()
if profile == appRuntimeBaselineProfile && RootReady(root) {
return nil
}
if profile == appRuntimeNodeStaticProfile && NodeReady(root) {
return nil
}
catalogSource := r.runtimeCatalogSource()
if catalogSource == "" {
return fmt.Errorf("managed app runtime is unavailable at %s and %s is not configured", root, tuttiAppRuntimeCatalogEnv)
}
platformArch := appRuntimePlatformArch(runtime.GOOS, runtime.GOARCH)
catalog, err := r.loadCatalog(ctx, catalogSource)
if err != nil {
return err
}
entry, ok := catalog.Runtimes[platformArch]
if !ok {
return fmt.Errorf("managed app runtime catalog does not contain platform %q", platformArch)
}
componentNames, err := appRuntimeProfileComponentNames(entry, profile)
if err != nil {
return err
}
missing := make([]string, 0, len(componentNames))
for _, name := range componentNames {
if !appRuntimeComponentReady(root, name) {
missing = append(missing, name)
}
}
if len(missing) == 0 {
return nil
}
return r.downloadRuntime(ctx, root, entry, missing)
}
func (r DefaultResolver) runtimeProfileComponentNames(ctx context.Context, profile string) ([]string, error) {
catalogSource := r.runtimeCatalogSource()
if catalogSource == "" {
return nil, fmt.Errorf("managed app runtime catalog is unavailable and %s is not configured", tuttiAppRuntimeCatalogEnv)
}
platformArch := appRuntimePlatformArch(runtime.GOOS, runtime.GOARCH)
catalog, err := r.loadCatalog(ctx, catalogSource)
if err != nil {
return nil, err
}
entry, ok := catalog.Runtimes[platformArch]
if !ok {
return nil, fmt.Errorf("managed app runtime catalog does not contain platform %q", platformArch)
}
return appRuntimeProfileComponentNames(entry, profile)
}
func RootReady(root string) bool {
if strings.TrimSpace(root) == "" {
return false
}
runtime, err := DefaultResolver{RuntimeRoot: root}.resolvedRuntime(root)
return err == nil &&
runtime.Python != "" &&
runtime.Node != "" &&
runtime.NPM != ""
}
// NodeReady reports whether root contains a compatible managed Node component.
func NodeReady(root string) bool {
return appRuntimeComponentReady(root, "node")
}
func appRuntimeComponentReady(root string, name string) bool {
if strings.TrimSpace(root) == "" {
return false
}
switch strings.TrimSpace(name) {
case "python":
return isExecutableFile(filepath.Join(root, "python", "bin", pythonBinaryName()))
case "node":
nodeBinDir := filepath.Join(root, "node", "bin")
return isExecutableFile(filepath.Join(nodeBinDir, nodeBinaryName())) &&
isExecutableFile(filepath.Join(nodeBinDir, npmBinaryName())) &&
isStandaloneCorepackWrapper(filepath.Join(nodeBinDir, corepackBinaryName()))
case "rtk":
return isExecutableFile(filepath.Join(root, "rtk", "bin", rtkBinaryName()))
default:
info, err := os.Stat(filepath.Join(root, name))
return err == nil && info.IsDir()
}
}
func (r DefaultResolver) loadCatalog(ctx context.Context, source string) (appRuntimeCatalog, error) {
data, err := r.readCatalog(ctx, source)
if err != nil {
return appRuntimeCatalog{}, err
}
var catalog appRuntimeCatalog
if err := json.Unmarshal(data, &catalog); err != nil {
return appRuntimeCatalog{}, fmt.Errorf("parse managed app runtime catalog: %w", err)
}
if !isSupportedAppRuntimeCatalogSchemaVersion(strings.TrimSpace(catalog.SchemaVersion)) {
return appRuntimeCatalog{}, fmt.Errorf("unsupported managed app runtime catalog schema version %q", catalog.SchemaVersion)
}
if len(catalog.Runtimes) == 0 {
return appRuntimeCatalog{}, fmt.Errorf("managed app runtime catalog has no runtimes")
}
for platform, entry := range catalog.Runtimes {
if err := validateManagedAppRuntimeCatalogEntry(platform, entry); err != nil {
return appRuntimeCatalog{}, err
}
}
return catalog, nil
}
func isSupportedAppRuntimeCatalogSchemaVersion(schemaVersion string) bool {
return schemaVersion == appRuntimeCatalogSchemaVersion
}
func (r DefaultResolver) runtimeCatalogSource() string {
for _, item := range r.environ() {
key, value, ok := strings.Cut(item, "=")
if ok && key == tuttiAppRuntimeCatalogEnv {
return strings.TrimSpace(value)
}
}
return defaultTuttiAppRuntimeCatalogURL
}
func (r DefaultResolver) readCatalog(ctx context.Context, source string) ([]byte, error) {
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
var lastErr error
for attempt := 1; attempt <= managedAppRuntimeCatalogRequestAttempts; attempt++ {
data, retry, err := r.readRemoteCatalogOnce(ctx, source)
if err == nil {
return data, nil
}
lastErr = err
if !retry || attempt == managedAppRuntimeCatalogRequestAttempts {
return nil, err
}
if err := waitForManagedAppRuntimeCatalogRetry(ctx, attempt); err != nil {
return nil, fmt.Errorf("download managed app runtime catalog: %w", err)
}
}
return nil, lastErr
}
data, err := os.ReadFile(source)
if err != nil {
return nil, fmt.Errorf("read managed app runtime catalog: %w", err)
}
return data, nil
}
func (r DefaultResolver) readRemoteCatalogOnce(ctx context.Context, source string) ([]byte, bool, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil)
if err != nil {
return nil, false, fmt.Errorf("create managed app runtime catalog request: %w", err)
}
response, err := r.httpClient().Do(request)
if err != nil {
if ctx.Err() != nil {
return nil, false, fmt.Errorf("download managed app runtime catalog: %w", ctx.Err())
}
return nil, isTransientManagedAppRuntimeCatalogError(err), fmt.Errorf("download managed app runtime catalog: %w", err)
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, managedAppRuntimeCatalogErrorDrainBytes))
_ = response.Body.Close()
retry := response.StatusCode == http.StatusRequestTimeout ||
response.StatusCode == http.StatusTooManyRequests ||
response.StatusCode >= http.StatusInternalServerError
return nil, retry, fmt.Errorf("download managed app runtime catalog: unexpected status %d", response.StatusCode)
}
data, err := io.ReadAll(io.LimitReader(response.Body, 2*1024*1024+1))
_ = response.Body.Close()
if err != nil {
if ctx.Err() != nil {
return nil, false, fmt.Errorf("read managed app runtime catalog: %w", ctx.Err())
}
return nil, true, fmt.Errorf("read managed app runtime catalog: %w", err)
}
if len(data) > 2*1024*1024 {
return nil, false, fmt.Errorf("managed app runtime catalog exceeds maximum size")
}
return data, false, nil
}
func isTransientManagedAppRuntimeCatalogError(err error) bool {
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) ||
errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.ECONNREFUSED) ||
errors.Is(err, syscall.ECONNABORTED) || errors.Is(err, syscall.ETIMEDOUT) {
return true
}
var timeoutError interface{ Timeout() bool }
if errors.As(err, &timeoutError) && timeoutError.Timeout() {
return true
}
var temporaryError interface{ Temporary() bool }
return errors.As(err, &temporaryError) && temporaryError.Temporary()
}
func waitForManagedAppRuntimeCatalogRetry(ctx context.Context, attempt int) error {
timer := time.NewTimer(time.Duration(attempt) * managedAppRuntimeCatalogRetryBaseDelay)
defer timer.Stop()
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return nil
}
}
func (r DefaultResolver) downloadRuntime(ctx context.Context, root string, entry appRuntimeCatalogEntry, componentNames []string) error {
parent := filepath.Dir(root)
downloadsDir := filepath.Join(parent, "downloads")
if err := os.MkdirAll(downloadsDir, 0o755); err != nil {
return fmt.Errorf("create managed app runtime download dir: %w", err)
}
downloads, err := r.downloadRuntimeComponents(ctx, downloadsDir, entry.Components, componentNames)
defer func() {
for _, download := range downloads {
_ = os.Remove(download.archivePath)
}
}()
if err != nil {
return err
}
stagingDir, err := os.MkdirTemp(parent, filepath.Base(root)+".tmp-")
if err != nil {
return fmt.Errorf("create managed app runtime staging dir: %w", err)
}
defer func() {
_ = os.RemoveAll(stagingDir)
}()
for _, download := range downloads {
if err := extractZipWithLimits(download.archivePath, stagingDir, maxManagedAppRuntimeArtifactBytes, maxManagedAppRuntimeExpandedBytes); err != nil {
return fmt.Errorf("extract managed app runtime component %q: %w", download.name, err)
}
}
for _, name := range componentNames {
if !appRuntimeComponentReady(stagingDir, name) {
return fmt.Errorf("managed app runtime artifact does not contain %q component", name)
}
}
if err := os.MkdirAll(parent, 0o755); err != nil {
return fmt.Errorf("create managed app runtime parent: %w", err)
}
if err := os.MkdirAll(root, 0o755); err != nil {
return fmt.Errorf("create managed app runtime root: %w", err)
}
for _, name := range componentNames {
source := filepath.Join(stagingDir, name)
target := filepath.Join(root, name)
if err := os.RemoveAll(target); err != nil {
return fmt.Errorf("replace managed app runtime component %q: %w", name, err)
}
if err := os.Rename(source, target); err != nil {
return fmt.Errorf("install managed app runtime component %q: %w", name, err)
}
}
return nil
}
type managedAppRuntimeComponentDownload struct {
name string
archivePath string
}
func (r DefaultResolver) downloadRuntimeComponents(
ctx context.Context,
downloadsDir string,
components map[string]appRuntimeCatalogComponent,
componentNames []string,
) ([]managedAppRuntimeComponentDownload, error) {
type result struct {
download managedAppRuntimeComponentDownload
err error
}
downloadCtx, cancel := context.WithCancel(ctx)
defer cancel()
results := make(chan result, len(componentNames))
var waitGroup sync.WaitGroup
for _, componentName := range componentNames {
name := componentName
component := components[name]
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
componentReport := componentDownloadProgressFromContext(downloadCtx)
componentCtx := downloadCtx
if componentReport != nil {
componentCtx = ContextWithDownloadProgress(downloadCtx, func(progress DownloadProgress) {
componentReport(name, progress)
})
}
download, err := r.downloadRuntimeComponent(componentCtx, downloadsDir, name, component)
if err != nil {
cancel()
}
results <- result{download: download, err: err}
}()
}
waitGroup.Wait()
close(results)
downloadsByName := make(map[string]managedAppRuntimeComponentDownload, len(componentNames))
var resultErr error
for item := range results {
if item.err != nil {
resultErr = errors.Join(resultErr, item.err)
continue
}
downloadsByName[item.download.name] = item.download
}
if resultErr != nil {
for _, download := range downloadsByName {
_ = os.Remove(download.archivePath)
}
return nil, resultErr
}
downloads := make([]managedAppRuntimeComponentDownload, 0, len(componentNames))
for _, name := range componentNames {
download, ok := downloadsByName[name]
if !ok {
return nil, fmt.Errorf("managed app runtime component %q was not downloaded", name)
}
downloads = append(downloads, download)
}
return downloads, nil
}
func (r DefaultResolver) downloadRuntimeComponent(
ctx context.Context,
downloadsDir string,
componentName string,
component appRuntimeCatalogComponent,
) (managedAppRuntimeComponentDownload, error) {
archiveFile, err := os.CreateTemp(downloadsDir, "runtime-"+safeAppRuntimeComponentName(componentName)+"-*.zip")
if err != nil {
return managedAppRuntimeComponentDownload{}, fmt.Errorf("create managed app runtime component download file: %w", err)
}
archivePath := archiveFile.Name()
if err := archiveFile.Close(); err != nil {
_ = os.Remove(archivePath)
return managedAppRuntimeComponentDownload{}, fmt.Errorf("close managed app runtime component download file: %w", err)
}
if err := r.fetchArtifact(ctx, strings.TrimSpace(component.ArtifactURL), archivePath); err != nil {
_ = os.Remove(archivePath)
return managedAppRuntimeComponentDownload{}, err
}
downloadedSHA256, _, err := fileSHA256AndSize(archivePath)
if err != nil {
_ = os.Remove(archivePath)
return managedAppRuntimeComponentDownload{}, err
}
if !strings.EqualFold(downloadedSHA256, strings.TrimSpace(component.ArtifactSHA256)) {
_ = os.Remove(archivePath)
return managedAppRuntimeComponentDownload{}, fmt.Errorf("managed app runtime component %q sha256 mismatch", componentName)
}
return managedAppRuntimeComponentDownload{name: componentName, archivePath: archivePath}, nil
}
func (r DefaultResolver) fetchArtifact(ctx context.Context, artifactURL string, destinationPath string) error {
if strings.HasPrefix(artifactURL, "http://") || strings.HasPrefix(artifactURL, "https://") {
return downloadArtifact(ctx, r.httpClient(), artifactURL, destinationPath)
}
source, err := os.Open(artifactURL)
if err != nil {
return fmt.Errorf("open managed app runtime artifact: %w", err)
}
defer func() {
_ = source.Close()
}()
if err := os.MkdirAll(filepath.Dir(destinationPath), 0o755); err != nil {
return fmt.Errorf("create managed app runtime artifact destination parent: %w", err)
}
target, err := os.OpenFile(destinationPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return fmt.Errorf("create managed app runtime artifact destination: %w", err)
}
_, copyErr := io.Copy(target, io.LimitReader(source, maxManagedAppRuntimeArtifactBytes+1))
info, statErr := target.Stat()
var sizeErr error
if statErr == nil && info != nil && info.Size() > maxManagedAppRuntimeArtifactBytes {
sizeErr = fmt.Errorf("managed app runtime artifact exceeds maximum size %d", maxManagedAppRuntimeArtifactBytes)
}
return errors.Join(
copyErr,
statErr,
target.Close(),
sizeErr,
)
}
func (r DefaultResolver) httpClient() *http.Client {
if r.HTTPClient != nil {
return r.HTTPClient
}
return httpx.Default()
}
func ContextWithDownloadProgress(ctx context.Context, report func(DownloadProgress)) context.Context {
if report == nil {
return ctx
}
return context.WithValue(ctx, downloadProgressKey{}, report)
}
func ContextWithComponentDownloadProgress(ctx context.Context, report func(string, DownloadProgress)) context.Context {
if report == nil {
return ctx
}
return context.WithValue(ctx, componentDownloadProgressKey{}, report)
}
func componentDownloadProgressFromContext(ctx context.Context) func(string, DownloadProgress) {
if ctx == nil {
return nil
}
report, _ := ctx.Value(componentDownloadProgressKey{}).(func(string, DownloadProgress))
return report
}
func downloadProgressFromContext(ctx context.Context) func(DownloadProgress) {
if ctx == nil {
return nil
}
report, _ := ctx.Value(downloadProgressKey{}).(func(DownloadProgress))
return report
}
func DefaultRoot(env []string) string {
cacheRoot := strings.TrimSpace(envValue(env, tuttiAppRuntimeCacheRootEnv))
if cacheRoot == "" {
cacheRoot = filepath.Join(tuttitypes.DefaultStateDir(), "app-runtimes")
}
return filepath.Join(cacheRoot, appRuntimePlatformArch(runtime.GOOS, runtime.GOARCH))
}
func (r DefaultResolver) DefaultRoot() string {
return DefaultRoot(r.environ())
}
func appRuntimePlatformArch(platform string, arch string) string {
return platform + "-" + arch
}
func ProcessEnv(overrides ...string) []string {
env := os.Environ()
for _, override := range overrides {
key, _, ok := strings.Cut(override, "=")
if !ok {
env = append(env, override)
continue
}
next := make([]string, 0, len(env)+1)
for _, item := range env {
itemKey, _, ok := strings.Cut(item, "=")
if ok && strings.EqualFold(itemKey, key) {
continue
}
next = append(next, item)
}
env = append(next, override)
}
// Inject the macOS system proxy so processes spawned with this env (agent
// installs, workspace apps) reach the upstream API through the same proxy as
// Resolver.Env()-spawned agents. Without it, an install from a restricted
// region connects directly and gets `403 Request not allowed`.
return runtimecmd.InjectSystemProxyEnv(env)
}
func EnvValue(env []string, key string) string {
for i := len(env) - 1; i >= 0; i-- {
candidateKey, value, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(candidateKey, key) {
return value
}
}
return ""
}
func (r DefaultResolver) environ() []string {
if r.Environ != nil {
return r.Environ()
}
return os.Environ()
}
func pythonBinaryName() string {
if runtime.GOOS == "windows" {
return "python.exe"
}
return "python3"
}
func nodeBinaryName() string {
if runtime.GOOS == "windows" {
return "node.exe"
}
return "node"
}
func npmBinaryName() string {
if runtime.GOOS == "windows" {
return "npm.cmd"
}
return "npm"
}
func corepackBinaryName() string {
if runtime.GOOS == "windows" {
return "corepack.cmd"
}
return "corepack"
}
func rtkBinaryName() string {
if runtime.GOOS == "windows" {
return "rtk.exe"
}
return "rtk"
}
func isStandaloneCorepackWrapper(path string) bool {
if !isExecutableFile(path) {
return false
}
content, err := os.ReadFile(path)
if err != nil {
return false
}
normalized := strings.ReplaceAll(string(content), `\`, "/")
if strings.Contains(normalized, "lib/node_modules/corepack/dist/corepack.js") {
return true
}
return runtime.GOOS == "windows" && strings.Contains(
normalized,
"node_modules/corepack/dist/corepack.js",
)
}
func isExecutableFile(path string) bool {
info, err := os.Stat(path)
if err != nil || info.IsDir() {
return false
}
if runtime.GOOS == "windows" {
return true
}
return info.Mode().Perm()&0o111 != 0
}
func pathEnvKey(env []string) string {
for i := len(env) - 1; i >= 0; i-- {
key, _, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(key, "PATH") {
return key
}
}
return "PATH"
}