github.com/c9s/go@v0.0.0-20180120015821-984e81f64e0c/misc/ios/go_darwin_arm_exec.go (about)

     1  // Copyright 2015 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // This program can be used as go_darwin_arm_exec by the Go tool.
     6  // It executes binaries on an iOS device using the XCode toolchain
     7  // and the ios-deploy program: https://github.com/phonegap/ios-deploy
     8  //
     9  // This script supports an extra flag, -lldb, that pauses execution
    10  // just before the main program begins and allows the user to control
    11  // the remote lldb session. This flag is appended to the end of the
    12  // script's arguments and is not passed through to the underlying
    13  // binary.
    14  //
    15  // This script requires that three environment variables be set:
    16  // 	GOIOS_DEV_ID: The codesigning developer id or certificate identifier
    17  // 	GOIOS_APP_ID: The provisioning app id prefix. Must support wildcard app ids.
    18  // 	GOIOS_TEAM_ID: The team id that owns the app id prefix.
    19  // $GOROOT/misc/ios contains a script, detect.go, that attempts to autodetect these.
    20  package main
    21  
    22  import (
    23  	"bytes"
    24  	"errors"
    25  	"flag"
    26  	"fmt"
    27  	"go/build"
    28  	"io"
    29  	"io/ioutil"
    30  	"log"
    31  	"os"
    32  	"os/exec"
    33  	"path/filepath"
    34  	"runtime"
    35  	"strings"
    36  	"sync"
    37  	"syscall"
    38  	"time"
    39  )
    40  
    41  const debug = false
    42  
    43  var errRetry = errors.New("failed to start test harness (retry attempted)")
    44  
    45  var tmpdir string
    46  
    47  var (
    48  	devID    string
    49  	appID    string
    50  	teamID   string
    51  	bundleID string
    52  	deviceID string
    53  )
    54  
    55  // lock is a file lock to serialize iOS runs. It is global to avoid the
    56  // garbage collector finalizing it, closing the file and releasing the
    57  // lock prematurely.
    58  var lock *os.File
    59  
    60  func main() {
    61  	log.SetFlags(0)
    62  	log.SetPrefix("go_darwin_arm_exec: ")
    63  	if debug {
    64  		log.Println(strings.Join(os.Args, " "))
    65  	}
    66  	if len(os.Args) < 2 {
    67  		log.Fatal("usage: go_darwin_arm_exec a.out")
    68  	}
    69  
    70  	// e.g. B393DDEB490947F5A463FD074299B6C0AXXXXXXX
    71  	devID = getenv("GOIOS_DEV_ID")
    72  
    73  	// e.g. Z8B3JBXXXX.org.golang.sample, Z8B3JBXXXX prefix is available at
    74  	// https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
    75  	appID = getenv("GOIOS_APP_ID")
    76  
    77  	// e.g. Z8B3JBXXXX, available at
    78  	// https://developer.apple.com/membercenter/index.action#accountSummary as Team ID.
    79  	teamID = getenv("GOIOS_TEAM_ID")
    80  
    81  	// Device IDs as listed with ios-deploy -c.
    82  	deviceID = os.Getenv("GOIOS_DEVICE_ID")
    83  
    84  	parts := strings.SplitN(appID, ".", 2)
    85  	// For compatibility with the old builders, use a fallback bundle ID
    86  	bundleID = "golang.gotest"
    87  	if len(parts) == 2 {
    88  		bundleID = parts[1]
    89  	}
    90  
    91  	var err error
    92  	tmpdir, err = ioutil.TempDir("", "go_darwin_arm_exec_")
    93  	if err != nil {
    94  		log.Fatal(err)
    95  	}
    96  
    97  	// This wrapper uses complicated machinery to run iOS binaries. It
    98  	// works, but only when running one binary at a time.
    99  	// Use a file lock to make sure only one wrapper is running at a time.
   100  	//
   101  	// The lock file is never deleted, to avoid concurrent locks on distinct
   102  	// files with the same path.
   103  	lockName := filepath.Join(os.TempDir(), "go_darwin_arm_exec-"+deviceID+".lock")
   104  	lock, err = os.OpenFile(lockName, os.O_CREATE|os.O_RDONLY, 0666)
   105  	if err != nil {
   106  		log.Fatal(err)
   107  	}
   108  	if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX); err != nil {
   109  		log.Fatal(err)
   110  	}
   111  	// Approximately 1 in a 100 binaries fail to start. If it happens,
   112  	// try again. These failures happen for several reasons beyond
   113  	// our control, but all of them are safe to retry as they happen
   114  	// before lldb encounters the initial getwd breakpoint. As we
   115  	// know the tests haven't started, we are not hiding flaky tests
   116  	// with this retry.
   117  	for i := 0; i < 5; i++ {
   118  		if i > 0 {
   119  			fmt.Fprintln(os.Stderr, "start timeout, trying again")
   120  		}
   121  		err = run(os.Args[1], os.Args[2:])
   122  		if err == nil || err != errRetry {
   123  			break
   124  		}
   125  	}
   126  	if !debug {
   127  		os.RemoveAll(tmpdir)
   128  	}
   129  	if err != nil {
   130  		fmt.Fprintf(os.Stderr, "go_darwin_arm_exec: %v\n", err)
   131  		os.Exit(1)
   132  	}
   133  }
   134  
   135  func getenv(envvar string) string {
   136  	s := os.Getenv(envvar)
   137  	if s == "" {
   138  		log.Fatalf("%s not set\nrun $GOROOT/misc/ios/detect.go to attempt to autodetect", envvar)
   139  	}
   140  	return s
   141  }
   142  
   143  func run(bin string, args []string) (err error) {
   144  	appdir := filepath.Join(tmpdir, "gotest.app")
   145  	os.RemoveAll(appdir)
   146  	if err := os.MkdirAll(appdir, 0755); err != nil {
   147  		return err
   148  	}
   149  
   150  	if err := cp(filepath.Join(appdir, "gotest"), bin); err != nil {
   151  		return err
   152  	}
   153  
   154  	pkgpath, err := copyLocalData(appdir)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	entitlementsPath := filepath.Join(tmpdir, "Entitlements.plist")
   160  	if err := ioutil.WriteFile(entitlementsPath, []byte(entitlementsPlist()), 0744); err != nil {
   161  		return err
   162  	}
   163  	if err := ioutil.WriteFile(filepath.Join(appdir, "Info.plist"), []byte(infoPlist(pkgpath)), 0744); err != nil {
   164  		return err
   165  	}
   166  	if err := ioutil.WriteFile(filepath.Join(appdir, "ResourceRules.plist"), []byte(resourceRules), 0744); err != nil {
   167  		return err
   168  	}
   169  
   170  	cmd := exec.Command(
   171  		"codesign",
   172  		"-f",
   173  		"-s", devID,
   174  		"--entitlements", entitlementsPath,
   175  		appdir,
   176  	)
   177  	if debug {
   178  		log.Println(strings.Join(cmd.Args, " "))
   179  	}
   180  	cmd.Stdout = os.Stdout
   181  	cmd.Stderr = os.Stderr
   182  	if err := cmd.Run(); err != nil {
   183  		return fmt.Errorf("codesign: %v", err)
   184  	}
   185  
   186  	oldwd, err := os.Getwd()
   187  	if err != nil {
   188  		return err
   189  	}
   190  	if err := os.Chdir(filepath.Join(appdir, "..")); err != nil {
   191  		return err
   192  	}
   193  	defer os.Chdir(oldwd)
   194  
   195  	// Setting up lldb is flaky. The test binary itself runs when
   196  	// started is set to true. Everything before that is considered
   197  	// part of the setup and is retried.
   198  	started := false
   199  	defer func() {
   200  		if r := recover(); r != nil {
   201  			if w, ok := r.(waitPanic); ok {
   202  				err = w.err
   203  				if !started {
   204  					fmt.Printf("lldb setup error: %v\n", err)
   205  					err = errRetry
   206  				}
   207  				return
   208  			}
   209  			panic(r)
   210  		}
   211  	}()
   212  
   213  	defer exec.Command("killall", "ios-deploy").Run() // cleanup
   214  	exec.Command("killall", "ios-deploy").Run()
   215  
   216  	var opts options
   217  	opts, args = parseArgs(args)
   218  
   219  	// ios-deploy invokes lldb to give us a shell session with the app.
   220  	s, err := newSession(appdir, args, opts)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	defer func() {
   225  		b := s.out.Bytes()
   226  		if err == nil && !debug {
   227  			i := bytes.Index(b, []byte("(lldb) process continue"))
   228  			if i > 0 {
   229  				b = b[i:]
   230  			}
   231  		}
   232  		os.Stdout.Write(b)
   233  	}()
   234  
   235  	cond := func(out *buf) bool {
   236  		i0 := s.out.LastIndex([]byte("(lldb)"))
   237  		i1 := s.out.LastIndex([]byte("fruitstrap"))
   238  		i2 := s.out.LastIndex([]byte(" connect"))
   239  		return i0 > 0 && i1 > 0 && i2 > 0
   240  	}
   241  	if err := s.wait("lldb start", cond, 15*time.Second); err != nil {
   242  		panic(waitPanic{err})
   243  	}
   244  
   245  	// Script LLDB. Oh dear.
   246  	s.do(`process handle SIGHUP  --stop false --pass true --notify false`)
   247  	s.do(`process handle SIGPIPE --stop false --pass true --notify false`)
   248  	s.do(`process handle SIGUSR1 --stop false --pass true --notify false`)
   249  	s.do(`process handle SIGCONT --stop false --pass true --notify false`)
   250  	s.do(`process handle SIGSEGV --stop false --pass true --notify false`) // does not work
   251  	s.do(`process handle SIGBUS  --stop false --pass true --notify false`) // does not work
   252  
   253  	if opts.lldb {
   254  		_, err := io.Copy(s.in, os.Stdin)
   255  		if err != io.EOF {
   256  			return err
   257  		}
   258  		return nil
   259  	}
   260  
   261  	started = true
   262  
   263  	s.doCmd("run", "stop reason = signal SIGINT", 20*time.Second)
   264  
   265  	startTestsLen := s.out.Len()
   266  	fmt.Fprintln(s.in, `process continue`)
   267  
   268  	passed := func(out *buf) bool {
   269  		// Just to make things fun, lldb sometimes translates \n into \r\n.
   270  		return s.out.LastIndex([]byte("\nPASS\n")) > startTestsLen ||
   271  			s.out.LastIndex([]byte("\nPASS\r")) > startTestsLen ||
   272  			s.out.LastIndex([]byte("\n(lldb) PASS\n")) > startTestsLen ||
   273  			s.out.LastIndex([]byte("\n(lldb) PASS\r")) > startTestsLen ||
   274  			s.out.LastIndex([]byte("exited with status = 0 (0x00000000) \n")) > startTestsLen ||
   275  			s.out.LastIndex([]byte("exited with status = 0 (0x00000000) \r")) > startTestsLen
   276  	}
   277  	err = s.wait("test completion", passed, opts.timeout)
   278  	if passed(s.out) {
   279  		// The returned lldb error code is usually non-zero.
   280  		// We check for test success by scanning for the final
   281  		// PASS returned by the test harness, assuming the worst
   282  		// in its absence.
   283  		return nil
   284  	}
   285  	return err
   286  }
   287  
   288  type lldbSession struct {
   289  	cmd      *exec.Cmd
   290  	in       *os.File
   291  	out      *buf
   292  	timedout chan struct{}
   293  	exited   chan error
   294  }
   295  
   296  func newSession(appdir string, args []string, opts options) (*lldbSession, error) {
   297  	lldbr, in, err := os.Pipe()
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	s := &lldbSession{
   302  		in:     in,
   303  		out:    new(buf),
   304  		exited: make(chan error),
   305  	}
   306  
   307  	iosdPath, err := exec.LookPath("ios-deploy")
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	cmdArgs := []string{
   312  		// lldb tries to be clever with terminals.
   313  		// So we wrap it in script(1) and be clever
   314  		// right back at it.
   315  		"script",
   316  		"-q", "-t", "0",
   317  		"/dev/null",
   318  
   319  		iosdPath,
   320  		"--debug",
   321  		"-u",
   322  		"-r",
   323  		"-n",
   324  		`--args=` + strings.Join(args, " ") + ``,
   325  		"--bundle", appdir,
   326  	}
   327  	if deviceID != "" {
   328  		cmdArgs = append(cmdArgs, "--id", deviceID)
   329  	}
   330  	s.cmd = exec.Command(cmdArgs[0], cmdArgs[1:]...)
   331  	if debug {
   332  		log.Println(strings.Join(s.cmd.Args, " "))
   333  	}
   334  
   335  	var out io.Writer = s.out
   336  	if opts.lldb {
   337  		out = io.MultiWriter(out, os.Stderr)
   338  	}
   339  	s.cmd.Stdout = out
   340  	s.cmd.Stderr = out // everything of interest is on stderr
   341  	s.cmd.Stdin = lldbr
   342  
   343  	if err := s.cmd.Start(); err != nil {
   344  		return nil, fmt.Errorf("ios-deploy failed to start: %v", err)
   345  	}
   346  
   347  	// Manage the -test.timeout here, outside of the test. There is a lot
   348  	// of moving parts in an iOS test harness (notably lldb) that can
   349  	// swallow useful stdio or cause its own ruckus.
   350  	if opts.timeout > 1*time.Second {
   351  		s.timedout = make(chan struct{})
   352  		time.AfterFunc(opts.timeout-1*time.Second, func() {
   353  			close(s.timedout)
   354  		})
   355  	}
   356  
   357  	go func() {
   358  		s.exited <- s.cmd.Wait()
   359  	}()
   360  
   361  	return s, nil
   362  }
   363  
   364  func (s *lldbSession) do(cmd string) { s.doCmd(cmd, "(lldb)", 0) }
   365  
   366  func (s *lldbSession) doCmd(cmd string, waitFor string, extraTimeout time.Duration) {
   367  	startLen := s.out.Len()
   368  	fmt.Fprintln(s.in, cmd)
   369  	cond := func(out *buf) bool {
   370  		i := s.out.LastIndex([]byte(waitFor))
   371  		return i > startLen
   372  	}
   373  	if err := s.wait(fmt.Sprintf("running cmd %q", cmd), cond, extraTimeout); err != nil {
   374  		panic(waitPanic{err})
   375  	}
   376  }
   377  
   378  func (s *lldbSession) wait(reason string, cond func(out *buf) bool, extraTimeout time.Duration) error {
   379  	doTimeout := 2*time.Second + extraTimeout
   380  	doTimedout := time.After(doTimeout)
   381  	for {
   382  		select {
   383  		case <-s.timedout:
   384  			if p := s.cmd.Process; p != nil {
   385  				p.Kill()
   386  			}
   387  			return fmt.Errorf("test timeout (%s)", reason)
   388  		case <-doTimedout:
   389  			if p := s.cmd.Process; p != nil {
   390  				p.Kill()
   391  			}
   392  			return fmt.Errorf("command timeout (%s for %v)", reason, doTimeout)
   393  		case err := <-s.exited:
   394  			return fmt.Errorf("exited (%s: %v)", reason, err)
   395  		default:
   396  			if cond(s.out) {
   397  				return nil
   398  			}
   399  			time.Sleep(20 * time.Millisecond)
   400  		}
   401  	}
   402  }
   403  
   404  type buf struct {
   405  	mu  sync.Mutex
   406  	buf []byte
   407  }
   408  
   409  func (w *buf) Write(in []byte) (n int, err error) {
   410  	w.mu.Lock()
   411  	defer w.mu.Unlock()
   412  	w.buf = append(w.buf, in...)
   413  	return len(in), nil
   414  }
   415  
   416  func (w *buf) LastIndex(sep []byte) int {
   417  	w.mu.Lock()
   418  	defer w.mu.Unlock()
   419  	return bytes.LastIndex(w.buf, sep)
   420  }
   421  
   422  func (w *buf) Bytes() []byte {
   423  	w.mu.Lock()
   424  	defer w.mu.Unlock()
   425  
   426  	b := make([]byte, len(w.buf))
   427  	copy(b, w.buf)
   428  	return b
   429  }
   430  
   431  func (w *buf) Len() int {
   432  	w.mu.Lock()
   433  	defer w.mu.Unlock()
   434  	return len(w.buf)
   435  }
   436  
   437  type waitPanic struct {
   438  	err error
   439  }
   440  
   441  type options struct {
   442  	timeout time.Duration
   443  	lldb    bool
   444  }
   445  
   446  func parseArgs(binArgs []string) (opts options, remainingArgs []string) {
   447  	var flagArgs []string
   448  	for _, arg := range binArgs {
   449  		if strings.Contains(arg, "-test.timeout") {
   450  			flagArgs = append(flagArgs, arg)
   451  		}
   452  		if strings.Contains(arg, "-lldb") {
   453  			flagArgs = append(flagArgs, arg)
   454  			continue
   455  		}
   456  		remainingArgs = append(remainingArgs, arg)
   457  	}
   458  	f := flag.NewFlagSet("", flag.ContinueOnError)
   459  	f.DurationVar(&opts.timeout, "test.timeout", 10*time.Minute, "")
   460  	f.BoolVar(&opts.lldb, "lldb", false, "")
   461  	f.Parse(flagArgs)
   462  	return opts, remainingArgs
   463  
   464  }
   465  
   466  func copyLocalDir(dst, src string) error {
   467  	if err := os.Mkdir(dst, 0755); err != nil {
   468  		return err
   469  	}
   470  
   471  	d, err := os.Open(src)
   472  	if err != nil {
   473  		return err
   474  	}
   475  	defer d.Close()
   476  	fi, err := d.Readdir(-1)
   477  	if err != nil {
   478  		return err
   479  	}
   480  
   481  	for _, f := range fi {
   482  		if f.IsDir() {
   483  			if f.Name() == "testdata" {
   484  				if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   485  					return err
   486  				}
   487  			}
   488  			continue
   489  		}
   490  		if err := cp(dst, filepath.Join(src, f.Name())); err != nil {
   491  			return err
   492  		}
   493  	}
   494  	return nil
   495  }
   496  
   497  func cp(dst, src string) error {
   498  	out, err := exec.Command("cp", "-a", src, dst).CombinedOutput()
   499  	if err != nil {
   500  		os.Stderr.Write(out)
   501  	}
   502  	return err
   503  }
   504  
   505  func copyLocalData(dstbase string) (pkgpath string, err error) {
   506  	cwd, err := os.Getwd()
   507  	if err != nil {
   508  		return "", err
   509  	}
   510  
   511  	finalPkgpath, underGoRoot, err := subdir()
   512  	if err != nil {
   513  		return "", err
   514  	}
   515  	cwd = strings.TrimSuffix(cwd, finalPkgpath)
   516  
   517  	// Copy all immediate files and testdata directories between
   518  	// the package being tested and the source root.
   519  	pkgpath = ""
   520  	for _, element := range strings.Split(finalPkgpath, string(filepath.Separator)) {
   521  		if debug {
   522  			log.Printf("copying %s", pkgpath)
   523  		}
   524  		pkgpath = filepath.Join(pkgpath, element)
   525  		dst := filepath.Join(dstbase, pkgpath)
   526  		src := filepath.Join(cwd, pkgpath)
   527  		if err := copyLocalDir(dst, src); err != nil {
   528  			return "", err
   529  		}
   530  	}
   531  
   532  	if underGoRoot {
   533  		// Copy timezone file.
   534  		//
   535  		// Typical apps have the zoneinfo.zip in the root of their app bundle,
   536  		// read by the time package as the working directory at initialization.
   537  		// As we move the working directory to the GOROOT pkg directory, we
   538  		// install the zoneinfo.zip file in the pkgpath.
   539  		err := cp(
   540  			filepath.Join(dstbase, pkgpath),
   541  			filepath.Join(cwd, "lib", "time", "zoneinfo.zip"),
   542  		)
   543  		if err != nil {
   544  			return "", err
   545  		}
   546  		// Copy src/runtime/textflag.h for (at least) Test386EndToEnd in
   547  		// cmd/asm/internal/asm.
   548  		runtimePath := filepath.Join(dstbase, "src", "runtime")
   549  		if err := os.MkdirAll(runtimePath, 0755); err != nil {
   550  			return "", err
   551  		}
   552  		err = cp(
   553  			filepath.Join(runtimePath, "textflag.h"),
   554  			filepath.Join(cwd, "src", "runtime", "textflag.h"),
   555  		)
   556  		if err != nil {
   557  			return "", err
   558  		}
   559  	}
   560  
   561  	return finalPkgpath, nil
   562  }
   563  
   564  // subdir determines the package based on the current working directory,
   565  // and returns the path to the package source relative to $GOROOT (or $GOPATH).
   566  func subdir() (pkgpath string, underGoRoot bool, err error) {
   567  	cwd, err := os.Getwd()
   568  	if err != nil {
   569  		return "", false, err
   570  	}
   571  	if root := runtime.GOROOT(); strings.HasPrefix(cwd, root) {
   572  		subdir, err := filepath.Rel(root, cwd)
   573  		if err != nil {
   574  			return "", false, err
   575  		}
   576  		return subdir, true, nil
   577  	}
   578  
   579  	for _, p := range filepath.SplitList(build.Default.GOPATH) {
   580  		if !strings.HasPrefix(cwd, p) {
   581  			continue
   582  		}
   583  		subdir, err := filepath.Rel(p, cwd)
   584  		if err == nil {
   585  			return subdir, false, nil
   586  		}
   587  	}
   588  	return "", false, fmt.Errorf(
   589  		"working directory %q is not in either GOROOT(%q) or GOPATH(%q)",
   590  		cwd,
   591  		runtime.GOROOT(),
   592  		build.Default.GOPATH,
   593  	)
   594  }
   595  
   596  func infoPlist(pkgpath string) string {
   597  	return `<?xml version="1.0" encoding="UTF-8"?>
   598  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   599  <plist version="1.0">
   600  <dict>
   601  <key>CFBundleName</key><string>golang.gotest</string>
   602  <key>CFBundleSupportedPlatforms</key><array><string>iPhoneOS</string></array>
   603  <key>CFBundleExecutable</key><string>gotest</string>
   604  <key>CFBundleVersion</key><string>1.0</string>
   605  <key>CFBundleIdentifier</key><string>` + bundleID + `</string>
   606  <key>CFBundleResourceSpecification</key><string>ResourceRules.plist</string>
   607  <key>LSRequiresIPhoneOS</key><true/>
   608  <key>CFBundleDisplayName</key><string>gotest</string>
   609  <key>GoExecWrapperWorkingDirectory</key><string>` + pkgpath + `</string>
   610  </dict>
   611  </plist>
   612  `
   613  }
   614  
   615  func entitlementsPlist() string {
   616  	return `<?xml version="1.0" encoding="UTF-8"?>
   617  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   618  <plist version="1.0">
   619  <dict>
   620  	<key>keychain-access-groups</key>
   621  	<array><string>` + appID + `</string></array>
   622  	<key>get-task-allow</key>
   623  	<true/>
   624  	<key>application-identifier</key>
   625  	<string>` + appID + `</string>
   626  	<key>com.apple.developer.team-identifier</key>
   627  	<string>` + teamID + `</string>
   628  </dict>
   629  </plist>
   630  `
   631  }
   632  
   633  const resourceRules = `<?xml version="1.0" encoding="UTF-8"?>
   634  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   635  <plist version="1.0">
   636  <dict>
   637  	<key>rules</key>
   638  	<dict>
   639  		<key>.*</key>
   640  		<true/>
   641  		<key>Info.plist</key>
   642  		<dict>
   643  			<key>omit</key>
   644  			<true/>
   645  			<key>weight</key>
   646  			<integer>10</integer>
   647  		</dict>
   648  		<key>ResourceRules.plist</key>
   649  		<dict>
   650  			<key>omit</key>
   651  			<true/>
   652  			<key>weight</key>
   653  			<integer>100</integer>
   654  		</dict>
   655  	</dict>
   656  </dict>
   657  </plist>
   658  `