github.com/singularityware/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  }