mirror of
https://github.com/NVIDIA/nvidia-container-toolkit
synced 2025-06-26 18:18:24 +00:00
Compare commits
15 Commits
v1.17.0-rc
...
v1.17.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d78868cd31 | ||
|
|
74b1e5ea8c | ||
|
|
88608781b6 | ||
|
|
fa5a4ac499 | ||
|
|
9f1bd62c42 | ||
|
|
9534249936 | ||
|
|
e1ea0056b9 | ||
|
|
c802c3089c | ||
|
|
771ac6b88a | ||
|
|
0f7aba9c3c | ||
|
|
3c07ea0b17 | ||
|
|
183dff9161 | ||
|
|
5e3e91a010 | ||
|
|
dc0e191093 | ||
|
|
a9ca6995f7 |
@@ -1,5 +1,14 @@
|
||||
# NVIDIA Container Toolkit Changelog
|
||||
|
||||
## v1.17.0-rc.2
|
||||
- Fix bug in locating libcuda.so form ldcache
|
||||
- Fix bug in sorting of symlink chain
|
||||
- Remove unsupported print-ldcache command
|
||||
- Remove csv-filename support from create-symlinks
|
||||
|
||||
## Changes in the Toolkit Container
|
||||
- Fallback to `crio-status` if `crio status` does not work when configuring the crio runtime
|
||||
|
||||
## v1.17.0-rc.1
|
||||
- Allow IMEX channels to be requested as volume mounts
|
||||
- Fix typo in error message
|
||||
|
||||
@@ -25,10 +25,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup/symlinks"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/oci"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/platform-support/tegra/csv"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
@@ -37,7 +34,6 @@ type command struct {
|
||||
|
||||
type config struct {
|
||||
hostRoot string
|
||||
filenames cli.StringSlice
|
||||
links cli.StringSlice
|
||||
containerSpec string
|
||||
}
|
||||
@@ -50,39 +46,36 @@ func NewCommand(logger logger.Interface) *cli.Command {
|
||||
return c.build()
|
||||
}
|
||||
|
||||
// build
|
||||
// build creates the create-symlink command.
|
||||
func (m command) build() *cli.Command {
|
||||
cfg := config{}
|
||||
|
||||
// Create the '' command
|
||||
c := cli.Command{
|
||||
Name: "create-symlinks",
|
||||
Usage: "A hook to create symlinks in the container. This can be used to process CSV mount specs",
|
||||
Usage: "A hook to create symlinks in the container.",
|
||||
Action: func(c *cli.Context) error {
|
||||
return m.run(c, &cfg)
|
||||
},
|
||||
}
|
||||
|
||||
c.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "host-root",
|
||||
Usage: "The root on the host filesystem to use to resolve symlinks",
|
||||
Destination: &cfg.hostRoot,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "csv-filename",
|
||||
Usage: "Specify a (CSV) filename to process",
|
||||
Destination: &cfg.filenames,
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "link",
|
||||
Usage: "Specify a specific link to create. The link is specified as target::link",
|
||||
Destination: &cfg.links,
|
||||
},
|
||||
// The following flags are testing-only flags.
|
||||
&cli.StringFlag{
|
||||
Name: "host-root",
|
||||
Usage: "The root on the host filesystem to use to resolve symlinks. This is only intended for testing.",
|
||||
Destination: &cfg.hostRoot,
|
||||
Hidden: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "container-spec",
|
||||
Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN",
|
||||
Usage: "Specify the path to the OCI container spec. If empty or '-' the spec will be read from STDIN. This is only intended for testing.",
|
||||
Destination: &cfg.containerSpec,
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -100,53 +93,8 @@ func (m command) run(c *cli.Context, cfg *config) error {
|
||||
return fmt.Errorf("failed to determined container root: %v", err)
|
||||
}
|
||||
|
||||
csvFiles := cfg.filenames.Value()
|
||||
|
||||
chainLocator := lookup.NewSymlinkChainLocator(
|
||||
lookup.WithLogger(m.logger),
|
||||
lookup.WithRoot(cfg.hostRoot),
|
||||
)
|
||||
|
||||
var candidates []string
|
||||
for _, file := range csvFiles {
|
||||
mountSpecs, err := csv.NewCSVFileParser(m.logger, file).Parse()
|
||||
if err != nil {
|
||||
m.logger.Debugf("Skipping CSV file %v: %v", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ms := range mountSpecs {
|
||||
if ms.Type != csv.MountSpecSym {
|
||||
continue
|
||||
}
|
||||
targets, err := chainLocator.Locate(ms.Path)
|
||||
if err != nil {
|
||||
m.logger.Warningf("Failed to locate symlink %v", ms.Path)
|
||||
}
|
||||
candidates = append(candidates, targets...)
|
||||
}
|
||||
}
|
||||
|
||||
created := make(map[string]bool)
|
||||
// candidates is a list of absolute paths to symlinks in a chain, or the final target of the chain.
|
||||
for _, candidate := range candidates {
|
||||
target, err := symlinks.Resolve(candidate)
|
||||
if err != nil {
|
||||
m.logger.Debugf("Skipping invalid link: %v", err)
|
||||
continue
|
||||
} else if target == candidate {
|
||||
m.logger.Debugf("%v is not a symlink", candidate)
|
||||
continue
|
||||
}
|
||||
|
||||
err = m.createLink(created, cfg.hostRoot, containerRoot, target, candidate)
|
||||
if err != nil {
|
||||
m.logger.Warningf("Failed to create link %v: %v", []string{target, candidate}, err)
|
||||
}
|
||||
}
|
||||
|
||||
links := cfg.links.Value()
|
||||
for _, l := range links {
|
||||
for _, l := range cfg.links.Value() {
|
||||
parts := strings.Split(l, "::")
|
||||
if len(parts) != 2 {
|
||||
m.logger.Warningf("Invalid link specification %v", l)
|
||||
@@ -158,9 +106,7 @@ func (m command) run(c *cli.Context, cfg *config) error {
|
||||
m.logger.Warningf("Failed to create link %v: %v", parts, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (m command) createLink(created map[string]bool, hostRoot string, containerRoot string, target string, link string) error {
|
||||
@@ -207,18 +153,3 @@ func changeRoot(current string, new string, path string) (string, error) {
|
||||
|
||||
return filepath.Join(new, relative), nil
|
||||
}
|
||||
|
||||
// Locate returns the link target of the specified filename or an empty slice if the
|
||||
// specified filename is not a symlink.
|
||||
func (m command) Locate(filename string) ([]string, error) {
|
||||
target, err := symlinks.Resolve(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if target == filename {
|
||||
m.logger.Debugf("%v is not a symlink", filename)
|
||||
return nil, nil
|
||||
}
|
||||
m.logger.Debugf("Resolved link: '%v' => '%v'", filename, target)
|
||||
return []string{target}, nil
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
|
||||
#
|
||||
# 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 createdevicenodes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/ldcache"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
logger logger.Interface
|
||||
}
|
||||
|
||||
type options struct {
|
||||
driverRoot string
|
||||
}
|
||||
|
||||
// NewCommand constructs a command sub-command with the specified logger
|
||||
func NewCommand(logger logger.Interface) *cli.Command {
|
||||
c := command{
|
||||
logger: logger,
|
||||
}
|
||||
return c.build()
|
||||
}
|
||||
|
||||
// build
|
||||
func (m command) build() *cli.Command {
|
||||
opts := options{}
|
||||
|
||||
c := cli.Command{
|
||||
Name: "print-ldcache",
|
||||
Usage: "A utility to print the contents of the ldcache",
|
||||
Before: func(c *cli.Context) error {
|
||||
return m.validateFlags(c, &opts)
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return m.run(c, &opts)
|
||||
},
|
||||
}
|
||||
|
||||
c.Flags = []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "driver-root",
|
||||
Usage: "the path to the driver root. Device nodes will be created at `DRIVER_ROOT`/dev",
|
||||
Value: "/",
|
||||
Destination: &opts.driverRoot,
|
||||
EnvVars: []string{"NVIDIA_DRIVER_ROOT", "DRIVER_ROOT"},
|
||||
},
|
||||
}
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (m command) validateFlags(r *cli.Context, opts *options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m command) run(c *cli.Context, opts *options) error {
|
||||
cache, err := ldcache.New(m.logger, opts.driverRoot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ldcache: %v", err)
|
||||
}
|
||||
|
||||
lib32, lib64 := cache.List()
|
||||
|
||||
if len(lib32) == 0 {
|
||||
m.logger.Info("No 32-bit libraries found")
|
||||
} else {
|
||||
m.logger.Infof("%d 32-bit libraries found", len(lib32))
|
||||
for _, lib := range lib32 {
|
||||
m.logger.Infof("%v", lib)
|
||||
}
|
||||
}
|
||||
if len(lib64) == 0 {
|
||||
m.logger.Info("No 64-bit libraries found")
|
||||
} else {
|
||||
m.logger.Infof("%d 64-bit libraries found", len(lib64))
|
||||
for _, lib := range lib64 {
|
||||
m.logger.Infof("%v", lib)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
|
||||
devchar "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk/system/create-dev-char-symlinks"
|
||||
devicenodes "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk/system/create-device-nodes"
|
||||
ldcache "github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk/system/print-ldcache"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
)
|
||||
|
||||
@@ -47,7 +46,6 @@ func (m command) build() *cli.Command {
|
||||
system.Subcommands = []*cli.Command{
|
||||
devchar.NewCommand(m.logger),
|
||||
devicenodes.NewCommand(m.logger),
|
||||
ldcache.NewCommand(m.logger),
|
||||
}
|
||||
|
||||
return &system
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
# This Dockerfile is also used to define the golang version used in this project
|
||||
# This allows dependabot to manage this version in addition to other images.
|
||||
FROM golang:1.23.1
|
||||
FROM golang:1.23.2
|
||||
|
||||
WORKDIR /work
|
||||
COPY * .
|
||||
|
||||
@@ -22,15 +22,12 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup/symlinks"
|
||||
)
|
||||
|
||||
const ldcachePath = "/etc/ld.so.cache"
|
||||
@@ -82,10 +79,9 @@ type entry2 struct {
|
||||
|
||||
// LDCache represents the interface for performing lookups into the LDCache
|
||||
//
|
||||
//go:generate moq -out ldcache_mock.go . LDCache
|
||||
//go:generate moq -rm -out ldcache_mock.go . LDCache
|
||||
type LDCache interface {
|
||||
List() ([]string, []string)
|
||||
Lookup(...string) ([]string, []string)
|
||||
}
|
||||
|
||||
type ldcache struct {
|
||||
@@ -105,14 +101,7 @@ func New(logger logger.Interface, root string) (LDCache, error) {
|
||||
|
||||
logger.Debugf("Opening ld.conf at %v", path)
|
||||
f, err := os.Open(path)
|
||||
if os.IsNotExist(err) {
|
||||
logger.Warningf("Could not find ld.so.cache at %v; creating empty cache", path)
|
||||
e := &empty{
|
||||
logger: logger,
|
||||
path: path,
|
||||
}
|
||||
return e, nil
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
@@ -196,7 +185,7 @@ type entry struct {
|
||||
}
|
||||
|
||||
// getEntries returns the entires of the ldcache in a go-friendly struct.
|
||||
func (c *ldcache) getEntries(selected func(string) bool) []entry {
|
||||
func (c *ldcache) getEntries() []entry {
|
||||
var entries []entry
|
||||
for _, e := range c.entries {
|
||||
bits := 0
|
||||
@@ -223,9 +212,6 @@ func (c *ldcache) getEntries(selected func(string) bool) []entry {
|
||||
c.logger.Debugf("Skipping invalid lib")
|
||||
continue
|
||||
}
|
||||
if !selected(lib) {
|
||||
continue
|
||||
}
|
||||
value := bytesToString(c.libs[e.Value:])
|
||||
if value == "" {
|
||||
c.logger.Debugf("Skipping invalid value for lib %v", lib)
|
||||
@@ -236,51 +222,19 @@ func (c *ldcache) getEntries(selected func(string) bool) []entry {
|
||||
bits: bits,
|
||||
value: value,
|
||||
}
|
||||
|
||||
entries = append(entries, e)
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// List creates a list of libraries in the ldcache.
|
||||
// The 32-bit and 64-bit libraries are returned separately.
|
||||
func (c *ldcache) List() ([]string, []string) {
|
||||
all := func(s string) bool { return true }
|
||||
|
||||
return c.resolveSelected(all)
|
||||
}
|
||||
|
||||
// Lookup searches the ldcache for the specified prefixes.
|
||||
// The 32-bit and 64-bit libraries matching the prefixes are returned.
|
||||
func (c *ldcache) Lookup(libPrefixes ...string) ([]string, []string) {
|
||||
c.logger.Debugf("Looking up %v in cache", libPrefixes)
|
||||
|
||||
// We define a functor to check whether a given library name matches any of the prefixes
|
||||
matchesAnyPrefix := func(s string) bool {
|
||||
for _, p := range libPrefixes {
|
||||
if strings.HasPrefix(s, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return c.resolveSelected(matchesAnyPrefix)
|
||||
}
|
||||
|
||||
// resolveSelected process the entries in the LDCach based on the supplied filter and returns the resolved paths.
|
||||
// The paths are separated by bittage.
|
||||
func (c *ldcache) resolveSelected(selected func(string) bool) ([]string, []string) {
|
||||
paths := make(map[int][]string)
|
||||
processed := make(map[string]bool)
|
||||
|
||||
for _, e := range c.getEntries(selected) {
|
||||
path, err := c.resolve(e.value)
|
||||
if err != nil {
|
||||
c.logger.Debugf("Could not resolve entry: %v", err)
|
||||
continue
|
||||
}
|
||||
for _, e := range c.getEntries() {
|
||||
path := filepath.Join(c.root, e.value)
|
||||
if processed[path] {
|
||||
continue
|
||||
}
|
||||
@@ -291,29 +245,6 @@ func (c *ldcache) resolveSelected(selected func(string) bool) ([]string, []strin
|
||||
return paths[32], paths[64]
|
||||
}
|
||||
|
||||
// resolve resolves the specified ldcache entry based on the value being processed.
|
||||
// The input is the name of the entry in the cache.
|
||||
func (c *ldcache) resolve(target string) (string, error) {
|
||||
name := filepath.Join(c.root, target)
|
||||
|
||||
c.logger.Debugf("checking %v", name)
|
||||
|
||||
link, err := symlinks.Resolve(name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve symlink: %v", err)
|
||||
}
|
||||
if link == name {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// We return absolute paths for all targets
|
||||
if !filepath.IsAbs(link) || strings.HasPrefix(link, ".") {
|
||||
link = filepath.Join(filepath.Dir(target), link)
|
||||
}
|
||||
|
||||
return c.resolve(link)
|
||||
}
|
||||
|
||||
// bytesToString converts a byte slice to a string.
|
||||
// This assumes that the byte slice is null-terminated
|
||||
func bytesToString(value []byte) string {
|
||||
|
||||
@@ -20,9 +20,6 @@ var _ LDCache = &LDCacheMock{}
|
||||
// ListFunc: func() ([]string, []string) {
|
||||
// panic("mock out the List method")
|
||||
// },
|
||||
// LookupFunc: func(strings ...string) ([]string, []string) {
|
||||
// panic("mock out the Lookup method")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // use mockedLDCache in code that requires LDCache
|
||||
@@ -33,22 +30,13 @@ type LDCacheMock struct {
|
||||
// ListFunc mocks the List method.
|
||||
ListFunc func() ([]string, []string)
|
||||
|
||||
// LookupFunc mocks the Lookup method.
|
||||
LookupFunc func(strings ...string) ([]string, []string)
|
||||
|
||||
// calls tracks calls to the methods.
|
||||
calls struct {
|
||||
// List holds details about calls to the List method.
|
||||
List []struct {
|
||||
}
|
||||
// Lookup holds details about calls to the Lookup method.
|
||||
Lookup []struct {
|
||||
// Strings is the strings argument value.
|
||||
Strings []string
|
||||
}
|
||||
}
|
||||
lockList sync.RWMutex
|
||||
lockLookup sync.RWMutex
|
||||
lockList sync.RWMutex
|
||||
}
|
||||
|
||||
// List calls ListFunc.
|
||||
@@ -77,35 +65,3 @@ func (mock *LDCacheMock) ListCalls() []struct {
|
||||
mock.lockList.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
// Lookup calls LookupFunc.
|
||||
func (mock *LDCacheMock) Lookup(strings ...string) ([]string, []string) {
|
||||
if mock.LookupFunc == nil {
|
||||
panic("LDCacheMock.LookupFunc: method is nil but LDCache.Lookup was just called")
|
||||
}
|
||||
callInfo := struct {
|
||||
Strings []string
|
||||
}{
|
||||
Strings: strings,
|
||||
}
|
||||
mock.lockLookup.Lock()
|
||||
mock.calls.Lookup = append(mock.calls.Lookup, callInfo)
|
||||
mock.lockLookup.Unlock()
|
||||
return mock.LookupFunc(strings...)
|
||||
}
|
||||
|
||||
// LookupCalls gets all the calls that were made to Lookup.
|
||||
// Check the length with:
|
||||
//
|
||||
// len(mockedLDCache.LookupCalls())
|
||||
func (mock *LDCacheMock) LookupCalls() []struct {
|
||||
Strings []string
|
||||
} {
|
||||
var calls []struct {
|
||||
Strings []string
|
||||
}
|
||||
mock.lockLookup.RLock()
|
||||
calls = mock.calls.Lookup
|
||||
mock.lockLookup.RUnlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
118
internal/lookup/ldcache.go
Normal file
118
internal/lookup/ldcache.go
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
# Copyright 2024 NVIDIA CORPORATION
|
||||
#
|
||||
# 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 lookup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/ldcache"
|
||||
)
|
||||
|
||||
type ldcacheLocator struct {
|
||||
*builder
|
||||
resolvesTo map[string]string
|
||||
}
|
||||
|
||||
var _ Locator = (*ldcacheLocator)(nil)
|
||||
|
||||
func NewLdcacheLocator(opts ...Option) Locator {
|
||||
b := newBuilder(opts...)
|
||||
|
||||
cache, err := ldcache.New(b.logger, b.root)
|
||||
if err != nil {
|
||||
b.logger.Warningf("Failed to load ldcache: %v", err)
|
||||
if b.isOptional {
|
||||
return &null{}
|
||||
}
|
||||
return ¬Found{}
|
||||
}
|
||||
|
||||
chain := NewSymlinkChainLocator(WithOptional(true))
|
||||
|
||||
resolvesTo := make(map[string]string)
|
||||
_, libs64 := cache.List()
|
||||
for _, library := range libs64 {
|
||||
if _, processed := resolvesTo[library]; processed {
|
||||
continue
|
||||
}
|
||||
candidates, err := chain.Locate(library)
|
||||
if err != nil {
|
||||
b.logger.Errorf("error processing library %s from ldcache: %v", library, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
resolvesTo[library] = library
|
||||
continue
|
||||
}
|
||||
|
||||
// candidates represents a symlink chain.
|
||||
// The first element represents the start of the chain and the last
|
||||
// element the final target.
|
||||
target := candidates[len(candidates)-1]
|
||||
for _, candidate := range candidates {
|
||||
resolvesTo[candidate] = target
|
||||
}
|
||||
}
|
||||
|
||||
return &ldcacheLocator{
|
||||
builder: b,
|
||||
resolvesTo: resolvesTo,
|
||||
}
|
||||
}
|
||||
|
||||
// Locate finds the specified libraryname.
|
||||
// If the input is a library name, the ldcache is searched otherwise the
|
||||
// provided path is resolved as a symlink.
|
||||
func (l ldcacheLocator) Locate(libname string) ([]string, error) {
|
||||
var matcher func(string, string) bool
|
||||
|
||||
if filepath.IsAbs(libname) {
|
||||
matcher = func(p string, c string) bool {
|
||||
m, _ := filepath.Match(filepath.Join(l.root, p), c)
|
||||
return m
|
||||
}
|
||||
} else {
|
||||
matcher = func(p string, c string) bool {
|
||||
m, _ := filepath.Match(p, filepath.Base(c))
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
var matches []string
|
||||
seen := make(map[string]bool)
|
||||
for name, target := range l.resolvesTo {
|
||||
if !matcher(libname, name) {
|
||||
continue
|
||||
}
|
||||
if seen[target] {
|
||||
continue
|
||||
}
|
||||
seen[target] = true
|
||||
matches = append(matches, target)
|
||||
}
|
||||
|
||||
slices.Sort(matches)
|
||||
|
||||
if len(matches) == 0 && !l.isOptional {
|
||||
return nil, fmt.Errorf("%s: %w", libname, ErrNotFound)
|
||||
}
|
||||
|
||||
return matches, nil
|
||||
}
|
||||
77
internal/lookup/ldcache_test.go
Normal file
77
internal/lookup/ldcache_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package lookup
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
testlog "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/test"
|
||||
)
|
||||
|
||||
func TestLDCacheLookup(t *testing.T) {
|
||||
logger, _ := testlog.NewNullLogger()
|
||||
|
||||
moduleRoot, err := test.GetModuleRoot()
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
rootFs string
|
||||
inputs []string
|
||||
expected string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
rootFs: "rootfs-empty",
|
||||
inputs: []string{"libcuda.so.1", "libcuda.so.*", "libcuda.so.*.*", "libcuda.so.999.88.77"},
|
||||
expectedError: ErrNotFound,
|
||||
},
|
||||
{
|
||||
rootFs: "rootfs-1",
|
||||
inputs: []string{
|
||||
"libcuda.so.1",
|
||||
"libcuda.so.*",
|
||||
"libcuda.so.*.*",
|
||||
"libcuda.so.999.88.77",
|
||||
"/lib/x86_64-linux-gnu/libcuda.so.1",
|
||||
"/lib/x86_64-linux-gnu/libcuda.so.*",
|
||||
"/lib/x86_64-linux-gnu/libcuda.so.*.*",
|
||||
"/lib/x86_64-linux-gnu/libcuda.so.999.88.77",
|
||||
},
|
||||
expected: "/lib/x86_64-linux-gnu/libcuda.so.999.88.77",
|
||||
},
|
||||
{
|
||||
rootFs: "rootfs-2",
|
||||
inputs: []string{
|
||||
"libcuda.so.1",
|
||||
"libcuda.so.*",
|
||||
"libcuda.so.*.*",
|
||||
"libcuda.so.999.88.77",
|
||||
"/var/lib/nvidia/lib64/libcuda.so.1",
|
||||
"/var/lib/nvidia/lib64/libcuda.so.*",
|
||||
"/var/lib/nvidia/lib64/libcuda.so.*.*",
|
||||
"/var/lib/nvidia/lib64/libcuda.so.999.88.77",
|
||||
},
|
||||
expected: "/var/lib/nvidia/lib64/libcuda.so.999.88.77",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
for _, input := range tc.inputs {
|
||||
t.Run(tc.rootFs+" "+input, func(t *testing.T) {
|
||||
rootfs := filepath.Join(moduleRoot, "testdata", "lookup", tc.rootFs)
|
||||
l := NewLdcacheLocator(
|
||||
WithLogger(logger),
|
||||
WithRoot(rootfs),
|
||||
)
|
||||
|
||||
candidates, err := l.Locate(input)
|
||||
require.ErrorIs(t, err, tc.expectedError)
|
||||
if tc.expectedError == nil {
|
||||
require.Equal(t, []string{filepath.Join(rootfs, tc.expected)}, candidates)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,6 @@
|
||||
|
||||
package lookup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/ldcache"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
)
|
||||
|
||||
type ldcacheLocator struct {
|
||||
logger logger.Interface
|
||||
cache ldcache.LDCache
|
||||
}
|
||||
|
||||
var _ Locator = (*ldcacheLocator)(nil)
|
||||
|
||||
// NewLibraryLocator creates a library locator using the specified options.
|
||||
func NewLibraryLocator(opts ...Option) Locator {
|
||||
b := newBuilder(opts...)
|
||||
@@ -63,39 +49,7 @@ func NewLibraryLocator(opts ...Option) Locator {
|
||||
|
||||
l := First(
|
||||
symlinkLocator,
|
||||
newLdcacheLocator(opts...),
|
||||
NewLdcacheLocator(opts...),
|
||||
)
|
||||
return l
|
||||
}
|
||||
|
||||
func newLdcacheLocator(opts ...Option) Locator {
|
||||
b := newBuilder(opts...)
|
||||
|
||||
cache, err := ldcache.New(b.logger, b.root)
|
||||
if err != nil {
|
||||
// If we failed to open the LDCache, we default to a symlink locator.
|
||||
b.logger.Warningf("Failed to load ldcache: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ldcacheLocator{
|
||||
logger: b.logger,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// Locate finds the specified libraryname.
|
||||
// If the input is a library name, the ldcache is searched otherwise the
|
||||
// provided path is resolved as a symlink.
|
||||
func (l ldcacheLocator) Locate(libname string) ([]string, error) {
|
||||
paths32, paths64 := l.cache.Lookup(libname)
|
||||
if len(paths32) > 0 {
|
||||
l.logger.Warningf("Ignoring 32-bit libraries for %v: %v", libname, paths32)
|
||||
}
|
||||
|
||||
if len(paths64) == 0 {
|
||||
return nil, fmt.Errorf("64-bit library %v: %w", libname, ErrNotFound)
|
||||
}
|
||||
|
||||
return paths64, nil
|
||||
}
|
||||
|
||||
@@ -24,82 +24,8 @@ import (
|
||||
|
||||
testlog "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/ldcache"
|
||||
)
|
||||
|
||||
func TestLDCacheLocator(t *testing.T) {
|
||||
logger, _ := testlog.NewNullLogger()
|
||||
|
||||
testDir := t.TempDir()
|
||||
symlinkDir := filepath.Join(testDir, "/lib/symlink")
|
||||
require.NoError(t, os.MkdirAll(symlinkDir, 0755))
|
||||
|
||||
versionLib := filepath.Join(symlinkDir, "libcuda.so.1.2.3")
|
||||
soLink := filepath.Join(symlinkDir, "libcuda.so")
|
||||
sonameLink := filepath.Join(symlinkDir, "libcuda.so.1")
|
||||
|
||||
_, err := os.Create(versionLib)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Symlink(versionLib, sonameLink))
|
||||
require.NoError(t, os.Symlink(sonameLink, soLink))
|
||||
|
||||
lut := newLdcacheLocator(
|
||||
WithLogger(logger),
|
||||
WithRoot(testDir),
|
||||
)
|
||||
|
||||
testCases := []struct {
|
||||
description string
|
||||
libname string
|
||||
ldcacheMap map[string]string
|
||||
expected []string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
description: "lib only resolves in LDCache",
|
||||
libname: "libcuda.so",
|
||||
ldcacheMap: map[string]string{
|
||||
"libcuda.so": "/lib/from/ldcache/libcuda.so.4.5.6",
|
||||
},
|
||||
expected: []string{"/lib/from/ldcache/libcuda.so.4.5.6"},
|
||||
},
|
||||
{
|
||||
description: "lib only not in LDCache returns error",
|
||||
libname: "libnotcuda.so",
|
||||
expectedError: ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
// We override the LDCache with a mock implementation
|
||||
l := lut.(*ldcacheLocator)
|
||||
l.cache = &ldcache.LDCacheMock{
|
||||
LookupFunc: func(strings ...string) ([]string, []string) {
|
||||
var result []string
|
||||
for _, s := range strings {
|
||||
if v, ok := tc.ldcacheMap[s]; ok {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return nil, result
|
||||
},
|
||||
}
|
||||
|
||||
candidates, err := lut.Locate(tc.libname)
|
||||
require.ErrorIs(t, err, tc.expectedError)
|
||||
|
||||
var cleanedCandidates []string
|
||||
for _, c := range candidates {
|
||||
// On MacOS /var and /tmp symlink to /private/var and /private/tmp which is included in the resolved path.
|
||||
cleanedCandidates = append(cleanedCandidates, strings.TrimPrefix(c, "/private"))
|
||||
}
|
||||
require.EqualValues(t, tc.expected, cleanedCandidates)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryLocator(t *testing.T) {
|
||||
logger, _ := testlog.NewNullLogger()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
|
||||
# Copyright 2024 NVIDIA CORPORATION
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -14,24 +14,23 @@
|
||||
# limitations under the License.
|
||||
**/
|
||||
|
||||
package ldcache
|
||||
package lookup
|
||||
|
||||
import "github.com/NVIDIA/nvidia-container-toolkit/internal/logger"
|
||||
import "fmt"
|
||||
|
||||
type empty struct {
|
||||
logger logger.Interface
|
||||
path string
|
||||
// A null locator always returns an empty response.
|
||||
type null struct {
|
||||
}
|
||||
|
||||
var _ LDCache = (*empty)(nil)
|
||||
|
||||
// List always returns nil for an empty ldcache
|
||||
func (e *empty) List() ([]string, []string) {
|
||||
// Locate always returns empty for a null locator.
|
||||
func (l *null) Locate(string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Lookup logs a debug message and returns nil for an empty ldcache
|
||||
func (e *empty) Lookup(prefixes ...string) ([]string, []string) {
|
||||
e.logger.Debugf("Calling Lookup(%v) on empty ldcache: %v", prefixes, e.path)
|
||||
return nil, nil
|
||||
// A notFound locator always returns an ErrNotFound error.
|
||||
type notFound struct {
|
||||
}
|
||||
|
||||
func (l *notFound) Locate(s string) ([]string, error) {
|
||||
return nil, fmt.Errorf("%s: %w", s, ErrNotFound)
|
||||
}
|
||||
81
internal/lookup/root/root_test.go
Normal file
81
internal/lookup/root/root_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
# Copyright 2023 NVIDIA CORPORATION
|
||||
#
|
||||
# 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 root
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
testlog "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/lookup"
|
||||
"github.com/NVIDIA/nvidia-container-toolkit/internal/test"
|
||||
)
|
||||
|
||||
func TestDriverLibrariesLocate(t *testing.T) {
|
||||
logger, _ := testlog.NewNullLogger()
|
||||
|
||||
moduleRoot, err := test.GetModuleRoot()
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
rootFs string
|
||||
inputs []string
|
||||
expected string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
rootFs: "rootfs-empty",
|
||||
inputs: []string{"libcuda.so.1", "libcuda.so.*", "libcuda.so.*.*", "libcuda.so.999.88.77"},
|
||||
expectedError: lookup.ErrNotFound,
|
||||
},
|
||||
{
|
||||
rootFs: "rootfs-no-cache-lib64",
|
||||
inputs: []string{"libcuda.so.1", "libcuda.so.*", "libcuda.so.*.*", "libcuda.so.999.88.77"},
|
||||
expected: "/usr/lib64/libcuda.so.999.88.77",
|
||||
},
|
||||
{
|
||||
rootFs: "rootfs-1",
|
||||
inputs: []string{"libcuda.so.1", "libcuda.so.*", "libcuda.so.*.*", "libcuda.so.999.88.77"},
|
||||
expected: "/lib/x86_64-linux-gnu/libcuda.so.999.88.77",
|
||||
},
|
||||
{
|
||||
rootFs: "rootfs-2",
|
||||
inputs: []string{"libcuda.so.1", "libcuda.so.*", "libcuda.so.*.*", "libcuda.so.999.88.77"},
|
||||
expected: "/var/lib/nvidia/lib64/libcuda.so.999.88.77",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
for _, input := range tc.inputs {
|
||||
t.Run(tc.rootFs+input, func(t *testing.T) {
|
||||
rootfs := filepath.Join(moduleRoot, "testdata", "lookup", tc.rootFs)
|
||||
driver := New(
|
||||
WithLogger(logger),
|
||||
WithDriverRoot(rootfs),
|
||||
)
|
||||
|
||||
candidates, err := driver.Libraries().Locate(input)
|
||||
require.ErrorIs(t, err, tc.expectedError)
|
||||
if tc.expectedError == nil {
|
||||
require.Equal(t, []string{filepath.Join(rootfs, tc.expected)}, candidates)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ func (p symlinkChain) Locate(pattern string) ([]string, error) {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
var filenames []string
|
||||
found := make(map[string]bool)
|
||||
for len(candidates) > 0 {
|
||||
candidate := candidates[0]
|
||||
@@ -70,6 +71,7 @@ func (p symlinkChain) Locate(pattern string) ([]string, error) {
|
||||
continue
|
||||
}
|
||||
found[candidate] = true
|
||||
filenames = append(filenames, candidate)
|
||||
|
||||
target, err := symlinks.Resolve(candidate)
|
||||
if err != nil {
|
||||
@@ -88,11 +90,6 @@ func (p symlinkChain) Locate(pattern string) ([]string, error) {
|
||||
candidates = append(candidates, target)
|
||||
}
|
||||
}
|
||||
|
||||
var filenames []string
|
||||
for f := range found {
|
||||
filenames = append(filenames, f)
|
||||
}
|
||||
return filenames, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +128,7 @@ func (c *Config) GetRuntimeConfig(name string) (engine.RuntimeConfig, error) {
|
||||
|
||||
// CommandLineSource returns the CLI-based containerd config loader
|
||||
func CommandLineSource(hostRoot string) toml.Loader {
|
||||
commandLine := chrootIfRequired(hostRoot, "containerd", "config", "dump")
|
||||
return toml.FromCommandLine(commandLine[0], commandLine[1:]...)
|
||||
return toml.FromCommandLine(chrootIfRequired(hostRoot, "containerd", "config", "dump")...)
|
||||
}
|
||||
|
||||
func chrootIfRequired(hostRoot string, commandLine ...string) []string {
|
||||
|
||||
@@ -155,8 +155,10 @@ func (c *Config) GetRuntimeConfig(name string) (engine.RuntimeConfig, error) {
|
||||
|
||||
// CommandLineSource returns the CLI-based crio config loader
|
||||
func CommandLineSource(hostRoot string) toml.Loader {
|
||||
commandLine := chrootIfRequired(hostRoot, "crio", "status", "config")
|
||||
return toml.FromCommandLine(commandLine[0], commandLine[1:]...)
|
||||
return toml.LoadFirst(
|
||||
toml.FromCommandLine(chrootIfRequired(hostRoot, "crio", "status", "config")...),
|
||||
toml.FromCommandLine(chrootIfRequired(hostRoot, "crio-status", "config")...),
|
||||
)
|
||||
}
|
||||
|
||||
func chrootIfRequired(hostRoot string, commandLine ...string) []string {
|
||||
|
||||
41
pkg/config/toml/list.go
Normal file
41
pkg/config/toml/list.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
# Copyright 2024 NVIDIA CORPORATION
|
||||
#
|
||||
# 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 toml
|
||||
|
||||
import "errors"
|
||||
|
||||
type firstOf []Loader
|
||||
|
||||
func LoadFirst(loaders ...Loader) Loader {
|
||||
return firstOf(loaders)
|
||||
}
|
||||
|
||||
func (loaders firstOf) Load() (*Tree, error) {
|
||||
var errs error
|
||||
for _, loader := range loaders {
|
||||
if loader == nil {
|
||||
continue
|
||||
}
|
||||
tree, err := loader.Load()
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, err)
|
||||
continue
|
||||
}
|
||||
return tree, nil
|
||||
}
|
||||
return nil, errs
|
||||
}
|
||||
@@ -36,12 +36,12 @@ func FromFile(path string) Loader {
|
||||
|
||||
// FromCommandLine creates a TOML source from the output of a shell command and its corresponding args.
|
||||
// If the command is empty, an empty config is returned.
|
||||
func FromCommandLine(cmd string, args ...string) Loader {
|
||||
if len(cmd) == 0 {
|
||||
func FromCommandLine(cmds ...string) Loader {
|
||||
if len(cmds) == 0 {
|
||||
return Empty
|
||||
}
|
||||
return &tomlCliSource{
|
||||
command: cmd,
|
||||
args: args,
|
||||
command: cmds[0],
|
||||
args: cmds[1:],
|
||||
}
|
||||
}
|
||||
|
||||
0
testdata/go.mod
vendored
Normal file
0
testdata/go.mod
vendored
Normal file
2
testdata/lookup/rootfs-1/README.md
vendored
Normal file
2
testdata/lookup/rootfs-1/README.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
This rootfs represents a host with the CUDA driver libraries installed in
|
||||
/lib/x86_64-linux-gnu. The included /etc/ld.so.cache was copied from such as system.
|
||||
BIN
testdata/lookup/rootfs-1/etc/ld.so.cache
vendored
Normal file
BIN
testdata/lookup/rootfs-1/etc/ld.so.cache
vendored
Normal file
Binary file not shown.
1
testdata/lookup/rootfs-1/lib/x86_64-linux-gnu/libcuda.so.1
vendored
Symbolic link
1
testdata/lookup/rootfs-1/lib/x86_64-linux-gnu/libcuda.so.1
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
libcuda.so.999.88.77
|
||||
0
testdata/lookup/rootfs-1/lib/x86_64-linux-gnu/libcuda.so.999.88.77
vendored
Normal file
0
testdata/lookup/rootfs-1/lib/x86_64-linux-gnu/libcuda.so.999.88.77
vendored
Normal file
3
testdata/lookup/rootfs-2/README.md
vendored
Normal file
3
testdata/lookup/rootfs-2/README.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
This rootfs represents a host with the CUDA driver libraries installed in
|
||||
/var/lib/nvidia/lib64. The included /etc/ld.so.cache was generated in a container
|
||||
simulating such as system.
|
||||
BIN
testdata/lookup/rootfs-2/etc/ld.so.cache
vendored
Normal file
BIN
testdata/lookup/rootfs-2/etc/ld.so.cache
vendored
Normal file
Binary file not shown.
1
testdata/lookup/rootfs-2/var/lib/nvidia/lib64/libcuda.so.1
vendored
Symbolic link
1
testdata/lookup/rootfs-2/var/lib/nvidia/lib64/libcuda.so.1
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
libcuda.so.999.88.77
|
||||
0
testdata/lookup/rootfs-2/var/lib/nvidia/lib64/libcuda.so.999.88.77
vendored
Normal file
0
testdata/lookup/rootfs-2/var/lib/nvidia/lib64/libcuda.so.999.88.77
vendored
Normal file
1
testdata/lookup/rootfs-empty/README.md
vendored
Normal file
1
testdata/lookup/rootfs-empty/README.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
The folders represents an empty rootfs.
|
||||
1
testdata/lookup/rootfs-no-cache-lib64/usr/lib64/libcuda.so.1
vendored
Symbolic link
1
testdata/lookup/rootfs-no-cache-lib64/usr/lib64/libcuda.so.1
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
libcuda.so.999.88.77
|
||||
0
testdata/lookup/rootfs-no-cache-lib64/usr/lib64/libcuda.so.999.88.77
vendored
Normal file
0
testdata/lookup/rootfs-no-cache-lib64/usr/lib64/libcuda.so.999.88.77
vendored
Normal file
@@ -14,7 +14,7 @@
|
||||
|
||||
LIB_NAME := nvidia-container-toolkit
|
||||
LIB_VERSION := 1.17.0
|
||||
LIB_TAG := rc.1
|
||||
LIB_TAG := rc.2
|
||||
|
||||
# The package version is the combination of the library version and tag.
|
||||
# If the tag is specified the two components are joined with a tilde (~).
|
||||
|
||||
Reference in New Issue
Block a user