github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/snap/pack/pack.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package pack
    21  
    22  import (
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  
    29  	"github.com/snapcore/snapd/gadget"
    30  	"github.com/snapcore/snapd/kernel"
    31  	"github.com/snapcore/snapd/logger"
    32  	"github.com/snapcore/snapd/snap"
    33  	"github.com/snapcore/snapd/snap/snapdir"
    34  	"github.com/snapcore/snapd/snap/squashfs"
    35  )
    36  
    37  // this could be shipped as a file like "info", and save on the memory and the
    38  // overhead of creating and removing the tempfile, but on darwin we can't AFAIK
    39  // easily know where it's been placed. As it's not really that big, just
    40  // shipping it in memory seems fine for now.
    41  // from click's click.build.ClickBuilderBase, and there from
    42  // @Dpkg::Source::Package::tar_ignore_default_pattern;
    43  // changed to match squashfs's "-wildcards" syntax
    44  //
    45  // for anchored vs non-anchored syntax see RELEASE_README in squashfs-tools.
    46  const excludesContent = `
    47  # "anchored", only match at top level
    48  DEBIAN
    49  .arch-ids
    50  .arch-inventory
    51  .bzr
    52  .bzr-builddeb
    53  .bzr.backup
    54  .bzr.tags
    55  .bzrignore
    56  .cvsignore
    57  .git
    58  .gitattributes
    59  .gitignore
    60  .gitmodules
    61  .hg
    62  .hgignore
    63  .hgsigs
    64  .hgtags
    65  .shelf
    66  .svn
    67  CVS
    68  DEADJOE
    69  RCS
    70  _MTN
    71  _darcs
    72  {arch}
    73  .snapignore
    74  
    75  # "non-anchored", match anywhere
    76  ... .[#~]*
    77  ... *.snap
    78  ... *.click
    79  ... .*.sw?
    80  ... *~
    81  ... ,,*
    82  `
    83  
    84  // small helper that returns the architecture of the snap, or "multi" if it's multiple arches
    85  func debArchitecture(info *snap.Info) string {
    86  	switch len(info.Architectures) {
    87  	case 0:
    88  		return "all"
    89  	case 1:
    90  		return info.Architectures[0]
    91  	default:
    92  		return "multi"
    93  	}
    94  }
    95  
    96  // CheckSkeleton attempts to validate snap data in source directory
    97  func CheckSkeleton(w io.Writer, sourceDir string) error {
    98  	info, err := loadAndValidate(sourceDir)
    99  	if err == nil {
   100  		snap.SanitizePlugsSlots(info)
   101  		if len(info.BadInterfaces) > 0 {
   102  			fmt.Fprintln(w, snap.BadInterfacesSummary(info))
   103  		}
   104  	}
   105  	return err
   106  }
   107  
   108  func loadAndValidate(sourceDir string) (*snap.Info, error) {
   109  	// ensure we have valid content
   110  	yaml, err := ioutil.ReadFile(filepath.Join(sourceDir, "meta", "snap.yaml"))
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	info, err := snap.InfoFromSnapYaml(yaml)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	if err := snap.Validate(info); err != nil {
   121  		return nil, fmt.Errorf("cannot validate snap %q: %v", info.InstanceName(), err)
   122  	}
   123  
   124  	if err := snap.ValidateContainer(snapdir.New(sourceDir), info, logger.Noticef); err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	if info.SnapType == snap.TypeGadget {
   129  		if err := gadget.Validate(sourceDir, nil); err != nil {
   130  			return nil, err
   131  		}
   132  	}
   133  	if info.SnapType == snap.TypeKernel {
   134  		if err := kernel.Validate(sourceDir); err != nil {
   135  			return nil, err
   136  		}
   137  	}
   138  
   139  	return info, nil
   140  }
   141  
   142  func snapPath(info *snap.Info, targetDir, snapName string) string {
   143  	if snapName == "" {
   144  		snapName = fmt.Sprintf("%s_%s_%v.snap", info.InstanceName(), info.Version, debArchitecture(info))
   145  	}
   146  	if targetDir != "" && !filepath.IsAbs(snapName) {
   147  		snapName = filepath.Join(targetDir, snapName)
   148  	}
   149  	return snapName
   150  }
   151  
   152  func prepare(sourceDir, targetDir string) (*snap.Info, error) {
   153  	info, err := loadAndValidate(sourceDir)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	if targetDir != "" {
   159  		if err := os.MkdirAll(targetDir, 0755); err != nil {
   160  			return nil, err
   161  		}
   162  	}
   163  
   164  	return info, nil
   165  }
   166  
   167  func excludesFile() (filename string, err error) {
   168  	tmpf, err := ioutil.TempFile("", ".snap-pack-exclude-")
   169  	if err != nil {
   170  		return "", err
   171  	}
   172  
   173  	// inspited by ioutil.WriteFile
   174  	n, err := tmpf.Write([]byte(excludesContent))
   175  	if err == nil && n < len(excludesContent) {
   176  		err = io.ErrShortWrite
   177  	}
   178  
   179  	if err1 := tmpf.Close(); err == nil {
   180  		err = err1
   181  	}
   182  
   183  	if err == nil {
   184  		filename = tmpf.Name()
   185  	}
   186  
   187  	return filename, err
   188  }
   189  
   190  type Options struct {
   191  	// TargetDir is the direction where the snap file will be placed, or empty
   192  	// to use the current directory
   193  	TargetDir string
   194  	// SnapName is the name of the snap file, or empty to use the default name
   195  	// which is <snapname>_<version>_<architecture>.snap
   196  	SnapName string
   197  	// Compression method to use
   198  	Compression string
   199  }
   200  
   201  var Defaults *Options = nil
   202  
   203  // Snap the given sourceDirectory and return the generated
   204  // snap file
   205  func Snap(sourceDir string, opts *Options) (string, error) {
   206  	if opts == nil {
   207  		opts = &Options{}
   208  	}
   209  	switch opts.Compression {
   210  	case "xz", "lzo", "":
   211  		// fine
   212  	default:
   213  		return "", fmt.Errorf("cannot use compression %q", opts.Compression)
   214  	}
   215  
   216  	info, err := prepare(sourceDir, opts.TargetDir)
   217  	if err != nil {
   218  		return "", err
   219  	}
   220  
   221  	excludes, err := excludesFile()
   222  	if err != nil {
   223  		return "", err
   224  	}
   225  	defer os.Remove(excludes)
   226  
   227  	snapName := snapPath(info, opts.TargetDir, opts.SnapName)
   228  	d := squashfs.New(snapName)
   229  	if err = d.Build(sourceDir, &squashfs.BuildOpts{
   230  		SnapType:     string(info.Type()),
   231  		Compression:  opts.Compression,
   232  		ExcludeFiles: []string{excludes},
   233  	}); err != nil {
   234  		return "", err
   235  	}
   236  
   237  	return snapName, nil
   238  }