github.com/apptainer/singularity@v3.1.1+incompatible/internal/pkg/build/apps/apps.go (about) 1 // Copyright (c) 2018, Sylabs Inc. All rights reserved. 2 // This software is licensed under a 3-clause BSD license. Please consult the 3 // LICENSE.md file distributed with the URIs of this project regarding your 4 // rights to use or distribute this software. 5 6 // Package apps [apps-plugin] provides the functions which are necessary for adding SCI-F apps support 7 // to Singularity 3.0.0. In 3.1.0+, this package will be able to be built standalone as 8 // a plugin so it will be maintainable separately from the core Singularity functionality 9 package apps 10 11 import ( 12 "bytes" 13 "encoding/json" 14 "fmt" 15 "io/ioutil" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "strings" 20 "sync" 21 22 "github.com/sylabs/singularity/internal/pkg/sylog" 23 "github.com/sylabs/singularity/pkg/build/types" 24 ) 25 26 const name = "singularity_apps" 27 28 const ( 29 sectionInstall = "appinstall" 30 sectionFiles = "appfiles" 31 sectionEnv = "appenv" 32 sectionTest = "apptest" 33 sectionHelp = "apphelp" 34 sectionRun = "apprun" 35 sectionLabels = "applabels" 36 ) 37 38 var ( 39 sections = map[string]bool{ 40 sectionInstall: true, 41 sectionFiles: true, 42 sectionEnv: true, 43 sectionTest: true, 44 sectionHelp: true, 45 sectionRun: true, 46 sectionLabels: true, 47 } 48 ) 49 50 const ( 51 globalEnv94Base = `## App Global Exports For: %[1]s 52 53 SCIF_APPDATA_%[1]s=/scif/data/%[1]s 54 SCIF_APPMETA_%[1]s=/scif/apps/%[1]s/scif 55 SCIF_APPROOT_%[1]s=/scif/apps/%[1]s 56 SCIF_APPBIN_%[1]s=/scif/apps/%[1]s/bin 57 SCIF_APPLIB_%[1]s=/scif/apps/%[1]s/lib 58 59 export SCIF_APPDATA_%[1]s SCIF_APPMETA_%[1]s SCIF_APPROOT_%[1]s SCIF_APPBIN_%[1]s SCIF_APPLIB_%[1]s 60 ` 61 62 globalEnv94AppEnv = `export SCIF_APPENV_%[1]s="/scif/apps/%[1]s/scif/env/90-environment.sh" 63 ` 64 globalEnv94AppLabels = `export SCIF_APPLABELS_%[1]s="/scif/apps/%[1]s/scif/labels.json" 65 ` 66 globalEnv94AppRun = `export SCIF_APPRUN_%[1]s="/scif/apps/%[1]s/scif/runscript" 67 ` 68 69 scifEnv01Base = `#!/bin/sh 70 71 SCIF_APPNAME=%[1]s 72 SCIF_APPROOT="/scif/apps/%[1]s" 73 SCIF_APPMETA="/scif/apps/%[1]s/scif" 74 SCIF_DATA="/scif/data" 75 SCIF_APPDATA="/scif/data/%[1]s" 76 SCIF_APPINPUT="/scif/data/%[1]s/input" 77 SCIF_APPOUTPUT="/scif/data/%[1]s/output" 78 export SCIF_APPDATA SCIF_APPNAME SCIF_APPROOT SCIF_APPMETA SCIF_APPINPUT SCIF_APPOUTPUT SCIF_DATA 79 ` 80 81 scifRunscriptBase = `#!/bin/sh 82 83 %s 84 ` 85 scifTestBase = `#!/bin/sh 86 87 %s 88 ` 89 90 scifInstallBase = ` 91 cd / 92 . %[1]s/scif/env/01-base.sh 93 94 cd %[1]s 95 %[2]s 96 97 cd / 98 ` 99 ) 100 101 // App stores the deffile sections of the app 102 type App struct { 103 Name string 104 Install string 105 Files string 106 Env string 107 Test string 108 Help string 109 Run string 110 Labels string 111 } 112 113 // BuildApp is the type which the build system can use to build an app in a bundle 114 type BuildApp struct { 115 Apps map[string]*App `json:"appsDefined"` 116 sync.Mutex 117 } 118 119 // New returns a new BuildPlugin for the plugin registry to hold 120 func New() *BuildApp { 121 return &BuildApp{ 122 Apps: make(map[string]*App), 123 } 124 125 } 126 127 // Name returns this handler's name [singularity_apps] 128 func (pl *BuildApp) Name() string { 129 return name 130 } 131 132 // HandleSection receives a string of each section from the deffile 133 func (pl *BuildApp) HandleSection(ident, section string) { 134 name, sect := getAppAndSection(ident) 135 if name == "" || sect == "" { 136 return 137 } 138 139 pl.initApp(name) 140 app := pl.Apps[name] 141 142 switch sect { 143 case sectionInstall: 144 app.Install = section 145 case sectionFiles: 146 app.Files = section 147 case sectionEnv: 148 app.Env = section 149 case sectionTest: 150 app.Test = section 151 case sectionHelp: 152 app.Help = section 153 case sectionRun: 154 app.Run = section 155 case sectionLabels: 156 app.Labels = section 157 default: 158 return 159 } 160 } 161 162 func (pl *BuildApp) initApp(name string) { 163 pl.Lock() 164 defer pl.Unlock() 165 166 _, ok := pl.Apps[name] 167 if !ok { 168 pl.Apps[name] = &App{ 169 Name: name, 170 Install: "", 171 Files: "", 172 Env: "", 173 Test: "", 174 Help: "", 175 Run: "", 176 } 177 } 178 } 179 180 // getAppAndSection returns the app name and section name from the header of the section: 181 // %SECTION APP ... returns APP, SECTION 182 func getAppAndSection(ident string) (appName string, sectionName string) { 183 identSplit := strings.Split(ident, " ") 184 185 if len(identSplit) < 2 { 186 return "", "" 187 } 188 189 if _, ok := sections[identSplit[0]]; !ok { 190 return "", "" 191 } 192 193 return identSplit[1], identSplit[0] 194 } 195 196 // HandleBundle is a hook where we can modify the bundle 197 func (pl *BuildApp) HandleBundle(b *types.Bundle) { 198 if err := pl.createAllApps(b); err != nil { 199 sylog.Fatalf("Unable to create apps: %s", err) 200 } 201 } 202 203 func (pl *BuildApp) createAllApps(b *types.Bundle) error { 204 globalEnv94 := "" 205 206 for name, app := range pl.Apps { 207 sylog.Debugf("Creating %s app in bundle", name) 208 if err := createAppRoot(b, app); err != nil { 209 return err 210 } 211 212 if err := writeEnvFile(b, app); err != nil { 213 return err 214 } 215 216 if err := writeRunscriptFile(b, app); err != nil { 217 return err 218 } 219 220 if err := writeTestFile(b, app); err != nil { 221 return err 222 } 223 224 if err := writeHelpFile(b, app); err != nil { 225 return err 226 } 227 228 if err := copyFiles(b, app); err != nil { 229 return err 230 } 231 232 if err := writeLabels(b, app); err != nil { 233 return err 234 } 235 236 globalEnv94 += globalAppEnv(b, app) 237 } 238 239 return ioutil.WriteFile(filepath.Join(b.Rootfs(), "/.singularity.d/env/94-appsbase.sh"), []byte(globalEnv94), 0755) 240 } 241 242 func createAppRoot(b *types.Bundle, a *App) error { 243 if err := os.MkdirAll(appBase(b, a), 0755); err != nil { 244 return err 245 } 246 247 if err := os.MkdirAll(filepath.Join(appBase(b, a), "/scif/"), 0755); err != nil { 248 return err 249 } 250 251 if err := os.MkdirAll(filepath.Join(appBase(b, a), "/bin/"), 0755); err != nil { 252 return err 253 } 254 255 if err := os.MkdirAll(filepath.Join(appBase(b, a), "/lib/"), 0755); err != nil { 256 return err 257 } 258 259 if err := os.MkdirAll(filepath.Join(appBase(b, a), "/scif/env/"), 0755); err != nil { 260 return err 261 } 262 263 if err := os.MkdirAll(filepath.Join(appData(b, a), "/input/"), 0755); err != nil { 264 return err 265 } 266 267 if err := os.MkdirAll(filepath.Join(appData(b, a), "/output/"), 0755); err != nil { 268 return err 269 } 270 271 return nil 272 } 273 274 // %appenv and 01-base.sh 275 func writeEnvFile(b *types.Bundle, a *App) error { 276 content := fmt.Sprintf(scifEnv01Base, a.Name) 277 if err := ioutil.WriteFile(filepath.Join(appMeta(b, a), "/env/01-base.sh"), []byte(content), 0755); err != nil { 278 return err 279 } 280 281 if a.Env == "" { 282 return nil 283 } 284 285 return ioutil.WriteFile(filepath.Join(appMeta(b, a), "/env/90-environment.sh"), []byte(a.Env), 0755) 286 } 287 288 func globalAppEnv(b *types.Bundle, a *App) string { 289 content := fmt.Sprintf(globalEnv94Base, a.Name) 290 291 if _, err := os.Stat(filepath.Join(appMeta(b, a), "/env/90-environment.sh")); err == nil { 292 content += fmt.Sprintf(globalEnv94AppEnv, a.Name) 293 } 294 295 if _, err := os.Stat(filepath.Join(appMeta(b, a), "/labels.json")); err == nil { 296 content += fmt.Sprintf(globalEnv94AppLabels, a.Name) 297 } 298 299 if _, err := os.Stat(filepath.Join(appMeta(b, a), "/runscript")); err == nil { 300 content += fmt.Sprintf(globalEnv94AppRun, a.Name) 301 } 302 303 return content 304 } 305 306 // %apprun 307 func writeRunscriptFile(b *types.Bundle, a *App) error { 308 if a.Run == "" { 309 return nil 310 } 311 312 content := fmt.Sprintf(scifRunscriptBase, a.Run) 313 return ioutil.WriteFile(filepath.Join(appMeta(b, a), "/runscript"), []byte(content), 0755) 314 } 315 316 // %apptest 317 func writeTestFile(b *types.Bundle, a *App) error { 318 if a.Test == "" { 319 return nil 320 } 321 322 content := fmt.Sprintf(scifTestBase, a.Test) 323 return ioutil.WriteFile(filepath.Join(appMeta(b, a), "/test"), []byte(content), 0755) 324 } 325 326 // %apphelp 327 func writeHelpFile(b *types.Bundle, a *App) error { 328 if a.Help == "" { 329 return nil 330 } 331 332 return ioutil.WriteFile(filepath.Join(appMeta(b, a), "/runscript.help"), []byte(a.Help), 0644) 333 } 334 335 // %appfile 336 func copyFiles(b *types.Bundle, a *App) error { 337 if a.Files == "" { 338 return nil 339 } 340 341 appBase := filepath.Join(b.Rootfs(), "/scif/apps/", a.Name) 342 for _, line := range strings.Split(a.Files, "\n") { 343 344 // skip empty or comment lines 345 if line = strings.TrimSpace(line); line == "" || strings.Index(line, "#") == 0 { 346 continue 347 } 348 349 // trim any comments and whitespace 350 trimLine := strings.Split(strings.TrimSpace(line), "#")[0] 351 splitLine := strings.SplitN(strings.TrimSpace(trimLine), " ", 2) 352 353 // copy to dst of same name in app if no dst is specified 354 var src, dst string 355 if len(splitLine) < 2 { 356 src = splitLine[0] 357 dst = splitLine[0] 358 } else { 359 src = splitLine[0] 360 dst = splitLine[1] 361 } 362 363 if err := copy(src, filepath.Join(appBase, dst)); err != nil { 364 return err 365 } 366 } 367 368 return nil 369 } 370 371 // %applabels 372 func writeLabels(b *types.Bundle, a *App) error { 373 lines := strings.Split(strings.TrimSpace(a.Labels), "\n") 374 labels := make(map[string]string) 375 376 // add default label 377 labels["SCIF_APP_NAME"] = a.Name 378 379 for _, line := range lines { 380 381 // skip empty or comment lines 382 if line = strings.TrimSpace(line); line == "" || strings.Index(line, "#") == 0 { 383 continue 384 } 385 var key, val string 386 lineSubs := strings.SplitN(line, " ", 2) 387 if len(lineSubs) < 2 { 388 key = strings.TrimSpace(lineSubs[0]) 389 val = "" 390 } else { 391 key = strings.TrimSpace(lineSubs[0]) 392 val = strings.TrimSpace(lineSubs[1]) 393 } 394 395 labels[key] = val 396 } 397 398 // make new map into json 399 text, err := json.MarshalIndent(labels, "", "\t") 400 if err != nil { 401 return err 402 } 403 404 appBase := filepath.Join(b.Rootfs(), "/scif/apps/", a.Name) 405 err = ioutil.WriteFile(filepath.Join(appBase, "scif/labels.json"), text, 0644) 406 return err 407 } 408 409 //util funcs 410 411 func appBase(b *types.Bundle, a *App) string { 412 return filepath.Join(b.Rootfs(), "/scif/apps/", a.Name) 413 } 414 415 func appMeta(b *types.Bundle, a *App) string { 416 return filepath.Join(appBase(b, a), "/scif/") 417 } 418 419 func appData(b *types.Bundle, a *App) string { 420 return filepath.Join(b.Rootfs(), "/scif/data/", a.Name) 421 } 422 423 func copy(src, dst string) error { 424 var stderr bytes.Buffer 425 copy := exec.Command("cp", "-fLr", src, dst) 426 copy.Stderr = &stderr 427 sylog.Debugf("Copying %v to %v", src, dst) 428 if err := copy.Run(); err != nil { 429 return fmt.Errorf("While copying %v to %v: %v: %v", src, dst, err, stderr.String()) 430 } 431 432 return nil 433 } 434 435 // HandlePost returns a script that should run after %post 436 func (pl *BuildApp) HandlePost() string { 437 post := "" 438 for name, app := range pl.Apps { 439 sylog.Debugf("Building app[%s] post script section", name) 440 441 post += buildPost(app) 442 } 443 444 return post 445 } 446 447 func buildPost(a *App) string { 448 return fmt.Sprintf(scifInstallBase, filepath.Join("/scif/apps/", a.Name), a.Install) 449 }