/
install.go
161 lines (146 loc) · 5.27 KB
/
install.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package kernel
import (
_ "embed"
"encoding/json"
"github.com/pkg/errors"
"k8s.io/klog/v2"
"os"
"os/exec"
"path"
"runtime"
"strings"
)
// JupyterDataDirEnv is the name of the environment variable pointing to
// the data files for Jupyter, including kernel configuration.
const JupyterDataDirEnv = "JUPYTER_DATA_DIR"
// Logo file, to install with kernel.
// See image copyright in bottom of README.md file.
//go:embed Go_gopher_favicon.svg
var logoSVG []byte
// jupyterKernelConfig is the Jupyter configuration to be
// converted to a `kernel.json` file under `~/.local/share/jupyter/kernels/gonb`
// (or `${HOME}/Library/Jupyter/kernels/` in Macs)
// See details in: https://jupyter-client.readthedocs.io/en/latest/kernels.html#kernelspecs
type jupyterKernelConfig struct {
Argv []string `json:"argv"`
DisplayName string `json:"display_name"`
Language string `json:"language"`
InterruptMode string `json:"interrupt_mode"`
Env map[string]string `json:"env"`
}
// Install gonb in users local Jupyter configuration, making it available. It assumes
// the kernel is implemented by the same binary calling this function (os.Args[0])
// and that the flag to pass the `connection_file` is `--kernel`.
//
// If the binary is under `/tmp` (or if forceCopy is true), it is copied to the location of
// the kernel configuration, and that copy is used.
//
// If forceDeps is true, installation will succeed even with missing dependencies.
//
// Documentation: https://jupyter-client.readthedocs.io/en/latest/kernels.html#kernelspecs
func Install(extraArgs []string, forceDeps, forceCopy bool) error {
gonbPath, err := os.Executable()
if err != nil {
return errors.Wrapf(err, "Failed to find path to GoNB binary")
}
config := jupyterKernelConfig{
Argv: []string{gonbPath, "--kernel", "{connection_file}"},
DisplayName: "Go (gonb)",
Language: "go",
InterruptMode: "message", // "message" (a `interrupt_request` is sent) or "signal" (using SIGINT signal)
Env: make(map[string]string),
}
if len(extraArgs) > 0 {
config.Argv = append(config.Argv, extraArgs...)
}
// Jupyter configuration directory for gonb.
home := os.Getenv("HOME")
jupyterDataDir := os.Getenv(JupyterDataDirEnv)
if jupyterDataDir == "" {
switch runtime.GOOS {
case "linux":
jupyterDataDir = path.Join(home, ".local/share/jupyter")
case "darwin":
jupyterDataDir = path.Join(home, "Library/Jupyter")
default:
return errors.Errorf("Unknown OS %q: not sure where to install GoNB kernel -- set the environment %q to force a location.", runtime.GOOS, JupyterDataDirEnv)
}
}
kernelDir := path.Join(jupyterDataDir, "/kernels/gonb")
if err := os.MkdirAll(kernelDir, 0755); err != nil {
return errors.WithMessagef(err, "failed to create configuration directory %q", kernelDir)
}
// If binary is in `/tmp` or `/var/folders`, then presumably it is a temporary compilation of Go binary,
// and we make a copy of the binary (since it will be deleted) to the configuration
// directory -- otherwise we just point to the current binary.
if forceCopy ||
strings.HasPrefix(os.Args[0], "/tmp/") ||
strings.HasPrefix(os.Args[0], "/var/folders") {
newBinary := path.Join(kernelDir, "gonb")
// Move the previous version out of the way.
if _, err := os.Stat(newBinary); err == nil {
err = os.Rename(newBinary, newBinary+"~")
if err != nil {
return errors.WithMessagef(err, "failed to rename old binary from %s to %s~", newBinary, newBinary)
}
}
err := copyFile(newBinary, os.Args[0])
if err != nil {
return errors.WithMessagef(err, "failed to copy temporary binary from %q to %q", os.Args[0], newBinary)
}
config.Argv[0] = newBinary
}
// Create kernel.json.
configPath := path.Join(kernelDir, "kernel.json")
f, err := os.Create(configPath)
if err != nil {
return errors.WithMessagef(err, "failed to create configuration file %q", configPath)
}
encoder := json.NewEncoder(f)
//encoder.SetIndent("", " ")
if err := encoder.Encode(&config); err != nil {
if err != nil {
return errors.WithMessagef(err, "failed to write configuration file %q", configPath)
}
}
if err := f.Close(); err != nil {
if err != nil {
return errors.WithMessagef(err, "failed to write configuration file %q", configPath)
}
}
klog.Infof("Go (gonb) kernel configuration installed in %q.\n", configPath)
// Create `logo-svg.svg`.
logoPath := path.Join(kernelDir, "logo-svg.svg")
err = os.WriteFile(logoPath, logoSVG, 0755)
if err != nil {
return errors.WithMessagef(err, "failed to install logo file %q", logoPath)
}
// Check that goimports and gopls are installed.
_, err = exec.LookPath("goimports")
if err == nil {
_, err = exec.LookPath("gopls")
}
if err != nil {
msg := `
Program goimports and/or gopls are not installed. They are required dependencies,
and generally are standard Go toolkit packages. You can install them with:
go install golang.org/x/tools/cmd/goimports@latest
go install golang.org/x/tools/gopls@latest
`
if !forceDeps {
klog.Fatalf(msg)
}
klog.Infof(msg)
err = nil
}
return nil
}
// copyFile, by reading all to memory -- not good for large files.
func copyFile(dst, src string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
err = os.WriteFile(dst, data, 0755)
return err
}