Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1dad3c8
Add unexport and export.
silverhadch Jul 12, 2025
cc714d6
Add Docs to new Features
silverhadch Jul 12, 2025
230ac94
Add new subcommands to common Usage
silverhadch Jul 12, 2025
0bb8b67
add help function
silverhadch Jul 12, 2025
6917465
Add unexport Help
silverhadch Jul 12, 2025
95e0bf2
Update export.go
silverhadch Jul 12, 2025
3054161
Update unexport.go
silverhadch Jul 12, 2025
4ec89ed
Update toolbox-export.1.md
silverhadch Jul 12, 2025
f84d5f8
Update toolbox-unexport.1.md
silverhadch Jul 12, 2025
4805d70
Update toolbox.1.md
silverhadch Jul 12, 2025
e4e414a
Import Utils
silverhadch Jul 13, 2025
2c8ae21
Add helper function for deletion
silverhadch Jul 13, 2025
8f66ed2
Unexport on Delete
silverhadch Jul 13, 2025
849f064
Add flagcompletion to --container flag
silverhadch Jul 13, 2025
967f5c5
Use new RunCommandwithOutput Function instead of calling toolbox from
silverhadch Jul 17, 2025
257af25
Format Code properly
silverhadch Jul 17, 2025
83e1f21
Update utils.go
silverhadch Aug 13, 2025
96c0f8f
Update utils.go
silverhadch Aug 13, 2025
2c7f7d2
Try Tests.
silverhadch Aug 14, 2025
3022520
Try Tests again
silverhadch Aug 14, 2025
6f372f0
Fix Test Failure due to missing .local/bin
silverhadch Aug 15, 2025
2a39b48
Fix Test
silverhadch Aug 15, 2025
66ecdcd
Fix Test 2
silverhadch Aug 15, 2025
c913579
Add missing asert
silverhadch Aug 15, 2025
2e8f6fc
Update unexport.go
silverhadch Aug 15, 2025
b70a94d
Update export.go
silverhadch Aug 15, 2025
534e56d
Add Test
silverhadch Aug 15, 2025
75e418e
Hot fix: Discard invalid output from the run with output.
silverhadch Aug 15, 2025
fbe46ca
Hot fix: Regression when removing.
silverhadch Aug 15, 2025
fcf28d4
Fix Tests.
silverhadch Aug 15, 2025
a276d3f
Make Unit test happy again.
silverhadch Aug 15, 2025
e1b2270
Use a pipe to temp file to cleanly capture required output for export…
silverhadch Aug 17, 2025
1e4fcdd
Make Unit-test happy... again.
silverhadch Aug 17, 2025
d2f2592
Make Unit-test happy... again... again
silverhadch Aug 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions doc/toolbox-export.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
% toolbox-export 1

## NAME
toolbox-export - Export binaries or applications from a toolbox container to your host

## SYNOPSIS
**toolbox export** [--bin _binary_] [--app _application_] --container _container_

## DESCRIPTION
The **toolbox export** command allows you to expose binaries or desktop applications from a toolbox container onto your host system. This is achieved by creating wrapper scripts for binaries in `~/.local/bin` and desktop files for applications in `~/.local/share/applications`. These exported items let you launch containerized tools seamlessly from your host environment.

## OPTIONS

**--bin _binary_**
: Export a binary from the toolbox container. The argument can be a binary name or a path inside the container.

**--app _application_**
: Export a desktop application from the toolbox container. This will search for an appropriate `.desktop` file inside the container and adapt it for host use.

**--container _container_**
: Name of the toolbox container from which the binary or application should be exported.

## EXAMPLES

Export the `vim` binary from the container named `arch`:
```
toolbox export --bin vim --container arch
```

Export the `firefox` application from the container named `fedora`:
```
toolbox export --app firefox --container fedora
```

## FILES

Exported binaries are placed in:
```
~/.local/bin/
```

Exported desktop files are placed in:
```
~/.local/share/applications/
```

## SEE ALSO
toolbox(1), toolbox-unexport(1)

## AUTHORS
Toolbox contributors
59 changes: 59 additions & 0 deletions doc/toolbox-unexport.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
% toolbox-unexport 1

## NAME
toolbox-unexport - Remove exported binaries and applications for a toolbox container

## SYNOPSIS
**toolbox unexport** --container _container_ [--bin _binary_] [--app _application_] [--all]

## DESCRIPTION
The **toolbox unexport** command removes exported binaries and/or desktop applications that were previously made available on the host from a specified toolbox container. This helps clean up your host from wrappers and desktop files created by the `toolbox export` command.

## OPTIONS

**--bin _binary_**
: Remove the exported binary wrapper for the specified binary, for the given container.

**--app _application_**
: Remove the exported desktop application for the specified app, for the given container.

**--all**
: Remove all exported binaries and applications for the specified container.

**--container _container_**
: The container whose exported binaries and applications should be removed.

## EXAMPLES

Remove the exported `vim` binary for container `arch`:
```
toolbox unexport --container arch --bin vim
```

Remove the exported `firefox` application for container `fedora`:
```
toolbox unexport --container fedora --app firefox
```

Remove all exported binaries and applications for container `arch`:
```
toolbox unexport --container arch --all
```

## FILES

Exported binaries are located in:
```
~/.local/bin/
```

Exported desktop files are located in:
```
~/.local/share/applications/
```

## SEE ALSO
toolbox(1), toolbox-export(1)

## AUTHORS
Toolbox contributors
8 changes: 8 additions & 0 deletions doc/toolbox.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ Create a new Toolbx container.

Enter a Toolbx container for interactive use.

**toolbox-export(1)**

Export binaries or desktop applications from a toolbox container to the host.

**toolbox-unexport(1)**

Remove exported binaries and applications for a toolbox container.

**toolbox-help(1)**

Display help information about Toolbx.
Expand Down
262 changes: 262 additions & 0 deletions src/cmd/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
/*
* Copyright © 2025 Hadi Chokr <[email protected]>
*
* 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 cmd

import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/containers/toolbox/pkg/utils"
"github.com/spf13/cobra"
)

var (
exportBin string
exportApp string
exportContainer string
)

var exportCmd = &cobra.Command{
Use: "export",
Short: "Export binaries or applications from a toolbox container",
RunE: runExport,
ValidArgsFunction: completionContainerNamesFiltered,
}

func init() {
exportCmd.Flags().StringVar(&exportBin, "bin", "", "Path or name of binary to export")
exportCmd.Flags().StringVar(&exportApp, "app", "", "Path or name of application to export")
exportCmd.Flags().StringVar(&exportContainer, "container", "", "Name of the toolbox container")

if err := exportCmd.RegisterFlagCompletionFunc("container", completionContainerNames); err != nil {
panic(fmt.Sprintf("failed to register flag completion function: %v", err))
}

exportCmd.SetHelpFunc(exportHelp)
rootCmd.AddCommand(exportCmd)
}

func runExport(cmd *cobra.Command, args []string) error {
if exportBin == "" && exportApp == "" {
return errors.New("must specify either --bin or --app")
}
if exportContainer == "" {
return errors.New("must specify --container")
}

if exportBin != "" {
return exportBinary(exportBin, exportContainer)
}
return exportApplication(exportApp, exportContainer)
}

func exportBinary(binName, containerName string) error {
// Ensure host export tmp dir exists
tmpDir := "/tmp/toolbox-export"
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create toolbox-export tmp dir: %v", err)
}

// Unique tmp file
tmpFile := filepath.Join(tmpDir, fmt.Sprintf("%s-%d.bin", binName, time.Now().UnixNano()))

// Run inside container, redirecting output to host file
cmd := fmt.Sprintf(
"(command -v %q || which %q || type -P %q || true) > /run/host%s",
binName, binName, binName, tmpFile,
)
if _, err := runCommandWithOutput(
containerName,
false, "", "", 0,
[]string{"sh", "--noprofile", "--norc", "-c", cmd},
false, false, true,
); err != nil {
return fmt.Errorf("failed to run command inside container: %v", err)
}

// Read back file content
content, err := os.ReadFile(tmpFile)
defer os.Remove(tmpFile)
if err != nil {
return fmt.Errorf("failed to read back exported binary path: %v", err)
}

binPath := strings.TrimSpace(string(content))
if binPath == "" {
return fmt.Errorf("binary %q not found in container", binName)
}

// Verify it's executable inside container
if _, err := runCommandWithOutput(
containerName, false, "", "", 0,
[]string{"test", "-x", binPath}, false, false, true,
); err != nil {
return fmt.Errorf("found path %q but it's not executable: %v", binPath, err)
}

// Create wrapper in ~/.local/bin
homeDir, err := os.UserHomeDir()
if err != nil {
return err
}
binDir := filepath.Join(homeDir, ".local", "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
return fmt.Errorf("failed to create bin directory: %v", err)
}

exportedBinPath := filepath.Join(binDir, binName)
script := fmt.Sprintf(`#!/bin/sh
# toolbox_binary
# name: %s
BIN_PATH="%s"
exec toolbox run -c %s "$BIN_PATH" "$@"
`, containerName, binPath, containerName)

if err := os.WriteFile(exportedBinPath, []byte(script), 0755); err != nil {
return fmt.Errorf("failed to create wrapper: %v", err)
}

fmt.Printf("Successfully exported %s from container %s to %s\n", binName, containerName, exportedBinPath)
return nil
}

func exportApplication(appName, containerName string) error {
tmpDir := "/tmp/toolbox-export"
if err := os.MkdirAll(tmpDir, 0755); err != nil {
return fmt.Errorf("failed to create toolbox-export tmp dir: %v", err)
}

// Unique tmp files
findFile := filepath.Join(tmpDir, fmt.Sprintf("%s-%d.find", appName, time.Now().UnixNano()))
catFile := filepath.Join(tmpDir, fmt.Sprintf("%s-%d.desktop", appName, time.Now().UnixNano()))

// Step 1: Run find inside container and redirect to host file
findCmd := fmt.Sprintf("find /usr/share/applications -name '%s.desktop' > /run/host%s", appName, findFile)
if _, err := runCommandWithOutput(containerName, false, "", "", 0, []string{"sh", "-c", findCmd}, false, false, true); err != nil {
return fmt.Errorf("failed to run find inside container: %v", err)
}

// Read back found desktop path
findOutput, err := os.ReadFile(findFile)
defer os.Remove(findFile)
if err != nil {
return fmt.Errorf("failed to read find output: %v", err)
}
desktopFile := strings.TrimSpace(string(findOutput))
if desktopFile == "" {
return fmt.Errorf("Error: application %s not found in container", appName)
}

// Step 2: Cat the desktop file content into host tmp
catCmd := fmt.Sprintf("cat %q > /run/host%s", desktopFile, catFile)
if _, err := runCommandWithOutput(containerName, false, "", "", 0, []string{"sh", "-c", catCmd}, false, false, true); err != nil {
return fmt.Errorf("failed to read desktop file %q: %v", desktopFile, err)
}

// Read back content
content, err := os.ReadFile(catFile)
defer os.Remove(catFile)
if err != nil {
return fmt.Errorf("failed to read desktop content: %v", err)
}
lines := strings.Split(string(content), "\n")

var newLines []string
started := false
hasNameTranslations := false

// Rewrite desktop file fields
for _, line := range lines {
if !started {
if strings.TrimSpace(line) == "[Desktop Entry]" {
started = true
newLines = append(newLines, line)
}
continue
}

if strings.HasPrefix(line, "Exec=") {
execCmd := line[5:]
line = fmt.Sprintf("Exec=toolbox run -c %s %s", containerName, execCmd)
} else if strings.HasPrefix(line, "Name=") {
line = fmt.Sprintf("Name=%s (on %s)", line[5:], containerName)
} else if strings.HasPrefix(line, "Name[") {
hasNameTranslations = true
} else if strings.HasPrefix(line, "GenericName=") {
line = fmt.Sprintf("GenericName=%s (on %s)", line[12:], containerName)
} else if strings.HasPrefix(line, "TryExec=") || line == "DBusActivatable=true" {
continue
}
newLines = append(newLines, line)
}

if hasNameTranslations {
for i, line := range newLines {
if strings.HasPrefix(line, "Name[") {
lang := line[5:strings.Index(line, "]")]
value := line[strings.Index(line, "=")+1:]
newLines[i] = fmt.Sprintf("Name[%s]=%s (on %s)", lang, value, containerName)
}
}
}

homeDir, err := os.UserHomeDir()
if err != nil {
return err
}
appsPath := filepath.Join(homeDir, ".local", "share", "applications")
exportedPath := filepath.Join(appsPath, filepath.Base(desktopFile))
exportedPath = strings.TrimSuffix(exportedPath, ".desktop") + "-" + containerName + ".desktop"

if err := os.MkdirAll(appsPath, 0755); err != nil {
return fmt.Errorf("failed to create applications directory: %v", err)
}
if err := os.WriteFile(exportedPath, []byte(strings.Join(newLines, "\n")), 0644); err != nil {
return fmt.Errorf("failed to create desktop file: %v", err)
}

exec.Command("update-desktop-database", appsPath).Run()

fmt.Printf("Successfully exported %s from container %s to %s\n", appName, containerName, exportedPath)
return nil
}

func exportHelp(cmd *cobra.Command, args []string) {
if utils.IsInsideContainer() {
if !utils.IsInsideToolboxContainer() {
fmt.Fprintf(os.Stderr, "Error: this is not a Toolbx container\n")
return
}

if _, err := utils.ForwardToHost(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}

return
}

if err := showManual("toolbox-export"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
return
}
}
Loading