Browse code

builder: remove container package dependency

Signed-off-by: Tibor Vass <tibor@docker.com>

Tibor Vass authored on 2015/12/10 23:35:53
Showing 10 changed files
... ...
@@ -7,9 +7,10 @@ package builder
7 7
 import (
8 8
 	"io"
9 9
 	"os"
10
+	"time"
10 11
 
11 12
 	"github.com/docker/docker/api/types"
12
-	"github.com/docker/docker/container"
13
+	"github.com/docker/docker/daemon"
13 14
 	"github.com/docker/docker/image"
14 15
 	"github.com/docker/docker/runconfig"
15 16
 )
... ...
@@ -106,30 +107,40 @@ func (fi *HashedFileInfo) SetHash(h string) {
106 106
 	fi.FileHash = h
107 107
 }
108 108
 
109
-// Docker abstracts calls to a Docker Daemon.
110
-type Docker interface {
109
+// Backend abstracts calls to a Docker Daemon.
110
+type Backend interface {
111 111
 	// TODO: use digest reference instead of name
112 112
 
113 113
 	// LookupImage looks up a Docker image referenced by `name`.
114
-	LookupImage(name string) (*image.Image, error)
114
+	GetImage(name string) (*image.Image, error)
115 115
 	// Pull tells Docker to pull image referenced by `name`.
116 116
 	Pull(name string) (*image.Image, error)
117
-
118
-	// Container looks up a Docker container referenced by `id`.
119
-	Container(id string) (*container.Container, error)
120
-	// Create creates a new Docker container and returns potential warnings
121
-	// TODO: put warnings in the error
122
-	Create(*runconfig.Config, *runconfig.HostConfig) (*container.Container, []string, error)
123
-	// Remove removes a container specified by `id`.
124
-	Remove(id string, cfg *types.ContainerRmConfig) error
117
+	// ContainerWsAttachWithLogs attaches to container.
118
+	ContainerWsAttachWithLogs(name string, cfg *daemon.ContainerWsAttachWithLogsConfig) error
119
+	// ContainerCreate creates a new Docker container and returns potential warnings
120
+	ContainerCreate(params *daemon.ContainerCreateConfig) (types.ContainerCreateResponse, error)
121
+	// ContainerRm removes a container specified by `id`.
122
+	ContainerRm(name string, config *types.ContainerRmConfig) error
125 123
 	// Commit creates a new Docker image from an existing Docker container.
126 124
 	Commit(string, *types.ContainerCommitConfig) (string, error)
127
-	// Copy copies/extracts a source FileInfo to a destination path inside a container
125
+	// Kill stops the container execution abruptly.
126
+	ContainerKill(containerID string, sig uint64) error
127
+	// Start starts a new container
128
+	ContainerStart(containerID string, hostConfig *runconfig.HostConfig) error
129
+	// ContainerWait stops processing until the given container is stopped.
130
+	ContainerWait(containerID string, timeout time.Duration) (int, error)
131
+
132
+	// ContainerUpdateCmd updates container.Path and container.Args
133
+	ContainerUpdateCmd(containerID string, cmd []string) error
134
+
135
+	// ContainerCopy copies/extracts a source FileInfo to a destination path inside a container
128 136
 	// specified by a container object.
129 137
 	// TODO: make an Extract method instead of passing `decompress`
130 138
 	// TODO: do not pass a FileInfo, instead refactor the archive package to export a Walk function that can be used
131 139
 	// with Context.Walk
132
-	Copy(c *container.Container, destPath string, src FileInfo, decompress bool) error
140
+	//ContainerCopy(name string, res string) (io.ReadCloser, error)
141
+	// TODO: use copyBackend api
142
+	BuilderCopy(containerID string, destPath string, src FileInfo, decompress bool) error
133 143
 
134 144
 	// Retain retains an image avoiding it to be removed or overwritten until a corresponding Release() call.
135 145
 	// TODO: remove
... ...
@@ -137,14 +148,6 @@ type Docker interface {
137 137
 	// Release releases a list of images that were retained for the time of a build.
138 138
 	// TODO: remove
139 139
 	Release(sessionID string, activeImages []string)
140
-	// Kill stops the container execution abruptly.
141
-	Kill(c *container.Container) error
142
-	// Mount mounts the root filesystem for the container.
143
-	Mount(c *container.Container) error
144
-	// Unmount unmounts the root filesystem for the container.
145
-	Unmount(c *container.Container) error
146
-	// Start starts a new container
147
-	Start(c *container.Container) error
148 140
 }
149 141
 
150 142
 // ImageCache abstracts an image cache store.
... ...
@@ -77,7 +77,7 @@ type Builder struct {
77 77
 	Stdout io.Writer
78 78
 	Stderr io.Writer
79 79
 
80
-	docker  builder.Docker
80
+	docker  builder.Backend
81 81
 	context builder.Context
82 82
 
83 83
 	dockerfile       *parser.Node
... ...
@@ -102,7 +102,7 @@ type Builder struct {
102 102
 // NewBuilder creates a new Dockerfile builder from an optional dockerfile and a Config.
103 103
 // If dockerfile is nil, the Dockerfile specified by Config.DockerfileName,
104 104
 // will be read from the Context passed to Build().
105
-func NewBuilder(config *Config, docker builder.Docker, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
105
+func NewBuilder(config *Config, docker builder.Backend, context builder.Context, dockerfile io.ReadCloser) (b *Builder, err error) {
106 106
 	if config == nil {
107 107
 		config = new(Config)
108 108
 	}
... ...
@@ -215,7 +215,7 @@ func from(b *Builder, args []string, attributes map[string]bool, original string
215 215
 	)
216 216
 	// TODO: don't use `name`, instead resolve it to a digest
217 217
 	if !b.Pull {
218
-		image, err = b.docker.LookupImage(name)
218
+		image, err = b.docker.GetImage(name)
219 219
 		// TODO: shouldn't we error out if error is different from "not found" ?
220 220
 	}
221 221
 	if image == nil {
... ...
@@ -394,18 +394,12 @@ func run(b *Builder, args []string, attributes map[string]bool, original string)
394 394
 
395 395
 	logrus.Debugf("[BUILDER] Command to be executed: %v", b.runConfig.Cmd)
396 396
 
397
-	c, err := b.create()
397
+	cID, err := b.create()
398 398
 	if err != nil {
399 399
 		return err
400 400
 	}
401 401
 
402
-	// Ensure that we keep the container mounted until the commit
403
-	// to avoid unmounting and then mounting directly again
404
-	b.docker.Mount(c)
405
-	defer b.docker.Unmount(c)
406
-
407
-	err = b.run(c)
408
-	if err != nil {
402
+	if err := b.run(cID); err != nil {
409 403
 		return err
410 404
 	}
411 405
 
... ...
@@ -414,11 +408,7 @@ func run(b *Builder, args []string, attributes map[string]bool, original string)
414 414
 	// properly match it.
415 415
 	b.runConfig.Env = env
416 416
 	b.runConfig.Cmd = saveCmd
417
-	if err := b.commit(c.ID, cmd, "run"); err != nil {
418
-		return err
419
-	}
420
-
421
-	return nil
417
+	return b.commit(cID, cmd, "run")
422 418
 }
423 419
 
424 420
 // CMD foo
... ...
@@ -23,7 +23,7 @@ import (
23 23
 	"github.com/docker/docker/api/types"
24 24
 	"github.com/docker/docker/builder"
25 25
 	"github.com/docker/docker/builder/dockerfile/parser"
26
-	"github.com/docker/docker/container"
26
+	"github.com/docker/docker/daemon"
27 27
 	"github.com/docker/docker/image"
28 28
 	"github.com/docker/docker/pkg/archive"
29 29
 	"github.com/docker/docker/pkg/httputils"
... ...
@@ -56,21 +56,16 @@ func (b *Builder) commit(id string, autoCmd *stringutils.StrSlice, comment strin
56 56
 		}
57 57
 		defer func(cmd *stringutils.StrSlice) { b.runConfig.Cmd = cmd }(cmd)
58 58
 
59
-		if hit, err := b.probeCache(); err != nil {
59
+		hit, err := b.probeCache()
60
+		if err != nil {
60 61
 			return err
61 62
 		} else if hit {
62 63
 			return nil
63 64
 		}
64
-		container, err := b.create()
65
+		id, err = b.create()
65 66
 		if err != nil {
66 67
 			return err
67 68
 		}
68
-		id = container.ID
69
-
70
-		if err := b.docker.Mount(container); err != nil {
71
-			return err
72
-		}
73
-		defer b.docker.Unmount(container)
74 69
 	}
75 70
 
76 71
 	// Note: Actually copy the struct
... ...
@@ -192,11 +187,10 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
192 192
 		return nil
193 193
 	}
194 194
 
195
-	container, _, err := b.docker.Create(b.runConfig, nil)
195
+	container, err := b.docker.ContainerCreate(&daemon.ContainerCreateConfig{Config: b.runConfig})
196 196
 	if err != nil {
197 197
 		return err
198 198
 	}
199
-	defer b.docker.Unmount(container)
200 199
 	b.tmpContainers[container.ID] = struct{}{}
201 200
 
202 201
 	comment := fmt.Sprintf("%s %s in %s", cmdName, origPaths, dest)
... ...
@@ -214,15 +208,12 @@ func (b *Builder) runContextCommand(args []string, allowRemote bool, allowLocalD
214 214
 	}
215 215
 
216 216
 	for _, info := range infos {
217
-		if err := b.docker.Copy(container, dest, info.FileInfo, info.decompress); err != nil {
217
+		if err := b.docker.BuilderCopy(container.ID, dest, info.FileInfo, info.decompress); err != nil {
218 218
 			return err
219 219
 		}
220 220
 	}
221 221
 
222
-	if err := b.commit(container.ID, cmd, comment); err != nil {
223
-		return err
224
-	}
225
-	return nil
222
+	return b.commit(container.ID, cmd, comment)
226 223
 }
227 224
 
228 225
 func (b *Builder) download(srcURL string) (fi builder.FileInfo, err error) {
... ...
@@ -414,8 +405,8 @@ func (b *Builder) processImageFrom(img *image.Image) error {
414 414
 	}
415 415
 
416 416
 	// The default path will be blank on Windows (set by HCS)
417
-	if len(b.runConfig.Env) == 0 && container.DefaultPathEnv != "" {
418
-		b.runConfig.Env = append(b.runConfig.Env, "PATH="+container.DefaultPathEnv)
417
+	if len(b.runConfig.Env) == 0 && system.DefaultPathEnv != "" {
418
+		b.runConfig.Env = append(b.runConfig.Env, "PATH="+system.DefaultPathEnv)
419 419
 	}
420 420
 
421 421
 	// Process ONBUILD triggers if they exist
... ...
@@ -487,9 +478,9 @@ func (b *Builder) probeCache() (bool, error) {
487 487
 	return true, nil
488 488
 }
489 489
 
490
-func (b *Builder) create() (*container.Container, error) {
490
+func (b *Builder) create() (string, error) {
491 491
 	if b.image == "" && !b.noBaseImage {
492
-		return nil, fmt.Errorf("Please provide a source image with `from` prior to run")
492
+		return "", fmt.Errorf("Please provide a source image with `from` prior to run")
493 493
 	}
494 494
 	b.runConfig.Image = b.image
495 495
 
... ...
@@ -515,12 +506,14 @@ func (b *Builder) create() (*container.Container, error) {
515 515
 	config := *b.runConfig
516 516
 
517 517
 	// Create the container
518
-	c, warnings, err := b.docker.Create(b.runConfig, hostConfig)
518
+	c, err := b.docker.ContainerCreate(&daemon.ContainerCreateConfig{
519
+		Config:     b.runConfig,
520
+		HostConfig: hostConfig,
521
+	})
519 522
 	if err != nil {
520
-		return nil, err
523
+		return "", err
521 524
 	}
522
-	defer b.docker.Unmount(c)
523
-	for _, warning := range warnings {
525
+	for _, warning := range c.Warnings {
524 526
 		fmt.Fprintf(b.Stdout, " ---> [Warning] %s\n", warning)
525 527
 	}
526 528
 
... ...
@@ -529,23 +522,24 @@ func (b *Builder) create() (*container.Container, error) {
529 529
 
530 530
 	if config.Cmd.Len() > 0 {
531 531
 		// override the entry point that may have been picked up from the base image
532
-		s := config.Cmd.Slice()
533
-		c.Path = s[0]
534
-		c.Args = s[1:]
532
+		if err := b.docker.ContainerUpdateCmd(c.ID, config.Cmd.Slice()); err != nil {
533
+			return "", err
534
+		}
535 535
 	}
536 536
 
537
-	return c, nil
537
+	return c.ID, nil
538 538
 }
539 539
 
540
-func (b *Builder) run(c *container.Container) error {
541
-	var errCh chan error
540
+func (b *Builder) run(cID string) (err error) {
541
+	errCh := make(chan error)
542 542
 	if b.Verbose {
543
-		errCh = c.Attach(nil, b.Stdout, b.Stderr)
544
-	}
545
-
546
-	//start the container
547
-	if err := b.docker.Start(c); err != nil {
548
-		return err
543
+		go func() {
544
+			errCh <- b.docker.ContainerWsAttachWithLogs(cID, &daemon.ContainerWsAttachWithLogsConfig{
545
+				OutStream: b.Stdout,
546
+				ErrStream: b.Stderr,
547
+				Stream:    true,
548
+			})
549
+		}()
549 550
 	}
550 551
 
551 552
 	finished := make(chan struct{})
... ...
@@ -553,13 +547,17 @@ func (b *Builder) run(c *container.Container) error {
553 553
 	go func() {
554 554
 		select {
555 555
 		case <-b.cancelled:
556
-			logrus.Debugln("Build cancelled, killing and removing container:", c.ID)
557
-			b.docker.Kill(c)
558
-			b.removeContainer(c.ID)
556
+			logrus.Debugln("Build cancelled, killing and removing container:", cID)
557
+			b.docker.ContainerKill(cID, 0)
558
+			b.removeContainer(cID)
559 559
 		case <-finished:
560 560
 		}
561 561
 	}()
562 562
 
563
+	if err := b.docker.ContainerStart(cID, nil); err != nil {
564
+		return err
565
+	}
566
+
563 567
 	if b.Verbose {
564 568
 		// Block on reading output from container, stop on err or chan closed
565 569
 		if err := <-errCh; err != nil {
... ...
@@ -567,8 +565,7 @@ func (b *Builder) run(c *container.Container) error {
567 567
 		}
568 568
 	}
569 569
 
570
-	// Wait for it to finish
571
-	if ret, _ := c.WaitStop(-1 * time.Second); ret != 0 {
570
+	if ret, _ := b.docker.ContainerWait(cID, -1); ret != 0 {
572 571
 		// TODO: change error type, because jsonmessage.JSONError assumes HTTP
573 572
 		return &jsonmessage.JSONError{
574 573
 			Message: fmt.Sprintf("The command '%s' returned a non-zero code: %d", b.runConfig.Cmd.ToString(), ret),
... ...
@@ -584,7 +581,7 @@ func (b *Builder) removeContainer(c string) error {
584 584
 		ForceRemove:  true,
585 585
 		RemoveVolume: true,
586 586
 	}
587
-	if err := b.docker.Remove(c, rmConfig); err != nil {
587
+	if err := b.docker.ContainerRm(c, rmConfig); err != nil {
588 588
 		fmt.Fprintf(b.Stdout, "Error removing intermediate container %s: %v\n", stringid.TruncateID(c), err)
589 589
 		return err
590 590
 	}
... ...
@@ -29,15 +29,8 @@ import (
29 29
 	"github.com/opencontainers/runc/libcontainer/label"
30 30
 )
31 31
 
32
-const (
33
-	// DefaultPathEnv is unix style list of directories to search for
34
-	// executables. Each directory is separated from the next by a colon
35
-	// ':' character .
36
-	DefaultPathEnv = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
37
-
38
-	// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
39
-	DefaultSHMSize int64 = 67108864
40
-)
32
+// DefaultSHMSize is the default size (64MB) of the SHM which will be mounted in the container
33
+const DefaultSHMSize int64 = 67108864
41 34
 
42 35
 // Container holds the fields specific to unixen implementations. See
43 36
 // CommonContainer for standard fields common to all containers.
... ...
@@ -66,7 +59,7 @@ func (container *Container) CreateDaemonEnvironment(linkedEnv []string) []string
66 66
 	}
67 67
 	// Setup environment
68 68
 	env := []string{
69
-		"PATH=" + DefaultPathEnv,
69
+		"PATH=" + system.DefaultPathEnv,
70 70
 		"HOSTNAME=" + fullHostname,
71 71
 		// Note: we don't set HOME here because it'll get autoset intelligently
72 72
 		// based on the value of USER inside dockerinit, but only if it isn't
... ...
@@ -7,10 +7,6 @@ import (
7 7
 	"github.com/docker/docker/volume"
8 8
 )
9 9
 
10
-// DefaultPathEnv is deliberately empty on Windows as the default path will be set by
11
-// the container. Docker has no context of what the default path should be.
12
-const DefaultPathEnv = ""
13
-
14 10
 // Container holds fields specific to the Windows implementation. See
15 11
 // CommonContainer for standard fields common to all containers.
16 12
 type Container struct {
... ...
@@ -13,7 +13,6 @@ import (
13 13
 	"github.com/docker/docker/api"
14 14
 	"github.com/docker/docker/api/types"
15 15
 	"github.com/docker/docker/builder"
16
-	"github.com/docker/docker/container"
17 16
 	"github.com/docker/docker/daemon"
18 17
 	"github.com/docker/docker/image"
19 18
 	"github.com/docker/docker/pkg/archive"
... ...
@@ -25,21 +24,16 @@ import (
25 25
 	"github.com/docker/docker/runconfig"
26 26
 )
27 27
 
28
-// Docker implements builder.Docker for the docker Daemon object.
28
+// Docker implements builder.Backend for the docker Daemon object.
29 29
 type Docker struct {
30
-	Daemon      *daemon.Daemon
30
+	*daemon.Daemon
31 31
 	OutOld      io.Writer
32 32
 	AuthConfigs map[string]types.AuthConfig
33 33
 	Archiver    *archive.Archiver
34 34
 }
35 35
 
36
-// ensure Docker implements builder.Docker
37
-var _ builder.Docker = Docker{}
38
-
39
-// LookupImage looks up a Docker image referenced by `name`.
40
-func (d Docker) LookupImage(name string) (*image.Image, error) {
41
-	return d.Daemon.GetImage(name)
42
-}
36
+// ensure Docker implements builder.Backend
37
+var _ builder.Backend = Docker{}
43 38
 
44 39
 // Pull tells Docker to pull image referenced by `name`.
45 40
 func (d Docker) Pull(name string) (*image.Image, error) {
... ...
@@ -79,38 +73,15 @@ func (d Docker) Pull(name string) (*image.Image, error) {
79 79
 	return d.Daemon.GetImage(name)
80 80
 }
81 81
 
82
-// Container looks up a Docker container referenced by `id`.
83
-func (d Docker) Container(id string) (*container.Container, error) {
84
-	return d.Daemon.GetContainer(id)
85
-}
86
-
87
-// Create creates a new Docker container and returns potential warnings
88
-func (d Docker) Create(cfg *runconfig.Config, hostCfg *runconfig.HostConfig) (*container.Container, []string, error) {
89
-	ccr, err := d.Daemon.ContainerCreate(&daemon.ContainerCreateConfig{
90
-		Name:            "",
91
-		Config:          cfg,
92
-		HostConfig:      hostCfg,
93
-		AdjustCPUShares: true,
94
-	})
95
-	if err != nil {
96
-		return nil, nil, err
97
-	}
98
-	container, err := d.Container(ccr.ID)
82
+// ContainerUpdateCmd updates Path and Args for the container with ID cID.
83
+func (d Docker) ContainerUpdateCmd(cID string, cmd []string) error {
84
+	c, err := d.Daemon.GetContainer(cID)
99 85
 	if err != nil {
100
-		return nil, ccr.Warnings, err
86
+		return err
101 87
 	}
102
-
103
-	return container, ccr.Warnings, d.Mount(container)
104
-}
105
-
106
-// Remove removes a container specified by `id`.
107
-func (d Docker) Remove(id string, cfg *types.ContainerRmConfig) error {
108
-	return d.Daemon.ContainerRm(id, cfg)
109
-}
110
-
111
-// Commit creates a new Docker image from an existing Docker container.
112
-func (d Docker) Commit(name string, cfg *types.ContainerCommitConfig) (string, error) {
113
-	return d.Daemon.Commit(name, cfg)
88
+	c.Path = cmd[0]
89
+	c.Args = cmd[1:]
90
+	return nil
114 91
 }
115 92
 
116 93
 // Retain retains an image avoiding it to be removed or overwritten until a corresponding Release() call.
... ...
@@ -125,11 +96,11 @@ func (d Docker) Release(sessionID string, activeImages []string) {
125 125
 	//d.Daemon.Graph().Release(sessionID, activeImages...)
126 126
 }
127 127
 
128
-// Copy copies/extracts a source FileInfo to a destination path inside a container
128
+// BuilderCopy copies/extracts a source FileInfo to a destination path inside a container
129 129
 // specified by a container object.
130 130
 // TODO: make sure callers don't unnecessarily convert destPath with filepath.FromSlash (Copy does it already).
131
-// Copy should take in abstract paths (with slashes) and the implementation should convert it to OS-specific paths.
132
-func (d Docker) Copy(c *container.Container, destPath string, src builder.FileInfo, decompress bool) error {
131
+// BuilderCopy should take in abstract paths (with slashes) and the implementation should convert it to OS-specific paths.
132
+func (d Docker) BuilderCopy(cID string, destPath string, src builder.FileInfo, decompress bool) error {
133 133
 	srcPath := src.Path()
134 134
 	destExists := true
135 135
 	rootUID, rootGID := d.Daemon.GetRemappedUIDGID()
... ...
@@ -137,6 +108,10 @@ func (d Docker) Copy(c *container.Container, destPath string, src builder.FileIn
137 137
 	// Work in daemon-local OS specific file paths
138 138
 	destPath = filepath.FromSlash(destPath)
139 139
 
140
+	c, err := d.Daemon.GetContainer(cID)
141
+	if err != nil {
142
+		return err
143
+	}
140 144
 	dest, err := c.GetResourcePath(destPath)
141 145
 	if err != nil {
142 146
 		return err
... ...
@@ -211,27 +186,6 @@ func (d Docker) GetCachedImage(imgID string, cfg *runconfig.Config) (string, err
211 211
 	return cache.ID().String(), nil
212 212
 }
213 213
 
214
-// Kill stops the container execution abruptly.
215
-func (d Docker) Kill(container *container.Container) error {
216
-	return d.Daemon.Kill(container)
217
-}
218
-
219
-// Mount mounts the root filesystem for the container.
220
-func (d Docker) Mount(c *container.Container) error {
221
-	return d.Daemon.Mount(c)
222
-}
223
-
224
-// Unmount unmounts the root filesystem for the container.
225
-func (d Docker) Unmount(c *container.Container) error {
226
-	d.Daemon.Unmount(c)
227
-	return nil
228
-}
229
-
230
-// Start starts a container
231
-func (d Docker) Start(c *container.Container) error {
232
-	return d.Daemon.Start(c)
233
-}
234
-
235 214
 // Following is specific to builder contexts
236 215
 
237 216
 // DetectContextFromRemoteURL returns a context and in certain cases the name of the dockerfile to be used
... ...
@@ -50,11 +50,7 @@ func (daemon *Daemon) ContainerStart(name string, hostConfig *runconfig.HostConf
50 50
 		return err
51 51
 	}
52 52
 
53
-	if err := daemon.containerStart(container); err != nil {
54
-		return err
55
-	}
56
-
57
-	return nil
53
+	return daemon.containerStart(container)
58 54
 }
59 55
 
60 56
 // Start starts a container
61 57
new file mode 100644
... ...
@@ -0,0 +1,8 @@
0
+// +build !windows
1
+
2
+package system
3
+
4
+// DefaultPathEnv is unix style list of directories to search for
5
+// executables. Each directory is separated from the next by a colon
6
+// ':' character .
7
+const DefaultPathEnv = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
0 8
new file mode 100644
... ...
@@ -0,0 +1,7 @@
0
+// +build windows
1
+
2
+package system
3
+
4
+// DefaultPathEnv is deliberately empty on Windows as the default path will be set by
5
+// the container. Docker has no context of what the default path should be.
6
+const DefaultPathEnv = ""