mirror of
https://github.com/NVIDIA/nvidia-container-toolkit
synced 2025-06-26 18:18:24 +00:00
Refactor toolkit installer
Signed-off-by: Evan Lezar <elezar@nvidia.com>
This commit is contained in:
85
cmd/nvidia-ctk-installer/toolkit/installer/artifact-root.go
Normal file
85
cmd/nvidia-ctk-installer/toolkit/installer/artifact-root.go
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
|
||||
)
|
||||
|
||||
// An artifactRoot is used as a source for installed artifacts.
|
||||
// It is refined by a directory path, a library locator, and an executable locator.
|
||||
type artifactRoot struct {
|
||||
path string
|
||||
libraries lookup.Locator
|
||||
executables lookup.Locator
|
||||
}
|
||||
|
||||
func newArtifactRoot(logger logger.Interface, rootDirectoryPath string) (*artifactRoot, error) {
|
||||
relativeLibrarySearchPaths := []string{
|
||||
"/usr/lib64",
|
||||
"/usr/lib/x86_64-linux-gnu",
|
||||
"/usr/lib/aarch64-linux-gnu",
|
||||
}
|
||||
var librarySearchPaths []string
|
||||
for _, l := range relativeLibrarySearchPaths {
|
||||
librarySearchPaths = append(librarySearchPaths, filepath.Join(rootDirectoryPath, l))
|
||||
}
|
||||
|
||||
a := artifactRoot{
|
||||
path: rootDirectoryPath,
|
||||
libraries: lookup.NewLibraryLocator(
|
||||
lookup.WithLogger(logger),
|
||||
lookup.WithCount(1),
|
||||
lookup.WithSearchPaths(librarySearchPaths...),
|
||||
),
|
||||
executables: lookup.NewExecutableLocator(
|
||||
logger,
|
||||
rootDirectoryPath,
|
||||
),
|
||||
}
|
||||
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func (r *artifactRoot) findLibrary(name string) (string, error) {
|
||||
candidates, err := r.libraries.Locate(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error locating library: %w", err)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "", fmt.Errorf("library %v not found", name)
|
||||
}
|
||||
|
||||
return candidates[0], nil
|
||||
}
|
||||
|
||||
func (r *artifactRoot) findExecutable(name string) (string, error) {
|
||||
candidates, err := r.executables.Locate(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error locating executable: %w", err)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "", fmt.Errorf("executable %v not found", name)
|
||||
}
|
||||
|
||||
return candidates[0], nil
|
||||
}
|
||||
47
cmd/nvidia-ctk-installer/toolkit/installer/directory.go
Normal file
47
cmd/nvidia-ctk-installer/toolkit/installer/directory.go
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
)
|
||||
|
||||
type createDirectory struct {
|
||||
logger logger.Interface
|
||||
}
|
||||
|
||||
func (t *toolkitInstaller) createDirectory() Installer {
|
||||
return &createDirectory{
|
||||
logger: t.logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *createDirectory) Install(dir string) error {
|
||||
if dir == "" {
|
||||
return nil
|
||||
}
|
||||
d.logger.Infof("Creating directory '%v'", dir)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating directory: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
184
cmd/nvidia-ctk-installer/toolkit/installer/executables.go
Normal file
184
cmd/nvidia-ctk-installer/toolkit/installer/executables.go
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk-installer/container/operator"
|
||||
)
|
||||
|
||||
type executable struct {
|
||||
requiresKernelModule bool
|
||||
path string
|
||||
symlink string
|
||||
args []string
|
||||
env map[string]string
|
||||
}
|
||||
|
||||
func (t *toolkitInstaller) collectExecutables(destDir string) ([]Installer, error) {
|
||||
configHome := filepath.Join(destDir, ".config")
|
||||
configDir := filepath.Join(configHome, "nvidia-container-runtime")
|
||||
configPath := filepath.Join(configDir, "config.toml")
|
||||
|
||||
executables := []executable{
|
||||
{
|
||||
path: "nvidia-ctk",
|
||||
},
|
||||
{
|
||||
path: "nvidia-cdi-hook",
|
||||
},
|
||||
}
|
||||
for _, runtime := range operator.GetRuntimes() {
|
||||
e := executable{
|
||||
path: runtime.Path,
|
||||
requiresKernelModule: true,
|
||||
env: map[string]string{
|
||||
"XDG_CONFIG_HOME": configHome,
|
||||
},
|
||||
}
|
||||
executables = append(executables, e)
|
||||
}
|
||||
executables = append(executables,
|
||||
executable{
|
||||
path: "nvidia-container-cli",
|
||||
env: map[string]string{"LD_LIBRARY_PATH": destDir + ":$LD_LIBRARY_PATH"},
|
||||
},
|
||||
)
|
||||
|
||||
executables = append(executables,
|
||||
executable{
|
||||
path: "nvidia-container-runtime-hook",
|
||||
symlink: "nvidia-container-toolkit",
|
||||
args: []string{fmt.Sprintf("-config %s", configPath)},
|
||||
},
|
||||
)
|
||||
|
||||
var installers []Installer
|
||||
for _, executable := range executables {
|
||||
executablePath, err := t.artifactRoot.findExecutable(executable.path)
|
||||
if err != nil {
|
||||
if t.ignoreErrors {
|
||||
log.Errorf("Ignoring error: %v", err)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrappedExecutableFilename := filepath.Base(executablePath)
|
||||
dotRealFilename := wrappedExecutableFilename + ".real"
|
||||
|
||||
w := &wrapper{
|
||||
Source: executablePath,
|
||||
WrappedExecutable: dotRealFilename,
|
||||
CheckModules: executable.requiresKernelModule,
|
||||
Args: executable.args,
|
||||
Envvars: map[string]string{
|
||||
"PATH": strings.Join([]string{destDir, "$PATH"}, ":"),
|
||||
},
|
||||
}
|
||||
for k, v := range executable.env {
|
||||
w.Envvars[k] = v
|
||||
}
|
||||
|
||||
installers = append(installers, w)
|
||||
|
||||
if executable.symlink == "" {
|
||||
continue
|
||||
}
|
||||
link := symlink{
|
||||
linkname: executable.symlink,
|
||||
target: filepath.Base(executablePath),
|
||||
}
|
||||
installers = append(installers, link)
|
||||
}
|
||||
|
||||
return installers, nil
|
||||
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
Source string
|
||||
Envvars map[string]string
|
||||
WrappedExecutable string
|
||||
CheckModules bool
|
||||
Args []string
|
||||
}
|
||||
|
||||
type render struct {
|
||||
*wrapper
|
||||
DestDir string
|
||||
}
|
||||
|
||||
func (w *wrapper) Install(destDir string) error {
|
||||
// Copy the executable with a .real extension.
|
||||
mode, err := installFile(w.Source, filepath.Join(destDir, w.WrappedExecutable))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a wrapper file.
|
||||
r := render{
|
||||
wrapper: w,
|
||||
DestDir: destDir,
|
||||
}
|
||||
content, err := r.render()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to render wrapper: %w", err)
|
||||
}
|
||||
wrapperFile := filepath.Join(destDir, filepath.Base(w.Source))
|
||||
return installContent(content, wrapperFile, mode|0111)
|
||||
}
|
||||
|
||||
func (w *render) render() (io.Reader, error) {
|
||||
wrapperTemplate := `#! /bin/sh
|
||||
{{- if (.CheckModules) }}
|
||||
cat /proc/modules | grep -e "^nvidia " >/dev/null 2>&1
|
||||
if [ "${?}" != "0" ]; then
|
||||
echo "nvidia driver modules are not yet loaded, invoking runc directly"
|
||||
exec runc "$@"
|
||||
fi
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Envvars }}
|
||||
{{$key}}={{$value}} \
|
||||
{{- end }}
|
||||
{{ .DestDir }}/{{ .WrappedExecutable }} \
|
||||
{{- range $arg := .Args }}
|
||||
{{$arg}} \
|
||||
{{- end }}
|
||||
"$@"
|
||||
`
|
||||
|
||||
var content bytes.Buffer
|
||||
tmpl, err := template.New("wrapper").Parse(wrapperTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tmpl.Execute(&content, w); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &content, nil
|
||||
}
|
||||
104
cmd/nvidia-ctk-installer/toolkit/installer/executables_test.go
Normal file
104
cmd/nvidia-ctk-installer/toolkit/installer/executables_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWrapperRender(t *testing.T) {
|
||||
testCases := []struct {
|
||||
description string
|
||||
w *wrapper
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
description: "executable is added",
|
||||
w: &wrapper{
|
||||
WrappedExecutable: "some-runtime",
|
||||
},
|
||||
expected: `#! /bin/sh
|
||||
/dest-dir/some-runtime \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "module check is added",
|
||||
w: &wrapper{
|
||||
WrappedExecutable: "some-runtime",
|
||||
CheckModules: true,
|
||||
},
|
||||
expected: `#! /bin/sh
|
||||
cat /proc/modules | grep -e "^nvidia " >/dev/null 2>&1
|
||||
if [ "${?}" != "0" ]; then
|
||||
echo "nvidia driver modules are not yet loaded, invoking runc directly"
|
||||
exec runc "$@"
|
||||
fi
|
||||
/dest-dir/some-runtime \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "environment is added",
|
||||
w: &wrapper{
|
||||
WrappedExecutable: "some-runtime",
|
||||
Envvars: map[string]string{
|
||||
"PATH": "/foo/bar/baz",
|
||||
},
|
||||
},
|
||||
expected: `#! /bin/sh
|
||||
PATH=/foo/bar/baz \
|
||||
/dest-dir/some-runtime \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
description: "args are added",
|
||||
w: &wrapper{
|
||||
WrappedExecutable: "some-runtime",
|
||||
Args: []string{"--config foo", "bar"},
|
||||
},
|
||||
expected: `#! /bin/sh
|
||||
/dest-dir/some-runtime \
|
||||
--config foo \
|
||||
bar \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
r := render{
|
||||
wrapper: tc.w,
|
||||
DestDir: "/dest-dir",
|
||||
}
|
||||
reader, err := r.render()
|
||||
require.NoError(t, err)
|
||||
|
||||
var content bytes.Buffer
|
||||
_, err = content.ReadFrom(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tc.expected, content.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Ensure, that fileInstallerMock does implement fileInstaller.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ fileInstaller = &fileInstallerMock{}
|
||||
|
||||
// fileInstallerMock is a mock implementation of fileInstaller.
|
||||
//
|
||||
// func TestSomethingThatUsesfileInstaller(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked fileInstaller
|
||||
// mockedfileInstaller := &fileInstallerMock{
|
||||
// installContentFunc: func(reader io.Reader, s string, v os.FileMode) error {
|
||||
// panic("mock out the installContent method")
|
||||
// },
|
||||
// installFileFunc: func(s1 string, s2 string) (os.FileMode, error) {
|
||||
// panic("mock out the installFile method")
|
||||
// },
|
||||
// installSymlinkFunc: func(s1 string, s2 string) error {
|
||||
// panic("mock out the installSymlink method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedfileInstaller in code that requires fileInstaller
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type fileInstallerMock struct {
|
||||
// installContentFunc mocks the installContent method.
|
||||
installContentFunc func(reader io.Reader, s string, v os.FileMode) error
|
||||
|
||||
// installFileFunc mocks the installFile method.
|
||||
installFileFunc func(s1 string, s2 string) (os.FileMode, error)
|
||||
|
||||
// installSymlinkFunc mocks the installSymlink method.
|
||||
installSymlinkFunc func(s1 string, s2 string) error
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// installContent holds details about calls to the installContent method.
|
||||
installContent []struct {
|
||||
// Reader is the reader argument value.
|
||||
Reader io.Reader
|
||||
// S is the s argument value.
|
||||
S string
|
||||
// V is the v argument value.
|
||||
V os.FileMode
|
||||
}
|
||||
// installFile holds details about calls to the installFile method.
|
||||
installFile []struct {
|
||||
// S1 is the s1 argument value.
|
||||
S1 string
|
||||
// S2 is the s2 argument value.
|
||||
S2 string
|
||||
}
|
||||
// installSymlink holds details about calls to the installSymlink method.
|
||||
installSymlink []struct {
|
||||
// S1 is the s1 argument value.
|
||||
S1 string
|
||||
// S2 is the s2 argument value.
|
||||
S2 string
|
||||
}
|
||||
}
|
||||
lockinstallContent sync.RWMutex
|
||||
lockinstallFile sync.RWMutex
|
||||
lockinstallSymlink sync.RWMutex
|
||||
}
|
||||
|
||||
// installContent calls installContentFunc.
|
||||
func (mock *fileInstallerMock) installContent(reader io.Reader, s string, v os.FileMode) error {
|
||||
if mock.installContentFunc == nil {
|
||||
panic("fileInstallerMock.installContentFunc: method is nil but fileInstaller.installContent was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Reader io.Reader
|
||||
S string
|
||||
V os.FileMode
|
||||
}{
|
||||
Reader: reader,
|
||||
S: s,
|
||||
V: v,
|
||||
}
|
||||
mock.lockinstallContent.Lock()
|
||||
mock.calls.installContent = append(mock.calls.installContent, callInfo)
|
||||
mock.lockinstallContent.Unlock()
|
||||
return mock.installContentFunc(reader, s, v)
|
||||
}
|
||||
|
||||
// installContentCalls gets all the calls that were made to installContent.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedfileInstaller.installContentCalls())
|
||||
func (mock *fileInstallerMock) installContentCalls() []struct {
|
||||
Reader io.Reader
|
||||
S string
|
||||
V os.FileMode
|
||||
} {
|
||||
var calls []struct {
|
||||
Reader io.Reader
|
||||
S string
|
||||
V os.FileMode
|
||||
}
|
||||
mock.lockinstallContent.RLock()
|
||||
calls = mock.calls.installContent
|
||||
mock.lockinstallContent.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// installFile calls installFileFunc.
|
||||
func (mock *fileInstallerMock) installFile(s1 string, s2 string) (os.FileMode, error) {
|
||||
if mock.installFileFunc == nil {
|
||||
panic("fileInstallerMock.installFileFunc: method is nil but fileInstaller.installFile was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S1 string
|
||||
S2 string
|
||||
}{
|
||||
S1: s1,
|
||||
S2: s2,
|
||||
}
|
||||
mock.lockinstallFile.Lock()
|
||||
mock.calls.installFile = append(mock.calls.installFile, callInfo)
|
||||
mock.lockinstallFile.Unlock()
|
||||
return mock.installFileFunc(s1, s2)
|
||||
}
|
||||
|
||||
// installFileCalls gets all the calls that were made to installFile.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedfileInstaller.installFileCalls())
|
||||
func (mock *fileInstallerMock) installFileCalls() []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
} {
|
||||
var calls []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
}
|
||||
mock.lockinstallFile.RLock()
|
||||
calls = mock.calls.installFile
|
||||
mock.lockinstallFile.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// installSymlink calls installSymlinkFunc.
|
||||
func (mock *fileInstallerMock) installSymlink(s1 string, s2 string) error {
|
||||
if mock.installSymlinkFunc == nil {
|
||||
panic("fileInstallerMock.installSymlinkFunc: method is nil but fileInstaller.installSymlink was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S1 string
|
||||
S2 string
|
||||
}{
|
||||
S1: s1,
|
||||
S2: s2,
|
||||
}
|
||||
mock.lockinstallSymlink.Lock()
|
||||
mock.calls.installSymlink = append(mock.calls.installSymlink, callInfo)
|
||||
mock.lockinstallSymlink.Unlock()
|
||||
return mock.installSymlinkFunc(s1, s2)
|
||||
}
|
||||
|
||||
// installSymlinkCalls gets all the calls that were made to installSymlink.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedfileInstaller.installSymlinkCalls())
|
||||
func (mock *fileInstallerMock) installSymlinkCalls() []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
} {
|
||||
var calls []struct {
|
||||
S1 string
|
||||
S2 string
|
||||
}
|
||||
mock.lockinstallSymlink.RLock()
|
||||
calls = mock.calls.installSymlink
|
||||
mock.lockinstallSymlink.RUnlock()
|
||||
return calls
|
||||
}
|
||||
168
cmd/nvidia-ctk-installer/toolkit/installer/installer.go
Normal file
168
cmd/nvidia-ctk-installer/toolkit/installer/installer.go
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
)
|
||||
|
||||
//go:generate moq -rm -fmt=goimports -out installer_mock.go . Installer
|
||||
type Installer interface {
|
||||
Install(string) error
|
||||
}
|
||||
|
||||
type toolkitInstaller struct {
|
||||
logger logger.Interface
|
||||
ignoreErrors bool
|
||||
sourceRoot string
|
||||
|
||||
artifactRoot *artifactRoot
|
||||
|
||||
ensureTargetDirectory Installer
|
||||
}
|
||||
|
||||
var _ Installer = (*toolkitInstaller)(nil)
|
||||
|
||||
// New creates a toolkit installer with the specified options.
|
||||
func New(opts ...Option) (Installer, error) {
|
||||
t := &toolkitInstaller{}
|
||||
for _, opt := range opts {
|
||||
opt(t)
|
||||
}
|
||||
|
||||
if t.logger == nil {
|
||||
t.logger = logger.New()
|
||||
}
|
||||
if t.sourceRoot == "" {
|
||||
t.sourceRoot = "/"
|
||||
}
|
||||
if t.artifactRoot == nil {
|
||||
artifactRoot, err := newArtifactRoot(t.logger, t.sourceRoot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.artifactRoot = artifactRoot
|
||||
}
|
||||
|
||||
if t.ensureTargetDirectory == nil {
|
||||
t.ensureTargetDirectory = t.createDirectory()
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Install ensures that the required toolkit files are installed in the specified directory.
|
||||
func (t *toolkitInstaller) Install(destDir string) error {
|
||||
var installers []Installer
|
||||
|
||||
installers = append(installers, t.ensureTargetDirectory)
|
||||
|
||||
libraries, err := t.collectLibraries()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect libraries: %w", err)
|
||||
}
|
||||
installers = append(installers, libraries...)
|
||||
|
||||
executables, err := t.collectExecutables(destDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to collect executables: %w", err)
|
||||
}
|
||||
installers = append(installers, executables...)
|
||||
|
||||
var errs error
|
||||
for _, i := range installers {
|
||||
errs = errors.Join(errs, i.Install(destDir))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
type symlink struct {
|
||||
linkname string
|
||||
target string
|
||||
}
|
||||
|
||||
func (s symlink) Install(destDir string) error {
|
||||
symlinkPath := filepath.Join(destDir, s.linkname)
|
||||
return installSymlink(s.target, symlinkPath)
|
||||
}
|
||||
|
||||
//go:generate moq -rm -fmt=goimports -out file-installer_mock.go . fileInstaller
|
||||
type fileInstaller interface {
|
||||
installContent(io.Reader, string, os.FileMode) error
|
||||
installFile(string, string) (os.FileMode, error)
|
||||
installSymlink(string, string) error
|
||||
}
|
||||
|
||||
var installSymlink = installSymlinkStub
|
||||
|
||||
func installSymlinkStub(target string, link string) error {
|
||||
err := os.Symlink(target, link)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating symlink '%v' => '%v': %v", link, target, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var installFile = installFileStub
|
||||
|
||||
func installFileStub(src string, dest string) (os.FileMode, error) {
|
||||
sourceInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error getting file info for '%v': %v", src, err)
|
||||
}
|
||||
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error opening source: %w", err)
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
mode := sourceInfo.Mode()
|
||||
if err := installContent(source, dest, mode); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return mode, nil
|
||||
}
|
||||
|
||||
var installContent = installContentStub
|
||||
|
||||
func installContentStub(content io.Reader, dest string, mode fs.FileMode) error {
|
||||
destination, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating destination: %w", err)
|
||||
}
|
||||
defer destination.Close()
|
||||
|
||||
_, err = io.Copy(destination, content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying file: %w", err)
|
||||
}
|
||||
err = os.Chmod(dest, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting mode for '%v': %v", dest, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
74
cmd/nvidia-ctk-installer/toolkit/installer/installer_mock.go
Normal file
74
cmd/nvidia-ctk-installer/toolkit/installer/installer_mock.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// Code generated by moq; DO NOT EDIT.
|
||||
// github.com/matryer/moq
|
||||
|
||||
package installer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Ensure, that InstallerMock does implement Installer.
|
||||
// If this is not the case, regenerate this file with moq.
|
||||
var _ Installer = &InstallerMock{}
|
||||
|
||||
// InstallerMock is a mock implementation of Installer.
|
||||
//
|
||||
// func TestSomethingThatUsesInstaller(t *testing.T) {
|
||||
//
|
||||
// // make and configure a mocked Installer
|
||||
// mockedInstaller := &InstallerMock{
|
||||
// InstallFunc: func(s string) error {
|
||||
// panic("mock out the Install method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedInstaller in code that requires Installer
|
||||
// // and then make assertions.
|
||||
//
|
||||
// }
|
||||
type InstallerMock struct {
|
||||
// InstallFunc mocks the Install method.
|
||||
InstallFunc func(s string) error
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// Install holds details about calls to the Install method.
|
||||
Install []struct {
|
||||
// S is the s argument value.
|
||||
S string
|
||||
}
|
||||
}
|
||||
lockInstall sync.RWMutex
|
||||
}
|
||||
|
||||
// Install calls InstallFunc.
|
||||
func (mock *InstallerMock) Install(s string) error {
|
||||
if mock.InstallFunc == nil {
|
||||
panic("InstallerMock.InstallFunc: method is nil but Installer.Install was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
S string
|
||||
}{
|
||||
S: s,
|
||||
}
|
||||
mock.lockInstall.Lock()
|
||||
mock.calls.Install = append(mock.calls.Install, callInfo)
|
||||
mock.lockInstall.Unlock()
|
||||
return mock.InstallFunc(s)
|
||||
}
|
||||
|
||||
// InstallCalls gets all the calls that were made to Install.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedInstaller.InstallCalls())
|
||||
func (mock *InstallerMock) InstallCalls() []struct {
|
||||
S string
|
||||
} {
|
||||
var calls []struct {
|
||||
S string
|
||||
}
|
||||
mock.lockInstall.RLock()
|
||||
calls = mock.calls.Install
|
||||
mock.lockInstall.RUnlock()
|
||||
return calls
|
||||
}
|
||||
251
cmd/nvidia-ctk-installer/toolkit/installer/installer_test.go
Normal file
251
cmd/nvidia-ctk-installer/toolkit/installer/installer_test.go
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
testlog "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
|
||||
)
|
||||
|
||||
func TestToolkitInstaller(t *testing.T) {
|
||||
logger, _ := testlog.NewNullLogger()
|
||||
|
||||
type contentCall struct {
|
||||
wrapper string
|
||||
path string
|
||||
mode fs.FileMode
|
||||
}
|
||||
var contentCalls []contentCall
|
||||
|
||||
installer := &fileInstallerMock{
|
||||
installFileFunc: func(s1, s2 string) (os.FileMode, error) {
|
||||
return 0666, nil
|
||||
},
|
||||
installContentFunc: func(reader io.Reader, s string, fileMode fs.FileMode) error {
|
||||
var b bytes.Buffer
|
||||
if _, err := b.ReadFrom(reader); err != nil {
|
||||
return err
|
||||
}
|
||||
contents := contentCall{
|
||||
wrapper: b.String(),
|
||||
path: s,
|
||||
mode: fileMode,
|
||||
}
|
||||
|
||||
contentCalls = append(contentCalls, contents)
|
||||
return nil
|
||||
},
|
||||
installSymlinkFunc: func(s1, s2 string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
installFile = installer.installFile
|
||||
installContent = installer.installContent
|
||||
installSymlink = installer.installSymlink
|
||||
|
||||
root := "/artifacts/test"
|
||||
libraries := &lookup.LocatorMock{
|
||||
LocateFunc: func(s string) ([]string, error) {
|
||||
switch s {
|
||||
case "libnvidia-container.so.1":
|
||||
return []string{filepath.Join(root, "libnvidia-container.so.987.65.43")}, nil
|
||||
case "libnvidia-container-go.so.1":
|
||||
return []string{filepath.Join(root, "libnvidia-container-go.so.1.23.4")}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%v not found", s)
|
||||
},
|
||||
}
|
||||
executables := &lookup.LocatorMock{
|
||||
LocateFunc: func(s string) ([]string, error) {
|
||||
switch s {
|
||||
case "nvidia-container-runtime.cdi":
|
||||
fallthrough
|
||||
case "nvidia-container-runtime.legacy":
|
||||
fallthrough
|
||||
case "nvidia-container-runtime":
|
||||
fallthrough
|
||||
case "nvidia-ctk":
|
||||
fallthrough
|
||||
case "nvidia-container-cli":
|
||||
fallthrough
|
||||
case "nvidia-container-runtime-hook":
|
||||
fallthrough
|
||||
case "nvidia-cdi-hook":
|
||||
return []string{filepath.Join(root, "usr/bin", s)}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%v not found", s)
|
||||
},
|
||||
}
|
||||
|
||||
r := &artifactRoot{
|
||||
libraries: libraries,
|
||||
executables: executables,
|
||||
}
|
||||
|
||||
createDirectory := &InstallerMock{
|
||||
InstallFunc: func(c string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
i := toolkitInstaller{
|
||||
logger: logger,
|
||||
artifactRoot: r,
|
||||
ensureTargetDirectory: createDirectory,
|
||||
}
|
||||
|
||||
err := i.Install("/foo/bar/baz")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.ElementsMatch(t,
|
||||
[]struct {
|
||||
S string
|
||||
}{
|
||||
{"/foo/bar/baz"},
|
||||
},
|
||||
createDirectory.InstallCalls(),
|
||||
)
|
||||
|
||||
require.ElementsMatch(t,
|
||||
installer.installFileCalls(),
|
||||
[]struct {
|
||||
S1 string
|
||||
S2 string
|
||||
}{
|
||||
{"/artifacts/test/libnvidia-container-go.so.1.23.4", "/foo/bar/baz/libnvidia-container-go.so.1.23.4"},
|
||||
{"/artifacts/test/libnvidia-container.so.987.65.43", "/foo/bar/baz/libnvidia-container.so.987.65.43"},
|
||||
{"/artifacts/test/usr/bin/nvidia-container-runtime.cdi", "/foo/bar/baz/nvidia-container-runtime.cdi.real"},
|
||||
{"/artifacts/test/usr/bin/nvidia-container-runtime.legacy", "/foo/bar/baz/nvidia-container-runtime.legacy.real"},
|
||||
{"/artifacts/test/usr/bin/nvidia-container-runtime", "/foo/bar/baz/nvidia-container-runtime.real"},
|
||||
{"/artifacts/test/usr/bin/nvidia-ctk", "/foo/bar/baz/nvidia-ctk.real"},
|
||||
{"/artifacts/test/usr/bin/nvidia-cdi-hook", "/foo/bar/baz/nvidia-cdi-hook.real"},
|
||||
{"/artifacts/test/usr/bin/nvidia-container-cli", "/foo/bar/baz/nvidia-container-cli.real"},
|
||||
{"/artifacts/test/usr/bin/nvidia-container-runtime-hook", "/foo/bar/baz/nvidia-container-runtime-hook.real"},
|
||||
},
|
||||
)
|
||||
|
||||
require.ElementsMatch(t,
|
||||
installer.installSymlinkCalls(),
|
||||
[]struct {
|
||||
S1 string
|
||||
S2 string
|
||||
}{
|
||||
{"libnvidia-container-go.so.1.23.4", "/foo/bar/baz/libnvidia-container-go.so.1"},
|
||||
{"libnvidia-container.so.987.65.43", "/foo/bar/baz/libnvidia-container.so.1"},
|
||||
{"nvidia-container-runtime-hook", "/foo/bar/baz/nvidia-container-toolkit"},
|
||||
},
|
||||
)
|
||||
|
||||
require.ElementsMatch(t,
|
||||
contentCalls,
|
||||
[]contentCall{
|
||||
{
|
||||
path: "/foo/bar/baz/nvidia-container-runtime",
|
||||
mode: 0777,
|
||||
wrapper: `#! /bin/sh
|
||||
cat /proc/modules | grep -e "^nvidia " >/dev/null 2>&1
|
||||
if [ "${?}" != "0" ]; then
|
||||
echo "nvidia driver modules are not yet loaded, invoking runc directly"
|
||||
exec runc "$@"
|
||||
fi
|
||||
PATH=/foo/bar/baz:$PATH \
|
||||
XDG_CONFIG_HOME=/foo/bar/baz/.config \
|
||||
/foo/bar/baz/nvidia-container-runtime.real \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: "/foo/bar/baz/nvidia-container-runtime.cdi",
|
||||
mode: 0777,
|
||||
wrapper: `#! /bin/sh
|
||||
cat /proc/modules | grep -e "^nvidia " >/dev/null 2>&1
|
||||
if [ "${?}" != "0" ]; then
|
||||
echo "nvidia driver modules are not yet loaded, invoking runc directly"
|
||||
exec runc "$@"
|
||||
fi
|
||||
PATH=/foo/bar/baz:$PATH \
|
||||
XDG_CONFIG_HOME=/foo/bar/baz/.config \
|
||||
/foo/bar/baz/nvidia-container-runtime.cdi.real \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: "/foo/bar/baz/nvidia-container-runtime.legacy",
|
||||
mode: 0777,
|
||||
wrapper: `#! /bin/sh
|
||||
cat /proc/modules | grep -e "^nvidia " >/dev/null 2>&1
|
||||
if [ "${?}" != "0" ]; then
|
||||
echo "nvidia driver modules are not yet loaded, invoking runc directly"
|
||||
exec runc "$@"
|
||||
fi
|
||||
PATH=/foo/bar/baz:$PATH \
|
||||
XDG_CONFIG_HOME=/foo/bar/baz/.config \
|
||||
/foo/bar/baz/nvidia-container-runtime.legacy.real \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: "/foo/bar/baz/nvidia-ctk",
|
||||
mode: 0777,
|
||||
wrapper: `#! /bin/sh
|
||||
PATH=/foo/bar/baz:$PATH \
|
||||
/foo/bar/baz/nvidia-ctk.real \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: "/foo/bar/baz/nvidia-cdi-hook",
|
||||
mode: 0777,
|
||||
wrapper: `#! /bin/sh
|
||||
PATH=/foo/bar/baz:$PATH \
|
||||
/foo/bar/baz/nvidia-cdi-hook.real \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: "/foo/bar/baz/nvidia-container-cli",
|
||||
mode: 0777,
|
||||
wrapper: `#! /bin/sh
|
||||
LD_LIBRARY_PATH=/foo/bar/baz:$LD_LIBRARY_PATH \
|
||||
PATH=/foo/bar/baz:$PATH \
|
||||
/foo/bar/baz/nvidia-container-cli.real \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
{
|
||||
path: "/foo/bar/baz/nvidia-container-runtime-hook",
|
||||
mode: 0777,
|
||||
wrapper: `#! /bin/sh
|
||||
PATH=/foo/bar/baz:$PATH \
|
||||
/foo/bar/baz/nvidia-container-runtime-hook.real \
|
||||
-config /foo/bar/baz/.config/nvidia-container-runtime/config.toml \
|
||||
"$@"
|
||||
`,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
73
cmd/nvidia-ctk-installer/toolkit/installer/libraries.go
Normal file
73
cmd/nvidia-ctk-installer/toolkit/installer/libraries.go
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// collectLibraries locates and installs the libraries that are part of
|
||||
// the nvidia-container-toolkit.
|
||||
// A predefined set of library candidates are considered, with the first one
|
||||
// resulting in success being installed to the toolkit folder. The install process
|
||||
// resolves the symlink for the library and copies the versioned library itself.
|
||||
func (t *toolkitInstaller) collectLibraries() ([]Installer, error) {
|
||||
requiredLibraries := []string{
|
||||
"libnvidia-container.so.1",
|
||||
"libnvidia-container-go.so.1",
|
||||
}
|
||||
|
||||
var installers []Installer
|
||||
for _, l := range requiredLibraries {
|
||||
libraryPath, err := t.artifactRoot.findLibrary(l)
|
||||
if err != nil {
|
||||
if t.ignoreErrors {
|
||||
log.Errorf("Ignoring error: %v", err)
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
installers = append(installers, library(libraryPath))
|
||||
|
||||
if filepath.Base(libraryPath) == l {
|
||||
continue
|
||||
}
|
||||
|
||||
link := symlink{
|
||||
linkname: l,
|
||||
target: filepath.Base(libraryPath),
|
||||
}
|
||||
installers = append(installers, link)
|
||||
}
|
||||
|
||||
return installers, nil
|
||||
}
|
||||
|
||||
type library string
|
||||
|
||||
// Install copies the library l to the destination folder.
|
||||
// The same basename is used in the destination folder.
|
||||
func (l library) Install(destinationDir string) error {
|
||||
dest := filepath.Join(destinationDir, filepath.Base(string(l)))
|
||||
|
||||
_, err := installFile(string(l), dest)
|
||||
return err
|
||||
}
|
||||
47
cmd/nvidia-ctk-installer/toolkit/installer/options.go
Normal file
47
cmd/nvidia-ctk-installer/toolkit/installer/options.go
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# 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 installer
|
||||
|
||||
import "github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
|
||||
type Option func(*toolkitInstaller)
|
||||
|
||||
func WithLogger(logger logger.Interface) Option {
|
||||
return func(ti *toolkitInstaller) {
|
||||
ti.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func WithArtifactRoot(artifactRoot *artifactRoot) Option {
|
||||
return func(ti *toolkitInstaller) {
|
||||
ti.artifactRoot = artifactRoot
|
||||
}
|
||||
}
|
||||
|
||||
func WithIgnoreErrors(ignoreErrors bool) Option {
|
||||
return func(ti *toolkitInstaller) {
|
||||
ti.ignoreErrors = ignoreErrors
|
||||
}
|
||||
}
|
||||
|
||||
// WithSourceRoot sets the root directory for locating artifacts to be installed.
|
||||
func WithSourceRoot(sourceRoot string) Option {
|
||||
return func(ti *toolkitInstaller) {
|
||||
ti.sourceRoot = sourceRoot
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user