github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/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  		// TODO:UC20: optionally pass model
   130  		// TODO:UC20: pass validation constraints which indicate intent
   131  		//            to have data encrypted
   132  		ginfo, err := gadget.ReadInfoAndValidate(sourceDir, nil, nil)
   133  		if err != nil {
   134  			return nil, err
   135  		}
   136  		if err := gadget.ValidateContent(ginfo, sourceDir, ""); err != nil {
   137  			return nil, err
   138  		}
   139  	}
   140  	if info.SnapType == snap.TypeKernel {
   141  		if err := kernel.Validate(sourceDir); err != nil {
   142  			return nil, err
   143  		}
   144  	}
   145  
   146  	return info, nil
   147  }
   148  
   149  func snapPath(info *snap.Info, targetDir, snapName string) string {
   150  	if snapName == "" {
   151  		snapName = fmt.Sprintf("%s_%s_%v.snap", info.InstanceName(), info.Version, debArchitecture(info))
   152  	}
   153  	if targetDir != "" && !filepath.IsAbs(snapName) {
   154  		snapName = filepath.Join(targetDir, snapName)
   155  	}
   156  	return snapName
   157  }
   158  
   159  func prepare(sourceDir, targetDir string) (*snap.Info, error) {
   160  	info, err := loadAndValidate(sourceDir)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	if targetDir != "" {
   166  		if err := os.MkdirAll(targetDir, 0755); err != nil {
   167  			return nil, err
   168  		}
   169  	}
   170  
   171  	return info, nil
   172  }
   173  
   174  func excludesFile() (filename string, err error) {
   175  	tmpf, err := ioutil.TempFile("", ".snap-pack-exclude-")
   176  	if err != nil {
   177  		return "", err
   178  	}
   179  
   180  	// inspited by ioutil.WriteFile
   181  	n, err := tmpf.Write([]byte(excludesContent))
   182  	if err == nil && n < len(excludesContent) {
   183  		err = io.ErrShortWrite
   184  	}
   185  
   186  	if err1 := tmpf.Close(); err == nil {
   187  		err = err1
   188  	}
   189  
   190  	if err == nil {
   191  		filename = tmpf.Name()
   192  	}
   193  
   194  	return filename, err
   195  }
   196  
   197  type Options struct {
   198  	// TargetDir is the direction where the snap file will be placed, or empty
   199  	// to use the current directory
   200  	TargetDir string
   201  	// SnapName is the name of the snap file, or empty to use the default name
   202  	// which is <snapname>_<version>_<architecture>.snap
   203  	SnapName string
   204  	// Compression method to use
   205  	Compression string
   206  }
   207  
   208  var Defaults *Options = nil
   209  
   210  // Snap the given sourceDirectory and return the generated
   211  // snap file
   212  func Snap(sourceDir string, opts *Options) (string, error) {
   213  	if opts == nil {
   214  		opts = &Options{}
   215  	}
   216  	switch opts.Compression {
   217  	case "xz", "lzo", "":
   218  		// fine
   219  	default:
   220  		return "", fmt.Errorf("cannot use compression %q", opts.Compression)
   221  	}
   222  
   223  	info, err := prepare(sourceDir, opts.TargetDir)
   224  	if err != nil {
   225  		return "", err
   226  	}
   227  
   228  	excludes, err := excludesFile()
   229  	if err != nil {
   230  		return "", err
   231  	}
   232  	defer os.Remove(excludes)
   233  
   234  	snapName := snapPath(info, opts.TargetDir, opts.SnapName)
   235  	d := squashfs.New(snapName)
   236  	if err = d.Build(sourceDir, &squashfs.BuildOpts{
   237  		SnapType:     string(info.Type()),
   238  		Compression:  opts.Compression,
   239  		ExcludeFiles: []string{excludes},
   240  	}); err != nil {
   241  		return "", err
   242  	}
   243  
   244  	return snapName, nil
   245  }