├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── ciel │ ├── build.go │ ├── ciel.go │ ├── doc.go │ ├── doctor.go │ ├── flags.go │ ├── guest_os.go │ ├── instances.go │ ├── mount_points.go │ ├── plugins.go │ ├── status.go │ └── tree.go ├── completions └── ciel ├── config └── config.go ├── display ├── input.go └── output.go ├── go.mod ├── go.sum ├── internal ├── abstract │ └── abstract.go ├── ciel │ └── cieldir.go ├── container │ ├── container.go │ ├── filesystem │ │ └── filesystem.go │ └── instance │ │ ├── instance.go │ │ └── lowlevel.go ├── packaging │ ├── config.go │ ├── output.go │ └── toolchain.go ├── pkgtree │ ├── bind.go │ ├── callbacks.go │ └── clone.go └── utils │ └── utils.go ├── ipc └── lowlevel.go ├── overlayfs ├── ciel.go ├── merge.go └── overlayfs.go ├── plugin ├── ciel-generate ├── ciel-localrepo ├── ciel-release └── ciel-switchmirror ├── proc-api └── proc.go └── systemd-api ├── dbus.go ├── machined ├── dbus.go ├── machine.go └── manager.go └── nspawn ├── api.go ├── backport.go ├── helper.go ├── structs.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | /bin 3 | 4 | # Build cache 5 | /workdir/ 6 | /instdir/ 7 | /config.go 8 | 9 | # Vendor packages 10 | /vendor/ 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 19 | .glide/ 20 | /glide.lock 21 | 22 | # Post-sed constants 23 | version.go 24 | 25 | .idea 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: required 3 | go: 4 | - 1.11.x 5 | - 1.13.x 6 | - tip 7 | os: 8 | - linux 9 | matrix: 10 | allow_failures: 11 | - go: tip 12 | fast_finish: true 13 | script: 14 | - make 15 | - sudo make install 16 | - ciel version 17 | - ciel help 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ciel 2 | 3 | Welcome! 4 | 5 | You may use the following shell commands to get the lastest code. 6 | ``` 7 | git clone https://github.com/AOSC-Dev/ciel 8 | git submodule update --init 9 | ``` 10 | 11 | If you want to make changes to anything in `/src/ciel-driver`, it is at repository https://github.com/AOSC-Dev/ciel-driver. 12 | 13 | ## Files & Directories 14 | 15 | Plugins are placed at `/plugin`, this directory will be copied to `/usr/libexec/ciel-plugin` when you do `make install`. 16 | 17 | You MUST keep the plugin executable, `chmod a+x ciel-myplugin`. 18 | 19 | A plugin could be a shell script, a python script, a binary... 20 | 21 | Use `./format.sh` to format your code BEFORE committing, I would suggest you to make a symbol link at `/.git/hook/pre_commit` to `format.sh`. 22 | 23 | 24 | ## Coding Style 25 | 26 | Keep it simple and readable. Come on, you can do it right. 27 | 28 | But, again, **don't forget to use `./format.sh` after coding things.** :) 29 | 30 | ## Commit Message 31 | 32 | ``` 33 | about_something: do what 34 | or 35 | about_something: do what; fix #123 36 | ``` 37 | 38 | You may check the `git log --oneline` out, there will be many examples for you. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 AOSC-Dev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJ=ciel 2 | ORG_PATH=github.com/AOSC-Dev 3 | REPO_PATH=$(ORG_PATH)/$(PROJ) 4 | PREFIX:=/usr/local 5 | CC:=/usr/bin/cc 6 | CXX:=/usr/bin/c++ 7 | 8 | VERSION=$(shell git describe --tags) 9 | SRCDIR=$(shell pwd) 10 | 11 | export GOPATH=$(SRCDIR)/workdir 12 | export CC 13 | export CXX 14 | 15 | ARCH:=$(shell uname -m) 16 | 17 | GOSRC=$(GOPATH)/src 18 | GOBIN=$(GOPATH)/bin 19 | CIELPATH=$(GOSRC)/ciel 20 | LD_FLAGS="-w -X $(REPO_PATH)/config.Version=$(VERSION) -X $(REPO_PATH)/config.Prefix=$(PREFIX)" 21 | DISTDIR=$(SRCDIR)/instdir 22 | 23 | all: build 24 | 25 | $(CIELPATH): 26 | mkdir -p $(DISTDIR)/bin 27 | mkdir -p $(DISTDIR)/libexec/ciel-plugin 28 | mkdir -p $(GOSRC) 29 | mkdir -p $(GOBIN) 30 | ln -f -s -T $(SRCDIR) $(CIELPATH) 31 | 32 | deps: $(CIELPATH) $(SRCDIR)/go.mod $(SRCDIR)/go.sum 33 | go mod vendor 34 | 35 | $(DISTDIR)/bin/ciel: deps 36 | export CC 37 | export CXX 38 | go build -o $@ -ldflags $(LD_FLAGS) $(REPO_PATH)/cmd/ciel 39 | 40 | plugin: plugin/* 41 | cp -fR $^ $(DISTDIR)/libexec/ciel-plugin 42 | 43 | build: $(DISTDIR)/bin/ciel plugin 44 | 45 | clean: 46 | rm -rf $(GOPATH) 47 | rm -rf $(DISTDIR) 48 | rm -rf $(SRCDIR)/vendor 49 | git clean -f -d $(SRCDIR) 50 | 51 | install: 52 | mkdir -p $(DESTDIR)/$(PREFIX) 53 | cp -R $(DISTDIR)/* $(DESTDIR)/$(PREFIX) 54 | 55 | .PHONY: all deps build plugin install clean 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ciel 2 [![Build Status](https://api.travis-ci.org/AOSC-Dev/ciel.svg)](https://travis-ci.org/AOSC-Dev/ciel) 2 | An **integrated packaging environment** for AOSC OS. 3 | 4 | **Ciel** /sjɛl/ uses *systemd-nspawn* container as its backend and *overlay* file system as support rollback feature. 5 | 6 | ## Manual 7 | 8 | ```bash 9 | ciel help 10 | ``` 11 | 12 | ## Installation 13 | 14 | ```bash 15 | make 16 | sudo make install 17 | ``` 18 | 19 | You may use `make PREFIX=/usr` and `sudo make install PREFIX=/usr` to install to other location. Defaults to `/usr/local`. 20 | 21 | ## Dependencies 22 | 23 | Building: 24 | - Go 25 | - C compiler 26 | - make 27 | - curl 28 | 29 | Runtime: 30 | - Systemd 31 | - tar 32 | - dos2unix 33 | 34 | Runtime Kernel: 35 | - Overlay file system 36 | - System-V semaphores 37 | -------------------------------------------------------------------------------- /cmd/ciel/build.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | "syscall" 13 | 14 | d "github.com/AOSC-Dev/ciel/display" 15 | "github.com/AOSC-Dev/ciel/internal/ciel" 16 | "github.com/AOSC-Dev/ciel/internal/container/instance" 17 | "github.com/AOSC-Dev/ciel/internal/packaging" 18 | "github.com/AOSC-Dev/ciel/internal/pkgtree" 19 | ) 20 | 21 | func buildConfig() { 22 | basePath := flagCielDir() 23 | instName := flagInstance() 24 | batch := flagBatch() 25 | var global = false 26 | flag.BoolVar(&global, "g", global, "global, configure for underlying OS") 27 | parse() 28 | 29 | i := &ciel.Ciel{BasePath: *basePath} 30 | i.Check() 31 | c := i.Container() 32 | 33 | var inst *instance.Instance 34 | 35 | if !global { 36 | c.CheckInst(*instName) 37 | inst = c.Instance(*instName) 38 | inst.Unmount() 39 | inst.MountLocal() 40 | defer func() { 41 | inst.Unmount() 42 | }() 43 | } 44 | 45 | suffix := " of UNDERLYING OS" 46 | if !global { 47 | suffix = "" 48 | } 49 | 50 | tc := packaging.DetectToolChain(global, inst, c) 51 | if tc.ACBS { 52 | packaging.SetTreePath(global, inst, c, pkgtree.TreePath) 53 | } 54 | 55 | var person string 56 | if tc.AB { 57 | if !*batch { 58 | for person == "" { 59 | person = d.ASK("Maintainer Info"+suffix, "Foo Bar ") 60 | } 61 | } else { 62 | person = "Bot " 63 | } 64 | packaging.SetMaintainer(global, inst, c, person) 65 | } 66 | 67 | if *batch || d.ASKLower("Would you like to disable DNSSEC feature"+suffix+"?", "yes/no") == "yes" { 68 | packaging.DisableDNSSEC(global, inst, c) 69 | } 70 | 71 | if !*batch && d.ASKLower("Would you like to edit sources.list"+suffix+"?", "yes/no") == "yes" { 72 | packaging.EditSourceList(global, inst, c) 73 | } 74 | 75 | if !*batch && d.ASKLower("Do you want to enable local packages repository?", "yes/no") == "yes" { 76 | packaging.InitLocalRepo(global, inst, c) 77 | // add the key to the APT trust store 78 | d.ITEM("create and import gpg keys") 79 | var prefix string 80 | if global { 81 | prefix = c.DistDir() 82 | } else { 83 | prefix = inst.MountPoint() 84 | } 85 | exitStatus := refreshLocalRepo(prefix, true) 86 | if exitStatus == 0 { 87 | d.OK() 88 | } else { 89 | d.FAILED_BECAUSE(fmt.Sprintf("Script exited with status %d", exitStatus)) 90 | } 91 | d.ITEM("bootstrap local repository") 92 | prefix = path.Join(c.GetCiel().GetBasePath(), "/OUTPUT/debs") 93 | exitStatus = refreshLocalRepo(prefix, false) 94 | if exitStatus == 0 { 95 | d.OK() 96 | } else { 97 | d.FAILED_BECAUSE(fmt.Sprintf("2nd stage script exited with status %d", exitStatus)) 98 | } 99 | } else { 100 | packaging.UnInitLocalRepo(global, inst, c) 101 | } 102 | } 103 | 104 | func refreshLocalRepo(debsDir string, firstRun bool) int { 105 | proc := filepath.Join(PluginDir, PluginPrefix+"localrepo") 106 | cmd := exec.Command(proc, debsDir) 107 | if firstRun { 108 | cmd.Env = []string{"CIEL_LR_FIRST=1"} 109 | } 110 | cmd.Stdout = nil 111 | cmd.Stderr = nil 112 | err := cmd.Run() 113 | if exitErr, ok := err.(*exec.ExitError); ok { 114 | return exitErr.Sys().(syscall.WaitStatus).ExitStatus() 115 | } 116 | if err != nil { 117 | log.Fatalln(err) 118 | } 119 | return 0 120 | } 121 | 122 | func build() { 123 | basePath := flagCielDir() 124 | instName := flagInstance() 125 | networkFlag := flagNetwork() 126 | noBooting := flagNoBooting() 127 | usingLocalRepo := false 128 | parse() 129 | 130 | i := &ciel.Ciel{BasePath: *basePath} 131 | i.Check() 132 | c := i.Container() 133 | c.CheckInst(*instName) 134 | inst := c.Instance(*instName) 135 | inst.Mount() 136 | 137 | debsDir := path.Join(i.GetBasePath(), "OUTPUT", "debs") 138 | dir, err := os.Getwd() 139 | debsDirTarget := path.Join(dir, inst.MountPoint(), "debs") 140 | if err != nil { 141 | log.Fatalln(err) 142 | } 143 | err = os.MkdirAll(debsDir, 0755) 144 | if err != nil { 145 | log.Fatalln(err) 146 | } 147 | err = os.MkdirAll(debsDirTarget, 0755) 148 | if err != nil { 149 | log.Fatalln(err) 150 | } 151 | err = syscall.Mount(debsDir, debsDirTarget, "", syscall.MS_BIND, "") 152 | if err != nil { 153 | log.Fatalln(err) 154 | } 155 | aptConfigPath := path.Join(inst.MountPoint(), packaging.DefaultRepoConfig) 156 | if _, err = os.Stat(aptConfigPath); err == nil { 157 | usingLocalRepo = true 158 | } 159 | if _, err := os.Stat(path.Join(debsDirTarget, "InRelease")); err != nil && usingLocalRepo { 160 | refreshLocalRepo(debsDir, false) 161 | } 162 | 163 | cmd := `acbs-build ` + strings.Join(flag.Args(), " ") 164 | 165 | exitStatus, err := _shellRun( 166 | inst, 167 | *networkFlag, 168 | !*noBooting, 169 | cmd, 170 | ) 171 | if err != nil { 172 | log.Println(err) 173 | } 174 | if exitStatus != 0 { 175 | os.Exit(exitStatus) 176 | } 177 | 178 | err = syscall.Unmount(debsDirTarget, 0) 179 | if err != nil { 180 | log.Fatalln(err) 181 | } 182 | 183 | if usingLocalRepo { 184 | d.Println(d.C0(d.WHITE, "Refreshing local repository... ")) 185 | refreshLocalRepo(debsDir, false) 186 | } 187 | //cmd = exec.Command("sh", "-c", "cp -p "+inst.MountPoint()+"/var/log/apt/history.log OUTPUT/") 188 | //cmd.Stderr = os.Stderr 189 | //err = cmd.Run() 190 | //if exitErr, ok := err.(*exec.ExitError); ok { 191 | // os.Exit(exitErr.Sys().(syscall.WaitStatus).ExitStatus()) 192 | //} 193 | //if err != nil { 194 | // log.Fatalln(err) 195 | //} 196 | 197 | // TODO: collect information 198 | } 199 | -------------------------------------------------------------------------------- /cmd/ciel/ciel.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | //go:generate ./generate.sh 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | "github.com/AOSC-Dev/ciel/config" 14 | d "github.com/AOSC-Dev/ciel/display" 15 | "github.com/AOSC-Dev/ciel/internal/ciel" 16 | ) 17 | 18 | var rawArgs []string 19 | 20 | func main() { 21 | log.SetFlags(log.LstdFlags | log.Lshortfile) 22 | var subCmd string 23 | if len(os.Args) >= 2 { 24 | if strings.HasPrefix(os.Args[1], "-") { 25 | rawArgs = os.Args[1:] 26 | } else { 27 | subCmd = os.Args[1] 28 | rawArgs = os.Args[2:] 29 | } 30 | } else { 31 | rawArgs = nil 32 | } 33 | flag.CommandLine = flag.NewFlagSet(os.Args[0]+" "+subCmd, flag.ExitOnError) 34 | router(subCmd) 35 | } 36 | 37 | func parse() { 38 | flag.CommandLine.Parse(rawArgs) 39 | } 40 | 41 | func router(subCmd string) { 42 | switch subCmd { 43 | case "version": 44 | fmt.Println("Ciel", config.Version) 45 | return 46 | case "help": 47 | docHelp() 48 | return 49 | } 50 | var routeTable = map[string]func(){ 51 | // Create Directory Structures 52 | "init": initCiel, // here 53 | "farewell": farewell, // here 54 | 55 | // Preparing and Removing Instance 56 | "add": add, // instances.go 57 | "load-os": untarGuestOS, // guest_os.go 58 | "factory-reset": factoryReset, // guest_os.go 59 | "update-os": update, // guest_os.go 60 | "rollback": rollback, // instances.go 61 | "commit": commit, // instances.go 62 | "del": del, // instances.go 63 | 64 | // Maintaining Instance Status 65 | "mount": mountCiel, // mount_points.go 66 | "stop": stop, // instances.go 67 | "down": shutdown, // mount_points.go 68 | "list": list, // status.go 69 | "": list, // status.go 70 | 71 | // Executing Commands 72 | "shell": shell, // instances.go 73 | "run": run, // instances.go 74 | 75 | // Building Configuration 76 | "load-tree": clone, // tree.go 77 | "update-tree": pull, // tree.go 78 | "config": buildConfig, // build.go 79 | 80 | // Building 81 | "build": build, // build.go 82 | 83 | "doctor": doctor, // doctor.go 84 | } 85 | requireEUID0() 86 | route, exists := routeTable[subCmd] 87 | if exists { 88 | route() 89 | return 90 | } 91 | plugins := getPlugins() 92 | for _, pluginItem := range plugins { 93 | if subCmd == pluginItem.Name { 94 | plugin(subCmd) 95 | return 96 | } 97 | } 98 | log.Fatalln("unknown command: " + subCmd) 99 | } 100 | 101 | func requireEUID0() { 102 | if os.Geteuid() != 0 { 103 | log.Fatalln("need to be root") 104 | } 105 | } 106 | 107 | func initCiel() { 108 | basePath := flagCielDir() 109 | parse() 110 | 111 | i := &ciel.Ciel{BasePath: *basePath} 112 | i.Init() 113 | } 114 | 115 | func farewell() { 116 | basePath := flagCielDir() 117 | batchFlag := flagBatch() 118 | parse() 119 | 120 | i := &ciel.Ciel{BasePath: *basePath} 121 | i.Check() 122 | c := i.Container() 123 | 124 | if !*batchFlag && d.ASKLower("DELETE ALL CIEL THINGS?", "yes/no") != "yes" { 125 | os.Exit(1) 126 | } 127 | 128 | d.SECTION("Farewell To Thee (Good Bye)") 129 | instList := c.GetAll() 130 | for _, inst := range instList { 131 | d.SECTION("Shutdown Instance " + inst.Name) 132 | inst.Unmount() 133 | } 134 | d.SECTION("DELETE .ciel DIRECTORY") 135 | d.ITEM("delete") 136 | err := os.RemoveAll(i.CielDir()) 137 | d.ERR(err) 138 | } 139 | -------------------------------------------------------------------------------- /cmd/ciel/doc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func docHelp() { 6 | fmt.Print(`Usage: 7 | ciel version 8 | ciel init 9 | ciel load-os [TAR_FILE] // unpack OS tarball or fetch the latest BuildKit from internet directly 10 | ciel load-tree [GIT_URL] // clone package tree from your link or AOSC OS ABBS at GitHub 11 | 12 | ciel update-os -- [params] // similar to 'apt-get update && apt-get dist-upgrade', params are appended to 'apt-get dist-upgrade' 13 | ciel update-tree // similar to 'git pull' 14 | 15 | 16 | ciel [list] 17 | ciel add INSTANCE 18 | ciel del INSTANCE 19 | ciel shell -i INSTANCE // start an interactive shell 20 | ciel shell -i INSTANCE "SHELL COMMAND LINE" 21 | ciel config (-i INSTANCE | -g) // configure system and toolchain for building (interactively) 22 | ciel build -i INSTANCE PACKAGE 23 | ciel rollback -i INSTANCE 24 | 25 | ciel down [-i INSTANCE] // shutdown & unmount all or one instance 26 | ciel mount [-i INSTANCE] // mount all or one instance 27 | 28 | Rarely used: 29 | ciel stop -i INSTANCE // shutdown an instance 30 | ciel run -i INSTANCE ABSPATH_TO_EXE ARG1 ARG2 ... 31 | // lower-level version of 'shell', without login environment, 32 | // without sourcing ~/.bash_profile 33 | ciel farewell // DELETE ALL CIEL THINGS, except OUTPUT, TREE etc. 34 | // equals to 'ciel down && rm -r .ciel' 35 | 36 | ciel doctor // diagnose problems 37 | 38 | Altering OS & Releasing OS: 39 | ciel load-os 40 | ciel update-os // see above 41 | ciel generate 42 | // (plugin) install packages and set up environment by RECIPE 43 | ciel factory-reset -i INSTANCE 44 | // delete all out-of-dpkg files 45 | ciel commit -i INSTANCE 46 | // commit changes onto the shared underlying OS 47 | ciel release VARIANT THREADS 48 | // (plugin) make a .tar.xz release for the underlying OS 49 | 50 | Global flags: 51 | -C CIEL_DIR // use CIEL_DIR as workdir instead of current directory 52 | -i INSTANCE // specify the INSTANCE to manipulate 53 | -batch // batch mode, no input is required 54 | -n // do not start 'init' (systemd) 55 | `) 56 | } 57 | -------------------------------------------------------------------------------- /cmd/ciel/doctor.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | 7 | d "github.com/AOSC-Dev/ciel/display" 8 | "github.com/AOSC-Dev/ciel/internal/ciel" 9 | "github.com/AOSC-Dev/ciel/internal/container/instance" 10 | "github.com/AOSC-Dev/ciel/systemd-api/nspawn" 11 | ) 12 | 13 | func doctor() { 14 | basePath := flagCielDir() 15 | instName := flagInstance() 16 | parse() 17 | 18 | i := &ciel.Ciel{BasePath: *basePath} 19 | i.Check() 20 | c := i.Container() 21 | 22 | var instList []*instance.Instance 23 | if *instName == "" { 24 | instList = c.GetAll() 25 | } else { 26 | c.CheckInst(*instName) 27 | instList = []*instance.Instance{c.Instance(*instName)} 28 | } 29 | for _, inst := range instList { 30 | d.SECTION("Status of " + inst.Name) 31 | 32 | d.ITEM("SEMAPHORES") 33 | fl, flErr := inst.FileSystemLock().Get() 34 | rl, rlErr := inst.RunLock().Get() 35 | flName := "file@" + strconv.FormatUint(uint64(inst.FileSystemLock().S), 16) 36 | rlName := "run@" + strconv.FormatUint(uint64(inst.RunLock().S), 16) 37 | if flErr == nil && rlErr == nil && fl <= 1 && rl <= 1 { 38 | d.Print(d.C0(switchColor(fl == 0), flName)) 39 | d.Print(" ") 40 | d.Print(d.C0(switchColor(rl == 0), rlName)) 41 | d.Println() 42 | } else { 43 | d.Println() 44 | d.ITEM(flName) 45 | if flErr != nil { 46 | d.Println(d.C(d.RED, flErr.Error())) 47 | } else { 48 | d.Println(d.C(d.RED, strconv.Itoa(fl))) 49 | } 50 | d.ITEM(rlName) 51 | if rlErr != nil { 52 | d.Println(d.C(d.RED, rlErr.Error())) 53 | } else { 54 | d.Println(d.C(d.RED, strconv.Itoa(rl))) 55 | } 56 | } 57 | 58 | d.ITEM("OVERLAYFS") 59 | d.Println(d.C0(switchColor(inst.Mounted()), "mounted")) 60 | 61 | d.ITEM("CONTAINER") 62 | d.Print(d.C0(switchColor(inst.Running()), "running")) 63 | d.Print(" ") 64 | d.Print(d.C0(switchColor(inst.RunningAsBootMode()), "bootMode")) 65 | d.Print(" ") 66 | d.Print(d.C0(switchColor(inst.RunningAsExclusiveMode()), "simpleMode")) 67 | d.Println() 68 | 69 | d.ITEM("SYSTEMD") 70 | st := nspawn.MachineStatus(context.Background(), inst.MachineId()) 71 | d.Print(d.C0(switchColor(nspawn.MachineDead(st)), "dead")) 72 | d.Print(" ") 73 | d.Print(d.C0(switchColor(nspawn.MachineRunning(st)), "running")) 74 | d.Println() 75 | d.ITEM("(raw str)") 76 | lim := len(st) - 1 - 20 77 | pf := "..." 78 | if lim < 0 { 79 | lim = 0 80 | pf = "" 81 | } 82 | d.Println(d.C(d.WHITE, pf+st[lim:])) 83 | } 84 | } 85 | 86 | //func checkUname() { 87 | // uname := syscall.Utsname{} 88 | // err := syscall.Uname(&uname) 89 | //} 90 | 91 | func switchColor(b bool) d.Color { 92 | if b { 93 | return d.CYAN 94 | } 95 | return d.WHITE 96 | } 97 | -------------------------------------------------------------------------------- /cmd/ciel/flags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | "github.com/AOSC-Dev/ciel/systemd-api/nspawn" 8 | ) 9 | 10 | func flagCielDir() *string { 11 | basePath := getEnv("CIEL_DIR", ".") 12 | flag.StringVar(&basePath, "C", basePath, "Ciel work `directory`; CIEL_DIR") 13 | return &basePath 14 | } 15 | func saveCielDir(basePath string) { 16 | saveEnv("CIEL_DIR", basePath) 17 | } 18 | 19 | func flagInstance() *string { 20 | instName := getEnv("CIEL_INST", "") 21 | flag.StringVar(&instName, "i", instName, "instance `name`; CIEL_INST") 22 | return &instName 23 | } 24 | func saveInstance(instName string) { 25 | saveEnv("CIEL_INST", instName) 26 | } 27 | 28 | func flagNetwork() *bool { 29 | network := getEnv("CIEL_NET", "false") == "true" 30 | flag.BoolVar(&network, "net", network, "create a network zone; CIEL_NET") 31 | return &network 32 | } 33 | func saveNetwork(network bool) { 34 | saveEnv("CIEL_NET", network) 35 | } 36 | 37 | func flagNoBooting() *bool { 38 | noBooting := getEnv("CIEL_BOOT", "false") == "true" 39 | flag.BoolVar(&noBooting, "n", noBooting, "do not boot the container; CIEL_BOOT") 40 | return &noBooting 41 | } 42 | func saveNoBooting(noBooting bool) { 43 | saveEnv("CIEL_BOOT", noBooting) 44 | } 45 | 46 | func flagBatch() *bool { 47 | batch := getEnv("CIEL_BATCH_MODE", "false") == "true" 48 | flag.BoolVar(&batch, "batch", batch, "do not ask; CIEL_BATCH_MODE") 49 | return &batch 50 | } 51 | func saveBatch(batch bool) { 52 | saveEnv("CIEL_BATCH_MODE", batch) 53 | } 54 | 55 | func flagLocalRepo() *bool { 56 | localRepo := getEnv("CIEL_LOCAL_REPO", "false") == "true" 57 | return &localRepo 58 | } 59 | func saveLocalRepo(localRepo bool) { 60 | saveEnv("CIEL_LOCAL_REPO", localRepo) 61 | } 62 | 63 | func getEnv(key, def string) string { 64 | v, ok := os.LookupEnv(key) 65 | if !ok { 66 | return def 67 | } 68 | return v 69 | } 70 | func saveEnv(key string, value interface{}) { 71 | switch v := value.(type) { 72 | case bool: 73 | if v { 74 | os.Setenv(key, "true") 75 | } else { 76 | os.Setenv(key, "false") 77 | } 78 | case string: 79 | os.Setenv(key, v) 80 | } 81 | } 82 | 83 | func buildContainerInfo(boot bool, network bool) *nspawn.ContainerInfo { 84 | ci := &nspawn.ContainerInfo{ 85 | Init: boot, 86 | } 87 | if network { 88 | ci.Network = &nspawn.NetworkInfo{ 89 | Zone: "ciel", 90 | } 91 | } 92 | return ci 93 | } 94 | 95 | func buildRunInfo(args []string) *nspawn.RunInfo { 96 | ri := &nspawn.RunInfo{ 97 | App: args[0], 98 | } 99 | if len(args) > 1 { 100 | ri.Args = args[1:] 101 | } 102 | return ri 103 | } 104 | -------------------------------------------------------------------------------- /cmd/ciel/guest_os.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "flag" 8 | "io/ioutil" 9 | "log" 10 | "net/url" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "regexp" 15 | "strconv" 16 | "strings" 17 | 18 | d "github.com/AOSC-Dev/ciel/display" 19 | "github.com/AOSC-Dev/ciel/internal/ciel" 20 | "github.com/AOSC-Dev/ciel/internal/container/instance" 21 | "github.com/AOSC-Dev/ciel/systemd-api/nspawn" 22 | ) 23 | 24 | // URL to the latest version of the tarball, currently hardcoded to amd64 architecture 25 | const ( 26 | LatestTarballURL = "https://releases.aosc.io/os-amd64/buildkit/aosc-os_buildkit_latest_amd64.tar.xz" 27 | ) 28 | 29 | func untarGuestOS() { 30 | basePath := flagCielDir() 31 | batchFlag := flagBatch() 32 | parse() 33 | 34 | i := &ciel.Ciel{BasePath: *basePath} 35 | i.Check() 36 | c := i.Container() 37 | 38 | tar := flag.Arg(0) 39 | if tar == "" { 40 | d.SECTION("Download OS") 41 | d.ITEM("latest tarball url") 42 | d.Println(d.C(d.CYAN, LatestTarballURL)) 43 | tarURL, _ := url.Parse(LatestTarballURL) 44 | _, DownloadTarball := filepath.Split(tarURL.Path) 45 | if DownloadTarball == "" { 46 | DownloadTarball = "latest.tar.xz" 47 | } 48 | cmd := exec.Command("curl", "-Lo", DownloadTarball, "-#", LatestTarballURL) 49 | cmd.Stdin = os.Stdin 50 | cmd.Stdout = os.Stdout 51 | cmd.Stderr = os.Stderr 52 | err := cmd.Run() 53 | d.ITEM("download") 54 | if err != nil { 55 | d.FAILED_BECAUSE(err.Error()) 56 | os.Remove(DownloadTarball) 57 | return 58 | } 59 | d.OK() 60 | tar = DownloadTarball 61 | } 62 | 63 | d.SECTION("Load OS From Compressed File") 64 | d.ITEM("are there any instances?") 65 | 66 | if instList := c.GetAllNames(); len(instList) != 0 { 67 | d.Println(d.C(d.YELLOW, strings.Join(instList, " "))) 68 | if !*batchFlag && d.ASKLower("DELETE ALL INSTANCES?", "yes/no") != "yes" { 69 | os.Exit(1) 70 | } 71 | for _, inst := range c.GetAll() { 72 | if inst.Running() { 73 | inst.Stop(context.TODO()) 74 | } 75 | if inst.Mounted() { 76 | inst.Unmount() 77 | } 78 | d.ITEM("delete " + inst.Name) 79 | err := c.DelInst(inst.Name) 80 | d.ERR(err) 81 | } 82 | } else { 83 | d.Println(d.C(d.CYAN, "NO")) 84 | } 85 | 86 | d.ITEM("is dist dir empty?") 87 | os.Mkdir(c.DistDir(), 0755) 88 | list, err := ioutil.ReadDir(c.DistDir()) 89 | if len(list) != 0 { 90 | d.Println(d.C(d.YELLOW, "NO")) 91 | if !*batchFlag && d.ASKLower("DELETE the old OS?", "yes/no") != "yes" { 92 | os.Exit(1) 93 | } 94 | d.ITEM("remove dist dir") 95 | if err := os.RemoveAll(c.DistDir()); err != nil { 96 | d.FAILED_BECAUSE(err.Error()) 97 | os.Exit(1) 98 | } 99 | d.OK() 100 | 101 | d.ITEM("re-create dist dir") 102 | if err := os.Mkdir(c.DistDir(), 0755); err != nil { 103 | d.FAILED_BECAUSE(err.Error()) 104 | os.Exit(1) 105 | } 106 | d.OK() 107 | } else if err != nil { 108 | d.FAILED_BECAUSE(err.Error()) 109 | os.Exit(1) 110 | } else { 111 | d.Println(d.C(d.CYAN, "YES")) 112 | } 113 | 114 | d.ITEM("unpacking os...") 115 | cmd := exec.Command("tar", "-xpf", tar, "-C", c.DistDir()) 116 | output, err := cmd.CombinedOutput() 117 | if err != nil { 118 | d.FAILED_BECAUSE(strings.TrimSpace(string(output))) 119 | } 120 | d.OK() 121 | } 122 | 123 | func update() { 124 | var runErr error 125 | var exitStatus int 126 | defer func() { 127 | if runErr != nil { 128 | os.Exit(1) 129 | } else if exitStatus != 0 { 130 | os.Exit(exitStatus) 131 | } 132 | }() 133 | 134 | basePath := flagCielDir() 135 | networkFlag := flagNetwork() 136 | batchFlag := flagBatch() 137 | parse() 138 | 139 | i := &ciel.Ciel{BasePath: *basePath} 140 | i.Check() 141 | c := i.Container() 142 | 143 | d.SECTION("Update Guest Operating System") 144 | d.ITEM("are there online instances?") 145 | ready := true 146 | for _, inst := range c.GetAll() { 147 | if inst.Running() || inst.Mounted() { 148 | ready = false 149 | d.Print(d.C(d.YELLOW, inst.Name) + " ") 150 | } 151 | } 152 | if ready { 153 | d.Print(d.C(d.CYAN, "NO")) 154 | } 155 | d.Println() 156 | 157 | if !ready { 158 | if !*batchFlag && d.ASKLower("Stop all instances?", "yes/no") != "yes" { 159 | os.Exit(1) 160 | } 161 | for _, inst := range c.GetAll() { 162 | if inst.Running() { 163 | inst.Stop(context.TODO()) 164 | } 165 | if inst.Mounted() { 166 | inst.Unmount() 167 | } 168 | } 169 | } 170 | 171 | const instName = "cielroot----update" 172 | d.ITEM("create temporary instance") 173 | c.AddInst(instName) 174 | d.OK() 175 | defer func() { 176 | d.ITEM("delete temporary instance") 177 | c.DelInst(instName) 178 | d.OK() 179 | }() 180 | inst := c.Instance(instName) 181 | d.ITEM("mount temporary instance") 182 | inst.Mount() 183 | d.OK() 184 | defer func() { 185 | inst.Unmount() 186 | }() 187 | 188 | type ExitError struct{} 189 | var run = func(cmd string) (int, error) { 190 | return _shellRun(inst, *networkFlag, true, cmd) 191 | } 192 | defer func() { 193 | p := recover() 194 | if p == nil { 195 | return 196 | } 197 | if _, isExit := p.(ExitError); !isExit { 198 | panic(p) 199 | } 200 | if runErr != nil { 201 | d.Println(d.C(d.RED, runErr.Error())) 202 | } else { 203 | d.Println(d.C(d.YELLOW, "INTERRUPTED, exit status: "+strconv.Itoa(exitStatus))) 204 | } 205 | }() 206 | 207 | exitStatus, runErr = run(`apt-get update --yes`) 208 | d.ITEM("update database") 209 | if runErr != nil || exitStatus != 0 { 210 | panic(ExitError{}) 211 | } 212 | d.OK() 213 | 214 | exitStatus, runErr = run(`apt-get -o Dpkg::Options::="--force-confnew" dist-upgrade --autoremove --purge --yes ` + strings.Join(flag.Args(), " ")) 215 | 216 | d.ITEM("update and auto-remove packages") 217 | if runErr != nil || exitStatus != 0 { 218 | panic(ExitError{}) 219 | } 220 | d.OK() 221 | 222 | d.ITEM("merge changes") 223 | err := inst.FileSystem().Merge() 224 | d.ERR(err) 225 | } 226 | 227 | func factoryReset() { 228 | var runErr error 229 | var exitStatus int 230 | defer func() { 231 | if runErr != nil { 232 | os.Exit(1) 233 | } else if exitStatus != 0 { 234 | os.Exit(exitStatus) 235 | } 236 | }() 237 | 238 | basePath := flagCielDir() 239 | instName := flagInstance() 240 | parse() 241 | 242 | i := &ciel.Ciel{BasePath: *basePath} 243 | i.Check() 244 | c := i.Container() 245 | c.CheckInst(*instName) 246 | inst := c.Instance(*instName) 247 | 248 | d.SECTION("Factory Reset Guest Operating System") 249 | 250 | inst.Stop(context.TODO()) 251 | 252 | ctnInfo := buildContainerInfo(false, false) 253 | runInfo := buildRunInfo([]string{ 254 | "/bin/apt-gen-list", 255 | }) 256 | if exitStatus, err := inst.Run(context.TODO(), ctnInfo, runInfo); exitStatus != 0 { 257 | log.Println(err) 258 | } 259 | 260 | inst.Stop(context.TODO()) 261 | d.ITEM("mount instance") 262 | inst.Mount() 263 | d.OK() 264 | 265 | d.ITEM("collect package list in dpkg") 266 | pkgList := dpkgPackages(inst) 267 | d.OK() 268 | 269 | d.ITEM("collect file set in packages") 270 | fileSet := dpkgPackageFiles(inst, pkgList) 271 | d.OK() 272 | 273 | i.GetTree().MountHandler(inst, false) 274 | i.GetOutput().MountHandler(inst, false) 275 | 276 | d.ITEM("remove out-of-package files") 277 | err := clean(inst.MountPoint(), fileSet, 278 | []string{ 279 | `^/tree`, 280 | `^/dev`, 281 | `^/efi`, 282 | `^/etc`, 283 | `^/run`, 284 | `^/usr`, 285 | `^/var/lib/apt/gen`, 286 | `^/var/lib/apt/extended_states`, 287 | `^/var/lib/dkms`, 288 | `^/var/lib/dpkg`, 289 | `^/var/log/journal$`, 290 | `^/root`, 291 | `^/home`, 292 | `/\.updated$`, 293 | }, []string{ 294 | `^/etc/.*-$`, 295 | `^/etc/machine-id`, 296 | `^/etc/ssh/ssh_host_.*`, 297 | `^/root/\.bash_history`, 298 | `^/var/lib/dpkg/.*-old$`, 299 | `^/var/tmp/.*`, 300 | `^/var/log/apt/.*`, 301 | `^/var/log/alternatives.log`, 302 | `^/var/log/journal/.*`, 303 | `^/var/log/lastlog`, 304 | `^/var/log/tallylog`, 305 | `^/var/log/btmp`, 306 | `^/var/log/wtmp`, 307 | }, func(path string, info os.FileInfo, err error) error { 308 | if err := os.RemoveAll(path); err != nil { 309 | log.Println("clean:", err.Error()) 310 | } 311 | return nil 312 | }) 313 | d.ERR(err) 314 | } 315 | 316 | func clean(root string, packageFiles map[string]bool, preserve []string, delete []string, fn filepath.WalkFunc) error { 317 | var preserveList []string 318 | if preserve != nil { 319 | for _, re := range preserve { 320 | preserveList = append(preserveList, "("+re+")") 321 | } 322 | } else { 323 | preserveList = []string{`($^)`} 324 | } 325 | var deleteList []string 326 | if delete != nil { 327 | for _, re := range delete { 328 | deleteList = append(deleteList, "("+re+")") 329 | } 330 | } else { 331 | deleteList = []string{`($^)`} 332 | } 333 | regexPreserve := regexp.MustCompile("(" + strings.Join(preserveList, "|") + ")") 334 | regexDelete := regexp.MustCompile("(" + strings.Join(deleteList, "|") + ")") 335 | 336 | if packageFiles == nil { 337 | return errors.New("no file in dpkg") 338 | } 339 | filepath.Walk(root, wrapWalkFunc(root, func(path string, info os.FileInfo, err error) error { 340 | if _, inDpkg := packageFiles[path]; inDpkg { 341 | return nil 342 | } 343 | if regexDelete.MatchString(path) { 344 | return fn(filepath.Join(root, path), info, err) 345 | } 346 | if regexPreserve.MatchString(path) { 347 | return nil 348 | } 349 | return fn(filepath.Join(root, path), info, err) 350 | })) 351 | 352 | return nil 353 | } 354 | 355 | func wrapWalkFunc(root string, fn filepath.WalkFunc) filepath.WalkFunc { 356 | return func(path string, info os.FileInfo, err error) error { 357 | if err != nil { 358 | return nil 359 | } 360 | if path == root { 361 | return nil 362 | } 363 | rel, _ := filepath.Rel(root, path) 364 | rel = filepath.Join("/", rel) 365 | return fn(rel, info, err) 366 | } 367 | } 368 | 369 | func dpkgPackages(i *instance.Instance) []string { 370 | ctnInfo := buildContainerInfo(false, false) 371 | 372 | stdout := new(bytes.Buffer) 373 | var args []string 374 | args = []string{ 375 | "/usr/bin/dpkg-query", 376 | "--show", 377 | "--showformat=${Package}\n", 378 | } 379 | runInfo := buildRunInfo(args) 380 | runInfo.StdDev = &nspawn.StdDevInfo{ 381 | Stderr: os.Stderr, 382 | Stdout: stdout, 383 | } 384 | if exitStatus, err := i.Run(context.TODO(), ctnInfo, runInfo); exitStatus != 0 { 385 | log.Println(err) 386 | return nil 387 | } 388 | pkgList := strings.Split(strings.TrimSpace(stdout.String()), "\n") 389 | for i := range pkgList { 390 | pkgList[i] = strings.TrimSpace(pkgList[i]) 391 | } 392 | return pkgList 393 | } 394 | 395 | func dpkgPackageFiles(i *instance.Instance, packages []string) map[string]bool { 396 | ctnInfo := buildContainerInfo(false, false) 397 | 398 | stdout := new(bytes.Buffer) 399 | var args []string 400 | args = []string{ 401 | "/usr/bin/dpkg-query", 402 | "--listfiles", 403 | } 404 | args = append(args, packages...) 405 | runInfo := buildRunInfo(args) 406 | runInfo.StdDev = &nspawn.StdDevInfo{ 407 | Stderr: os.Stderr, 408 | Stdout: stdout, 409 | } 410 | if exitStatus, err := i.Run(context.TODO(), ctnInfo, runInfo); exitStatus != 0 { 411 | log.Println(err) 412 | return nil 413 | } 414 | 415 | hashMap := make(map[string]bool, 100000) 416 | dataSet := strings.Split(stdout.String(), "\n") 417 | root := i.MountPoint() 418 | for _, record := range dataSet { 419 | record = strings.TrimSpace(record) 420 | if len(record) == 0 { 421 | continue 422 | } 423 | path := strings.TrimSpace(record) 424 | evalSymlinksCleanCache() 425 | path = evalSymlinks(root, path, true) 426 | hashMap[path] = true 427 | } 428 | return hashMap 429 | } 430 | 431 | var cachedLstat map[string]bool 432 | 433 | // evalSymlinks resolves symbolic links IN path based on specified root, outputs an unique path. 434 | // 435 | // noLeaf: true - do not resolve the last object in path (file or directory). true in common cases. 436 | func evalSymlinks(root string, path string, noLeaf bool) string { 437 | path = filepath.Clean(path) 438 | var pos int 439 | var prefix string 440 | for pos != len(path) { 441 | // split 442 | if delimPos := strings.IndexRune(path[pos:], filepath.Separator); delimPos == -1 { 443 | // deepest one 444 | if noLeaf { 445 | break 446 | } 447 | pos = len(path) 448 | prefix = path 449 | } else { 450 | // directories in the middle 451 | pos += delimPos + 1 452 | if pos == 1 { 453 | prefix = path[:pos] 454 | } else { 455 | prefix = path[:pos-1] 456 | } 457 | } 458 | 459 | isLink, cached := cachedLstat[prefix] 460 | if cached { 461 | if !isLink { 462 | continue // most common 463 | } 464 | panic("loop in symlink") 465 | } else { 466 | fi, _ := os.Lstat(filepath.Join(root, prefix)) 467 | if fi == nil || fi.Mode()&os.ModeSymlink == 0 { 468 | cachedLstat[prefix] = false 469 | continue 470 | } 471 | cachedLstat[prefix] = true 472 | target, _ := os.Readlink(filepath.Join(root, prefix)) 473 | if !filepath.IsAbs(target) { 474 | target = filepath.Join(filepath.Dir(prefix), target) 475 | } 476 | target = evalSymlinks(root, target, noLeaf) // fix new path (prefix) 477 | path = filepath.Join(target, path[pos:]) 478 | pos = len(target) 479 | } 480 | } 481 | return path 482 | } 483 | 484 | func evalSymlinksCleanCache() { 485 | cachedLstat = make(map[string]bool) 486 | } 487 | -------------------------------------------------------------------------------- /cmd/ciel/instances.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path" 10 | "strconv" 11 | "strings" 12 | 13 | d "github.com/AOSC-Dev/ciel/display" 14 | "github.com/AOSC-Dev/ciel/internal/ciel" 15 | "github.com/AOSC-Dev/ciel/internal/container/instance" 16 | ) 17 | 18 | func add() { 19 | basePath := flagCielDir() 20 | parse() 21 | instName := flag.Arg(0) 22 | 23 | if instName == "" { 24 | log.Fatalln("give me a name for the new instance") 25 | } 26 | if strings.Contains(instName, " ") { 27 | log.Fatalln("do not contain white space") 28 | } 29 | 30 | i := &ciel.Ciel{BasePath: *basePath} 31 | i.Check() 32 | c := i.Container() 33 | 34 | if c.InstExists(instName) { 35 | log.Fatalln("already has " + instName) 36 | } 37 | c.AddInst(instName) 38 | c.Instance(instName).Mount() 39 | } 40 | 41 | func del() { 42 | basePath := flagCielDir() 43 | parse() 44 | instName := flag.Arg(0) 45 | 46 | i := &ciel.Ciel{BasePath: *basePath} 47 | i.Check() 48 | c := i.Container() 49 | c.CheckInst(instName) 50 | 51 | c.Instance(instName).Unmount() 52 | c.DelInst(instName) 53 | } 54 | 55 | func shell() { 56 | basePath := flagCielDir() 57 | instName := flagInstance() 58 | networkFlag := flagNetwork() 59 | noBooting := flagNoBooting() 60 | parse() 61 | 62 | if flag.NArg() > 1 { 63 | log.Fatalln("you must pass one argument only") 64 | } 65 | 66 | i := &ciel.Ciel{BasePath: *basePath} 67 | i.Check() 68 | c := i.Container() 69 | c.CheckInst(*instName) 70 | 71 | inst := c.Instance(*instName) 72 | inst.Mount() 73 | 74 | if flag.NArg() == 0 { 75 | exitStatus, err := _openShell( 76 | inst, 77 | *networkFlag, 78 | !*noBooting, 79 | ) 80 | if err != nil { 81 | log.Println(err) 82 | } 83 | os.Exit(exitStatus) 84 | } 85 | exitStatus, err := _shellRun( 86 | inst, 87 | *networkFlag, 88 | !*noBooting, 89 | flag.Arg(0), 90 | ) 91 | if err != nil { 92 | log.Println(err) 93 | } 94 | os.Exit(exitStatus) 95 | } 96 | 97 | func run() { 98 | basePath := flagCielDir() 99 | instName := flagInstance() 100 | networkFlag := flagNetwork() 101 | noBooting := flagNoBooting() 102 | parse() 103 | 104 | i := &ciel.Ciel{BasePath: *basePath} 105 | i.Check() 106 | c := i.Container() 107 | c.CheckInst(*instName) 108 | 109 | inst := c.Instance(*instName) 110 | inst.Mount() 111 | 112 | ctnInfo := buildContainerInfo(!*noBooting, *networkFlag) 113 | runInfo := buildRunInfo(flag.Args()) 114 | runInfo.UseSystemdRun = true 115 | exitStatus, err := inst.Run( 116 | context.TODO(), 117 | ctnInfo, 118 | runInfo, 119 | ) 120 | 121 | if err != nil { 122 | log.Println(err) 123 | } 124 | os.Exit(exitStatus) 125 | } 126 | 127 | func _openShell(inst *instance.Instance, network bool, boot bool) (int, error) { 128 | inst.Mount() 129 | rootShell, err := inst.Shell("root") 130 | if err != nil { 131 | return -1, err 132 | } 133 | ctnInfo := buildContainerInfo(boot, network) 134 | runInfo := buildRunInfo([]string{rootShell}) 135 | 136 | exitStatus, err := inst.Run( 137 | context.TODO(), 138 | ctnInfo, 139 | runInfo, 140 | ) 141 | if err != nil { 142 | return -1, err 143 | } 144 | return exitStatus, nil 145 | } 146 | 147 | func _shellRun(inst *instance.Instance, network bool, boot bool, cmd string) (int, error) { 148 | inst.Mount() 149 | var args []string 150 | rootShell, err := inst.Shell("root") 151 | if err != nil { 152 | return -1, err 153 | } 154 | if cmd != "" { 155 | cmd = cmd + "; echo $?>/.ciel-exit-status" 156 | args = []string{ 157 | rootShell, 158 | "--login", 159 | "-c", cmd, 160 | } 161 | } else { 162 | args = []string{ 163 | rootShell, 164 | } 165 | } 166 | ctnInfo := buildContainerInfo(boot, network) 167 | runInfo := buildRunInfo(args) 168 | exitStatus, err := inst.Run( 169 | context.TODO(), 170 | ctnInfo, 171 | runInfo, 172 | ) 173 | exitStatusFile := path.Join(inst.MountPoint(), ".ciel-exit-status") 174 | if b, err := ioutil.ReadFile(exitStatusFile); err == nil { 175 | os.Remove(exitStatusFile) 176 | if realExitStatus, err := strconv.Atoi(strings.TrimSpace(string(b))); err == nil { 177 | return realExitStatus, nil 178 | } 179 | log.Println(err) 180 | return exitStatus, nil 181 | } 182 | if os.IsNotExist(err) { 183 | log.Println("session was accidentally terminated") 184 | } else { 185 | log.Println(err) 186 | } 187 | if exitStatus == 0 { 188 | exitStatus = 1 189 | } 190 | return exitStatus, nil 191 | } 192 | 193 | func stop() { 194 | basePath := flagCielDir() 195 | instName := flagInstance() 196 | parse() 197 | 198 | i := &ciel.Ciel{BasePath: *basePath} 199 | i.Check() 200 | c := i.Container() 201 | c.CheckInst(*instName) 202 | 203 | inst := c.Instance(*instName) 204 | 205 | err := inst.Stop(context.TODO()) 206 | if err != nil { 207 | log.Fatal(err) 208 | } 209 | } 210 | 211 | func rollback() { 212 | basePath := flagCielDir() 213 | instName := flagInstance() 214 | parse() 215 | 216 | i := &ciel.Ciel{BasePath: *basePath} 217 | i.Check() 218 | c := i.Container() 219 | c.CheckInst(*instName) 220 | 221 | inst := c.Instance(*instName) 222 | 223 | d.SECTION("Rollback Changes") 224 | d.ITEM("is running?") 225 | if inst.Running() { 226 | d.Println(d.C(d.YELLOW, "ONLINE")) 227 | inst.Unmount() 228 | } else { 229 | d.Println(d.C(d.CYAN, "OFFLINE")) 230 | } 231 | inst.FileSystem().Rollback() 232 | } 233 | 234 | func commit() { 235 | basePath := flagCielDir() 236 | instName := flagInstance() 237 | parse() 238 | 239 | i := &ciel.Ciel{BasePath: *basePath} 240 | i.Check() 241 | c := i.Container() 242 | c.CheckInst(*instName) 243 | 244 | inst := c.Instance(*instName) 245 | 246 | d.SECTION("Commit Changes") 247 | d.ITEM("is running?") 248 | if inst.Running() { 249 | d.Println(d.C(d.YELLOW, "ONLINE")) 250 | inst.Unmount() 251 | } else { 252 | d.Println(d.C(d.CYAN, "OFFLINE")) 253 | } 254 | inst.FileSystem().Merge() 255 | } 256 | -------------------------------------------------------------------------------- /cmd/ciel/mount_points.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/AOSC-Dev/ciel/internal/ciel" 7 | 8 | d "github.com/AOSC-Dev/ciel/display" 9 | ) 10 | 11 | func mountCiel() { 12 | basePath := flagCielDir() 13 | instName := flagInstance() 14 | parse() 15 | 16 | i := &ciel.Ciel{BasePath: *basePath} 17 | i.Check() 18 | c := i.Container() 19 | 20 | if *instName == "" { 21 | instList := c.GetAll() 22 | for _, inst := range instList { 23 | err := inst.Mount() 24 | if err != nil { 25 | log.Println(inst.Name+":", err) 26 | } 27 | } 28 | return 29 | } 30 | c.CheckInst(*instName) 31 | c.Instance(*instName).Mount() 32 | } 33 | 34 | func shutdown() { 35 | basePath := flagCielDir() 36 | instName := flagInstance() 37 | parse() 38 | 39 | i := &ciel.Ciel{BasePath: *basePath} 40 | i.Check() 41 | c := i.Container() 42 | 43 | if *instName == "" { 44 | instList := c.GetAll() 45 | for _, inst := range instList { 46 | d.SECTION("Shutdown Instance " + inst.Name) 47 | inst.Unmount() 48 | } 49 | return 50 | } 51 | c.CheckInst(*instName) 52 | d.SECTION("Shutdown Instance " + *instName) 53 | c.Instance(*instName).Unmount() 54 | } 55 | -------------------------------------------------------------------------------- /cmd/ciel/plugins.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | "syscall" 12 | 13 | "github.com/AOSC-Dev/ciel/config" 14 | ) 15 | 16 | // constants definitions for the plugin paths 17 | var ( 18 | LibExecDir = config.Prefix + "/libexec" 19 | PluginDir = LibExecDir + "/ciel-plugin" 20 | PluginPrefix = "ciel-" 21 | ) 22 | 23 | // Plugin defines a ciel plugin 24 | type Plugin struct { 25 | Name string 26 | // TODO: get script usage from header comments 27 | Usage string 28 | } 29 | 30 | func plugin(subCmd string) int { 31 | basePath := flagCielDir() 32 | instName := flagInstance() 33 | networkFlag := flagNetwork() 34 | noBooting := flagNoBooting() 35 | batchFlag := flagBatch() 36 | parse() 37 | saveCielDir(*basePath) 38 | saveInstance(*instName) 39 | saveNetwork(*networkFlag) 40 | saveNoBooting(*noBooting) 41 | saveBatch(*batchFlag) 42 | 43 | proc := filepath.Join(PluginDir, PluginPrefix+subCmd) 44 | cmd := exec.Command(proc, flag.Args()...) 45 | cmd.Stdin = os.Stdin 46 | cmd.Stdout = os.Stdout 47 | cmd.Stderr = os.Stderr 48 | err := cmd.Run() 49 | 50 | if err != nil { 51 | if exitError, ok := err.(*exec.ExitError); ok { 52 | exitStatus := exitError.Sys().(syscall.WaitStatus) 53 | return exitStatus.ExitStatus() 54 | } 55 | log.Printf("failed to run plugin %s: %v\n", subCmd, err) 56 | return 1 57 | } 58 | return 0 59 | } 60 | 61 | func getPlugins() []Plugin { 62 | var Plugins []Plugin 63 | files, err := ioutil.ReadDir(PluginDir) 64 | if err != nil { 65 | log.Fatalf("failed to get files under plugin directory: %v\n", err) 66 | } 67 | for _, f := range files { 68 | if f.IsDir() { 69 | continue 70 | } else { 71 | fname := f.Name() 72 | if len(fname) > len(PluginPrefix) && strings.HasPrefix(fname, PluginPrefix) { 73 | Plugins = append(Plugins, Plugin{Name: fname[len(PluginPrefix):]}) 74 | } 75 | } 76 | } 77 | return Plugins 78 | } 79 | -------------------------------------------------------------------------------- /cmd/ciel/status.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/AOSC-Dev/ciel/internal/ciel" 7 | 8 | d "github.com/AOSC-Dev/ciel/display" 9 | ) 10 | 11 | func list() { 12 | basePath := flagCielDir() 13 | parse() 14 | 15 | i := &ciel.Ciel{BasePath: *basePath} 16 | i.Check() 17 | c := i.Container() 18 | 19 | fmt.Printf("%s\t%s\t%s\t%s\n", "INSTANCE", "WORKDIR", "STATUS", "BOOT") 20 | for _, inst := range c.GetAll() { 21 | showInst := inst.Name 22 | tabs := "\t\t" 23 | if len(showInst) > 7 { 24 | tabs = "\t" 25 | } 26 | if len(showInst) > 14 { 27 | showInst = showInst[:12] + ".." 28 | } 29 | var fsStatus, ctnStatus, boot string 30 | if inst.Running() { 31 | ctnStatus = d.C0(d.GREEN, "running") 32 | if inst.RunningAsBootMode() { 33 | boot = d.C(d.CYAN, "yes") 34 | } else { 35 | boot = d.C(d.PURPLE, "no") 36 | } 37 | } else { 38 | ctnStatus = d.C0(d.WHITE, "offline") 39 | } 40 | if inst.Mounted() { 41 | fsStatus = d.C0(d.GREEN, "mounted") 42 | } else { 43 | fsStatus = "free" 44 | } 45 | fmt.Printf("%s%s%s\t%s\t%s\n", showInst, tabs, fsStatus, ctnStatus, boot) 46 | } 47 | fmt.Println() 48 | count := len(c.GetAllNames()) 49 | if count <= 1 { 50 | fmt.Printf("%d instance listed.\n", count) 51 | } else { 52 | fmt.Printf("%d instances listed.\n", count) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cmd/ciel/tree.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | 7 | "github.com/AOSC-Dev/ciel/internal/ciel" 8 | ) 9 | 10 | // URL to the main abbs repository 11 | const ( 12 | GitAOSCOSABBS = "https://github.com/AOSC-Dev/aosc-os-abbs" 13 | ) 14 | 15 | func clone() { 16 | basePath := flagCielDir() 17 | parse() 18 | 19 | i := &ciel.Ciel{BasePath: *basePath} 20 | i.Check() 21 | t := i.Tree() 22 | 23 | tree := flag.Arg(0) 24 | if tree == "" { 25 | tree = GitAOSCOSABBS 26 | } 27 | os.Exit(t.Clone(tree)) 28 | } 29 | 30 | func pull() { 31 | basePath := flagCielDir() 32 | parse() 33 | 34 | i := &ciel.Ciel{BasePath: *basePath} 35 | i.Check() 36 | t := i.Tree() 37 | 38 | os.Exit(t.Pull()) 39 | } 40 | -------------------------------------------------------------------------------- /completions/ciel: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | subcmds="version init load-os load-tree update-os update-tree \ 4 | list add del shell config build rollback down mount stop run \ 5 | farewell doctor load-os update-os generate factory-reset commit \ 6 | release -batch -n -i -C" 7 | 8 | _ciel_list_instances() { 9 | [ -d .ciel/container/instances ] || return 10 | find .ciel/container/instances -maxdepth 1 -mindepth 1 -type d -printf '%f\n' 11 | } 12 | 13 | _ciel_list_packages() { 14 | [ -d TREE ] || return 15 | GROUPS="$(find "TREE/groups/" -maxdepth 1 -mindepth 1 -type f -printf 'groups/%f\n')" 16 | COMPREPLY+=($(compgen -W "$GROUPS" -- "${1}")) 17 | if [[ "$1" == *'/'* ]]; then 18 | return 19 | fi 20 | COMPREPLY+=($(find "TREE" -maxdepth 2 -mindepth 2 -type d -not -path "TREE/.git" -name "${1}*" -printf '%f\n')) 21 | } 22 | 23 | _ciel() { 24 | local cur prev words cword state 25 | _init_completion || return 26 | 27 | if [[ "$prev" = "ciel" ]]; then 28 | COMPREPLY=($(compgen -W "$subcmds" -- "$cur")) 29 | return 30 | fi 31 | 32 | for word in "${words[@]}"; do 33 | if [[ "$word" == @(build) ]]; then 34 | state="$word" 35 | break 36 | fi 37 | done 38 | 39 | case "$prev" in 40 | # options with no argument 41 | list | init | version | farewell | doctor | generate | update-os | update-tree) 42 | COMPREPLY=() 43 | ;; 44 | # option(s) with file argument 45 | load-os) 46 | _filedir -f 47 | ;; 48 | # options with bare instance argument 49 | add | del) 50 | COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) 51 | ;; 52 | # options with -i instance argument 53 | shell | config | build | rollback | down | mount | stop | run | factory-reset | commit) 54 | COMPREPLY=($(compgen -W "-i" -- "$cur")) 55 | if [[ "$prev" = 'build' ]]; then 56 | _ciel_list_packages "$cur" 57 | elif [[ "$prev" = 'config' ]]; then 58 | COMPREPLY+=($(compgen -W "-g" -- "$cur")) 59 | fi 60 | ;; 61 | # options after -i instance argument 62 | -i) 63 | COMPREPLY=($(compgen -W "$(_ciel_list_instances)" -- "$cur")) 64 | return 65 | ;; 66 | esac 67 | 68 | # continuous completion for packages 69 | if [[ "$state" = 'build' ]]; then 70 | _ciel_list_packages "$cur" 71 | fi 72 | } 73 | 74 | complete -F _ciel ciel 75 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | Version = "development version" 5 | Prefix = "/usr/local/" 6 | ) 7 | -------------------------------------------------------------------------------- /display/input.go: -------------------------------------------------------------------------------- 1 | package d 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func ASK(msg string, options string) string { 10 | Println() 11 | Println(strings.Repeat(" ", MaxLength-3) + C(WHITE, "=== "+msg+" ===")) 12 | Print(strings.Repeat(" ", MaxLength-3) + ">>> (" + options + "): " + Clr(YELLOW)) 13 | buf := bufio.NewReader(os.Stdin) 14 | 15 | answer, _, _ := buf.ReadLine() 16 | Println(ClrRst()) 17 | return strings.TrimSpace(string(answer)) 18 | } 19 | 20 | func ASKLower(msg string, options string) string { 21 | return strings.ToLower(ASK(msg, options)) 22 | } 23 | -------------------------------------------------------------------------------- /display/output.go: -------------------------------------------------------------------------------- 1 | package d 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type Color int 11 | 12 | const MaxLength = 30 13 | 14 | const ( 15 | _ Color = 30 + iota 16 | RED 17 | GREEN 18 | YELLOW 19 | BLUE 20 | PURPLE 21 | CYAN 22 | WHITE 23 | ) 24 | 25 | func Println(v ...interface{}) { fmt.Fprintln(os.Stderr, v...) } 26 | func Print(v ...interface{}) { fmt.Fprint(os.Stderr, v...) } 27 | 28 | func C0(cid Color, s string) string { 29 | return Clr0(cid) + s + ClrRst() 30 | } 31 | func C(cid Color, s string) string { 32 | return Clr(cid) + s + ClrRst() 33 | } 34 | func Clr0(cid Color) string { 35 | if cid == WHITE { 36 | return "\033[0m" 37 | } 38 | return "\033[0;" + strconv.Itoa(int(cid)) + "m" 39 | } 40 | func Clr(cid Color) string { 41 | if cid == WHITE { 42 | return "\033[1m" 43 | } 44 | return "\033[1;" + strconv.Itoa(int(cid)) + "m" 45 | } 46 | func ClrRst() string { 47 | return "\033[0m" 48 | } 49 | 50 | func StripEsc(s string) string { 51 | const ( 52 | PLAIN = iota 53 | ESC 54 | ESC_END 55 | CSI_1 56 | CSI_2 57 | CSI_END 58 | ) 59 | var state = PLAIN 60 | var pt string 61 | for _, c := range s { 62 | switch { 63 | case state == PLAIN && c == 0x1b: 64 | state = ESC 65 | case state == ESC && c == '[': 66 | state = CSI_1 67 | case state == ESC && c != '[': 68 | state = ESC_END 69 | case state == CSI_1 && 0x30 <= c && c <= 0x3f: 70 | state = CSI_1 71 | case state == CSI_1 && 0x20 <= c && c <= 0x2f: 72 | state = CSI_2 73 | case (state == CSI_1 || state == CSI_2) && 0x40 <= c && c <= 0x7e: 74 | state = CSI_END 75 | default: 76 | state = PLAIN 77 | } 78 | if state == PLAIN { 79 | pt += string(c) 80 | } 81 | } 82 | // _ _ _ 83 | // | \ | 3* | 2* 84 | // PLAIN --1b-> ESC --'['-> CSI_1 --2*-> CSI_2 85 | // | / / 86 | // |----------/---- 40~7e --------------/ 87 | return pt 88 | } 89 | 90 | func EscLen(s string) int { 91 | plainText := StripEsc(s) 92 | return len(plainText) 93 | } 94 | 95 | func ITEM(s string) { 96 | l := MaxLength - EscLen(s) 97 | if l < 0 { 98 | l = 0 99 | s = s[:MaxLength-2] + ".." 100 | } 101 | Print(strings.Repeat(" ", l) + s + " ") 102 | } 103 | 104 | var firstSection = true 105 | 106 | func SECTION(s string) { 107 | if firstSection { 108 | firstSection = false 109 | } else { 110 | Println() 111 | } 112 | Println(strings.Repeat(" ", MaxLength+1) + C(WHITE, s)) 113 | } 114 | 115 | func OK() { 116 | Println(C(CYAN, "OK")) 117 | } 118 | 119 | func FAILED() { 120 | Println(C(RED, "FAILED")) 121 | } 122 | 123 | func FAILED_BECAUSE(s string) { 124 | Println(C(RED, s)) 125 | } 126 | 127 | func SKIPPED() { 128 | Println(C0(WHITE, "--")) 129 | } 130 | 131 | func ERR(err error) { 132 | if err == nil { 133 | OK() 134 | } else { 135 | FAILED_BECAUSE(err.Error()) 136 | } 137 | } 138 | 139 | func WARN(err error) { 140 | if err == nil { 141 | OK() 142 | } else { 143 | Println(C(YELLOW, err.Error())) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AOSC-Dev/ciel 2 | 3 | go 1.13 4 | 5 | require github.com/godbus/dbus/v5 v5.0.3 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= 2 | github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 3 | -------------------------------------------------------------------------------- /internal/abstract/abstract.go: -------------------------------------------------------------------------------- 1 | package abstract 2 | 3 | type Ciel interface { 4 | GetBasePath() string 5 | GetTree() Tree 6 | GetOutput() Tree 7 | GetContainer() Container 8 | } 9 | 10 | type Container interface { 11 | GetBasePath() string 12 | DistDir() string 13 | GetCiel() Ciel 14 | } 15 | 16 | type Instance interface { 17 | MountPoint() string 18 | GetContainer() Container 19 | } 20 | 21 | type Tree interface { 22 | MountHandler(instance Instance, mount bool) 23 | } 24 | -------------------------------------------------------------------------------- /internal/ciel/cieldir.go: -------------------------------------------------------------------------------- 1 | package ciel 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "path" 7 | "strings" 8 | 9 | "github.com/AOSC-Dev/ciel/internal/abstract" 10 | "github.com/AOSC-Dev/ciel/internal/container" 11 | "github.com/AOSC-Dev/ciel/internal/packaging" 12 | "github.com/AOSC-Dev/ciel/internal/pkgtree" 13 | "github.com/AOSC-Dev/ciel/internal/utils" 14 | ) 15 | 16 | const ( 17 | DotCielDirName = ".ciel" 18 | 19 | ContainerDirName = DotCielDirName + "/container" 20 | TreeDirName = "TREE" 21 | OutputDirName = "OUTPUT/debs" 22 | 23 | VersionFile = DotCielDirName + "/version" 24 | Version = "2" 25 | ) 26 | 27 | type Ciel struct { 28 | BasePath string 29 | } 30 | 31 | func (i *Ciel) Check() { 32 | ver, err := ioutil.ReadFile(i.VerFile()) 33 | if err != nil { 34 | log.Fatalln("not a Ciel work directory here") 35 | } 36 | if strings.TrimSpace(string(ver)) != Version { 37 | log.Fatalln("your Ciel work directory is an incompatible version") 38 | } 39 | } 40 | func (i *Ciel) CielDir() string { 41 | return path.Join(i.BasePath, DotCielDirName) 42 | } 43 | func (i *Ciel) VerFile() string { 44 | return path.Join(i.BasePath, VersionFile) 45 | } 46 | func (i *Ciel) containerDir() string { 47 | return path.Join(i.BasePath, ContainerDirName) 48 | } 49 | func (i *Ciel) treeDir() string { 50 | return path.Join(i.BasePath, TreeDirName) 51 | } 52 | func (i *Ciel) outDir() string { 53 | return path.Join(i.BasePath, OutputDirName) 54 | } 55 | 56 | func (i *Ciel) Init() { 57 | utils.MustMkdir(i.CielDir()) 58 | if err := ioutil.WriteFile(i.VerFile(), []byte(Version), 0644); err != nil { 59 | log.Panic(err) 60 | } 61 | i.Container().Init() 62 | } 63 | 64 | func (i *Ciel) Container() *container.Container { 65 | return &container.Container{Parent: i, BasePath: i.containerDir()} 66 | } 67 | func (i *Ciel) Tree() *pkgtree.Tree { 68 | return &pkgtree.Tree{Parent: i, BasePath: i.treeDir()} 69 | } 70 | func (i *Ciel) Output() *packaging.Tree { 71 | return &packaging.Tree{Parent: i, BasePath: i.outDir()} 72 | } 73 | func (i *Ciel) GetContainer() abstract.Container { 74 | return i.Container() 75 | } 76 | func (i *Ciel) GetTree() abstract.Tree { 77 | return i.Tree() 78 | } 79 | func (i *Ciel) GetOutput() abstract.Tree { 80 | return i.Output() 81 | } 82 | func (i *Ciel) GetBasePath() string { return i.BasePath } 83 | -------------------------------------------------------------------------------- /internal/container/container.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path" 9 | "strings" 10 | 11 | "github.com/AOSC-Dev/ciel/internal/abstract" 12 | "github.com/AOSC-Dev/ciel/internal/container/instance" 13 | "github.com/AOSC-Dev/ciel/internal/utils" 14 | ) 15 | 16 | const ( 17 | DistDirName = "dist" 18 | InstDirName = "instances" 19 | ) 20 | 21 | var ( 22 | ErrInvalidInstName = errors.New("invalid instance name") 23 | ) 24 | 25 | type Container struct { 26 | Parent abstract.Ciel 27 | BasePath string 28 | } 29 | 30 | func (i *Container) DistDir() string { 31 | return path.Join(i.BasePath, DistDirName) 32 | } 33 | func (i *Container) InstDir() string { 34 | return path.Join(i.BasePath, InstDirName) 35 | } 36 | 37 | func (i *Container) Init() { 38 | utils.MustMkdir(i.BasePath) 39 | utils.MustMkdir(i.DistDir()) 40 | utils.MustMkdir(i.InstDir()) 41 | } 42 | 43 | func (i *Container) Instance(name string) *instance.Instance { 44 | return &instance.Instance{Parent: i, BasePath: i.InstDir(), Name: name} 45 | } 46 | 47 | func (i *Container) AddInst(name string) error { 48 | if strings.ContainsAny(name, "/\\ ") { 49 | return ErrInvalidInstName 50 | } 51 | utils.MustMkdir(path.Join(i.InstDir(), name)) 52 | return i.Instance(name).Init() 53 | } 54 | func (i *Container) DelInst(name string) error { 55 | i.Instance(name).RunLock().Remove() 56 | i.Instance(name).FileSystemLock().Remove() 57 | return os.RemoveAll(path.Join(i.InstDir(), name)) 58 | } 59 | 60 | func (i *Container) InstExists(name string) bool { 61 | if strings.ContainsAny(name, "/\\ ") { 62 | return false 63 | } 64 | instDir := path.Join(i.InstDir(), name) 65 | if instDir == path.Clean(i.InstDir()) { 66 | return false 67 | } 68 | if stat, err := os.Stat(instDir); err != nil || !stat.IsDir() { 69 | return false 70 | } 71 | return true 72 | } 73 | func (i *Container) CheckInst(name string) { 74 | if name == "" { 75 | log.Fatalln("you must specify a instance") 76 | } 77 | if !i.InstExists(name) { 78 | log.Fatalln("instance '" + name + "' does not exist") 79 | } 80 | } 81 | 82 | func (i *Container) GetAll() []*instance.Instance { 83 | list := i.GetAllNames() 84 | var instList []*instance.Instance 85 | for _, name := range list { 86 | instList = append(instList, i.Instance(name)) 87 | } 88 | return instList 89 | } 90 | func (i *Container) GetAllNames() []string { 91 | subDirs, err := ioutil.ReadDir(i.InstDir()) 92 | if err != nil { 93 | log.Panic(err) 94 | } 95 | var subDirNames []string 96 | for _, subDirs := range subDirs { 97 | if subDirs.IsDir() { 98 | subDirNames = append(subDirNames, subDirs.Name()) 99 | } 100 | } 101 | return subDirNames 102 | } 103 | 104 | func (i *Container) GetBasePath() string { return i.BasePath } 105 | func (i *Container) GetCiel() abstract.Ciel { return i.Parent } 106 | -------------------------------------------------------------------------------- /internal/container/filesystem/filesystem.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | type FileSystem interface { 4 | MountLocal() error 5 | Mount(readOnly bool) error 6 | Unmount() error 7 | 8 | Rollback() error 9 | Merge() error 10 | } 11 | -------------------------------------------------------------------------------- /internal/container/instance/instance.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path" 11 | "strings" 12 | 13 | "github.com/AOSC-Dev/ciel/ipc" 14 | "github.com/AOSC-Dev/ciel/overlayfs" 15 | "github.com/AOSC-Dev/ciel/proc-api" 16 | "github.com/AOSC-Dev/ciel/systemd-api/machined" 17 | "github.com/AOSC-Dev/ciel/systemd-api/nspawn" 18 | 19 | "github.com/AOSC-Dev/ciel/internal/abstract" 20 | "github.com/AOSC-Dev/ciel/internal/container/filesystem" 21 | 22 | d "github.com/AOSC-Dev/ciel/display" 23 | ) 24 | 25 | const ( 26 | LayerDirName = "layers" 27 | SemIdFileSystemMutex = 0x11 28 | SemIdRunMutex = 0x22 29 | ) 30 | 31 | var ( 32 | ErrMode = errors.New("another instance is running in exclusive mode") 33 | ) 34 | 35 | type Instance struct { 36 | Parent abstract.Container 37 | BasePath string 38 | Name string 39 | } 40 | 41 | func (i *Instance) Init() error { 42 | layersDir := path.Join(i.Dir(), LayerDirName) 43 | return overlayfs.Create(layersDir) 44 | } 45 | func (i *Instance) FileSystem() filesystem.FileSystem { 46 | inst := overlayfs.FromPath(i.Parent.DistDir(), path.Join(i.Dir(), LayerDirName)) 47 | inst.MountPoint = "./" + i.Name 48 | return inst 49 | } 50 | 51 | func (i *Instance) MountPoint() string { 52 | return path.Join(i.Parent.GetCiel().GetBasePath(), i.Name) 53 | } 54 | func (i *Instance) MountLocal() error { 55 | fs := i.FileSystem() 56 | CriticalSection := i.FileSystemLock() 57 | 58 | CriticalSection.Lock() 59 | defer CriticalSection.Unlock() 60 | 61 | if !i.Mounted() { 62 | if err := fs.MountLocal(); err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | } 68 | func (i *Instance) Mount() error { 69 | fs := i.FileSystem() 70 | CriticalSection := i.FileSystemLock() 71 | 72 | CriticalSection.Lock() 73 | defer CriticalSection.Unlock() 74 | 75 | if !i.Mounted() { 76 | if err := fs.Mount(false); err != nil { 77 | return err 78 | } 79 | i.Parent.GetCiel().GetTree().MountHandler(i, true) 80 | i.Parent.GetCiel().GetOutput().MountHandler(i, true) 81 | } 82 | return nil 83 | } 84 | func (i *Instance) Unmount() error { 85 | i.Stop(context.Background()) 86 | fs := i.FileSystem() 87 | CriticalSection := i.FileSystemLock() 88 | 89 | CriticalSection.Lock() 90 | defer CriticalSection.Unlock() 91 | 92 | var err error 93 | if i.Mounted() { 94 | i.Parent.GetCiel().GetTree().MountHandler(i, false) 95 | i.Parent.GetCiel().GetOutput().MountHandler(i, false) 96 | d.ITEM("unmount " + i.Name) 97 | if err := fs.Unmount(); err != nil { 98 | d.FAILED_BECAUSE(err.Error()) 99 | return err 100 | } 101 | d.OK() 102 | } else { 103 | d.ITEM("unmount " + i.Name) 104 | d.SKIPPED() 105 | err = os.ErrNotExist 106 | } 107 | d.ITEM("remove mount point") 108 | tryRemove(i.MountPoint()) 109 | return err 110 | } 111 | 112 | func tryRemove(path string) { 113 | err := os.Remove(path) 114 | if err == nil { 115 | d.OK() 116 | return 117 | } 118 | if os.IsNotExist(err) { 119 | d.SKIPPED() 120 | return 121 | } 122 | d.FAILED_BECAUSE(err.Error()) 123 | } 124 | func (i *Instance) Mounted() bool { 125 | return proc.Mounted(i.MountPoint()) 126 | } 127 | 128 | func (i *Instance) Run(ctx context.Context, ctnInfo *nspawn.ContainerInfo, runInfo *nspawn.RunInfo) (int, error) { 129 | defer RecoverTerminalAttr() 130 | machineId := i.MachineId() 131 | 132 | CriticalSection := i.RunLock() 133 | 134 | if i.RunningAsExclusiveMode() { 135 | return -1, ErrMode 136 | } 137 | 138 | var boot bool 139 | 140 | if ctnInfo.Init && nspawn.IsBootable(i.MountPoint()) { 141 | boot = true 142 | } else { 143 | boot = false 144 | } 145 | 146 | if boot { 147 | var err error 148 | CriticalSection.Lock() 149 | if !i.Running() { 150 | err = nspawn.SystemdNspawnBoot(ctx, machineId, i.MountPoint(), ctnInfo) 151 | } 152 | CriticalSection.Unlock() 153 | if err != nil { 154 | return -1, err 155 | } 156 | if !i.RunningAsBootMode() { 157 | return -1, ErrMode 158 | } 159 | if runInfo.UseSystemdRun { 160 | return nspawn.SystemdRun(ctx, machineId, runInfo) 161 | } 162 | return nspawn.MachinectlShell(ctx, machineId, runInfo) 163 | } else { 164 | return nspawn.SystemdNspawnRun(ctx, machineId, i.MountPoint(), ctnInfo, runInfo) 165 | } 166 | } 167 | 168 | func (i *Instance) Stop(ctx context.Context) error { 169 | d.ITEM("stop " + i.Name) 170 | if !i.Running() { 171 | d.SKIPPED() 172 | return nil 173 | } 174 | var err error 175 | if i.RunningAsBootMode() { 176 | err = nspawn.MachinectlPowerOff(ctx, i.MachineId()) 177 | } else { 178 | err = nspawn.MachinectlTerminate(ctx, i.MachineId()) 179 | } 180 | d.ERR(err) 181 | return err 182 | } 183 | 184 | func (i *Instance) Running() bool { 185 | m := machined.NewManager() 186 | _, err := m.GetMachine(i.MachineId()) 187 | return err == nil 188 | } 189 | 190 | func (i *Instance) RunningAsBootMode() bool { 191 | m := machined.NewManager() 192 | machine, err := m.GetMachine(i.MachineId()) 193 | if err != nil { 194 | return false 195 | } 196 | leader, err := machine.Leader() 197 | if err != nil { 198 | log.Fatalln(err) 199 | } 200 | host, err := proc.GetParentProcessID(leader) 201 | if err != nil { 202 | log.Fatalln(err) 203 | } 204 | cmdline, err := proc.GetCommandLineByPID(host) 205 | if err != nil { 206 | log.Fatalln(err) 207 | } 208 | for _, arg := range cmdline { 209 | if arg == "-b" || arg == "--boot" { 210 | return true 211 | } 212 | } 213 | return false 214 | } 215 | 216 | func (i *Instance) RunningAsExclusiveMode() bool { 217 | m := machined.NewManager() 218 | machine, err := m.GetMachine(i.MachineId()) 219 | if err != nil { 220 | return false 221 | } 222 | leader, err := machine.Leader() 223 | if err != nil { 224 | log.Fatalln(err) 225 | } 226 | host, err := proc.GetParentProcessID(leader) 227 | if err != nil { 228 | log.Fatalln(err) 229 | } 230 | cmdline, err := proc.GetCommandLineByPID(host) 231 | if err != nil { 232 | log.Fatalln(err) 233 | } 234 | for _, arg := range cmdline { 235 | if arg == "-b" || arg == "--boot" { 236 | return false 237 | } 238 | } 239 | return true 240 | } 241 | 242 | func (i *Instance) Dir() string { 243 | return path.Join(i.BasePath, i.Name) 244 | } 245 | 246 | func (i *Instance) MachineId() string { 247 | return fmt.Sprintf("%s-%x", i.Name, ipc.GenFileKey(i.Parent.GetCiel().GetBasePath(), 0)) 248 | } 249 | 250 | func (i *Instance) FileSystemLock() ipc.Mutex { 251 | return ipc.NewMutex(i.Dir(), SemIdFileSystemMutex, true) 252 | } 253 | 254 | func (i *Instance) RunLock() ipc.Mutex { 255 | return ipc.NewMutex(i.Dir(), SemIdRunMutex, true) 256 | } 257 | 258 | func (i *Instance) Shell(user string) (string, error) { 259 | shell := "/bin/sh" 260 | passwdFileName := path.Join(i.MountPoint(), "/etc/passwd") 261 | a, err := ioutil.ReadFile(passwdFileName) 262 | if err != nil { 263 | return "", err 264 | } 265 | passwd := string(a) 266 | for _, userInfo := range strings.Split(passwd, "\n") { 267 | if userInfo == "" { 268 | continue 269 | } 270 | fields := strings.Split(userInfo, ":") 271 | if fields[0] == user { 272 | shell = fields[6] 273 | } 274 | } 275 | return shell, nil 276 | } 277 | 278 | func (i *Instance) GetContainer() abstract.Container { return i.Parent } 279 | -------------------------------------------------------------------------------- /internal/container/instance/lowlevel.go: -------------------------------------------------------------------------------- 1 | package instance 2 | 3 | /* 4 | #include 5 | #include 6 | #include 7 | 8 | struct termios tty_state; 9 | int stdin_flags; 10 | 11 | void save_stdin_attr() { 12 | stdin_flags = fcntl(0, F_GETFL, 0); 13 | } 14 | 15 | void recover_stdin_attr() { 16 | if (fcntl(0, F_GETFL, 0) != stdin_flags) { 17 | fcntl(0, F_SETFL, stdin_flags); 18 | putchar('\n'); 19 | } 20 | } 21 | 22 | void save_tty_attr() { 23 | tcgetattr(0, &tty_state); 24 | } 25 | 26 | void recover_tty_attr() { 27 | tcsetattr(0, TCSANOW, &tty_state); 28 | } 29 | 30 | void save_terminal_attr() { 31 | save_tty_attr(); 32 | save_stdin_attr(); 33 | } 34 | 35 | void recover_terminal_attr() { 36 | recover_tty_attr(); 37 | recover_stdin_attr(); 38 | } 39 | */ 40 | import "C" 41 | 42 | func init() { 43 | C.save_terminal_attr() 44 | } 45 | 46 | func RecoverTerminalAttr() { 47 | C.recover_terminal_attr() 48 | } 49 | -------------------------------------------------------------------------------- /internal/packaging/config.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | "path" 8 | 9 | "github.com/AOSC-Dev/ciel/internal/abstract" 10 | 11 | d "github.com/AOSC-Dev/ciel/display" 12 | ) 13 | 14 | // constant definitions for packaging related variables 15 | const ( 16 | DefaultEditor = "/usr/bin/editor" 17 | DefaultRepoConfig = "/etc/apt/sources.list.d/ciel-local.list" 18 | ) 19 | 20 | // EditSourceList : config function to let user manipulate the apt config inside the container 21 | func EditSourceList(global bool, i abstract.Instance, c abstract.Container) { 22 | var root string 23 | if global { 24 | root = c.DistDir() 25 | } else { 26 | root = i.MountPoint() 27 | } 28 | editor := editor() 29 | cmd := exec.Command(editor, path.Join(root, "/etc/apt/sources.list")) 30 | cmd.Stdin = os.Stdin 31 | cmd.Stdout = os.Stdout 32 | cmd.Stderr = os.Stderr 33 | cmd.Run() 34 | } 35 | 36 | // SetTreePath : config function to set the acbs search path 37 | func SetTreePath(global bool, i abstract.Instance, c abstract.Container, tree string) { 38 | var root string 39 | if global { 40 | root = c.DistDir() 41 | } else { 42 | root = i.MountPoint() 43 | } 44 | config := `[default]` + "\n" 45 | config += `location = ` + path.Clean(tree) + "\n" 46 | d.ITEM("set tree path") 47 | err := ioutil.WriteFile(path.Join(root, "/etc/acbs/forest.conf"), []byte(config), 0644) 48 | d.ERR(err) 49 | } 50 | 51 | // DisableDNSSEC : config function to disable DNSSEC service 52 | func DisableDNSSEC(global bool, i abstract.Instance, c abstract.Container) { 53 | var root string 54 | if global { 55 | root = c.DistDir() 56 | } else { 57 | root = i.MountPoint() 58 | } 59 | config := `[Resolve]` + "\n" 60 | config += `DNSSEC=no` + "\n" 61 | d.ITEM("disable DNSSEC") 62 | err := ioutil.WriteFile(path.Join(root, "/etc/systemd/resolved.conf"), []byte(config), 0644) 63 | d.ERR(err) 64 | } 65 | 66 | // SetMaintainer : config function to let user specify maintainer information 67 | func SetMaintainer(global bool, i abstract.Instance, c abstract.Container, person string) { 68 | var root string 69 | if global { 70 | root = c.DistDir() 71 | } else { 72 | root = i.MountPoint() 73 | } 74 | config := `#!/bin/bash` + "\n" 75 | config += `ABMPM=dpkg` + "\n" 76 | config += `ABAPMS=` + "\n" 77 | config += `MTER="` + person + `"` + "\n" 78 | config += `ABINSTALL=dpkg` + "\n" 79 | d.ITEM("set maintainer") 80 | err := ioutil.WriteFile(path.Join(root, "/usr/lib/autobuild3/etc/autobuild/ab3cfg.sh"), []byte(config), 0644) 81 | d.ERR(err) 82 | } 83 | 84 | // InitLocalRepo : initialize local repository 85 | func InitLocalRepo(global bool, i abstract.Instance, c abstract.Container) { 86 | var root string 87 | if global { 88 | root = c.DistDir() 89 | } else { 90 | root = i.MountPoint() 91 | } 92 | config := `deb file:///debs /` + "\n" 93 | d.ITEM("initialize local repository") 94 | err := ioutil.WriteFile(path.Join(root, DefaultRepoConfig), []byte(config), 0644) 95 | d.ERR(err) 96 | } 97 | 98 | // UnInitLocalRepo : remove local repository (configuration only) 99 | func UnInitLocalRepo(global bool, i abstract.Instance, c abstract.Container) { 100 | var root string 101 | if global { 102 | root = c.DistDir() 103 | } else { 104 | root = i.MountPoint() 105 | } 106 | d.ITEM("un-initialize local repository") 107 | err := os.Remove(path.Join(root, DefaultRepoConfig)) 108 | d.ERR(err) 109 | } 110 | 111 | func editor() string { 112 | if s := os.Getenv("VISUAL"); s != "" { 113 | return s 114 | } 115 | if s := os.Getenv("EDITOR"); s != "" { 116 | return s 117 | } 118 | return DefaultEditor 119 | } 120 | -------------------------------------------------------------------------------- /internal/packaging/output.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "syscall" 8 | 9 | d "github.com/AOSC-Dev/ciel/display" 10 | "github.com/AOSC-Dev/ciel/internal/abstract" 11 | "github.com/AOSC-Dev/ciel/proc-api" 12 | ) 13 | 14 | const ( 15 | OutputPath = "/debs" 16 | ) 17 | 18 | type Tree struct { 19 | Parent abstract.Ciel 20 | BasePath string 21 | } 22 | 23 | func (t *Tree) Mount(mountPoint string) { 24 | if _, err := os.Stat(t.BasePath); os.IsNotExist(err) { 25 | err = os.MkdirAll(t.BasePath, 0755) 26 | if err != nil { 27 | return 28 | } 29 | } 30 | outputMountPoint := path.Join(mountPoint, OutputPath) 31 | os.MkdirAll(outputMountPoint, 0755) 32 | if !proc.Mounted(outputMountPoint) { 33 | syscall.Mount(t.BasePath, outputMountPoint, "", syscall.MS_BIND, "") 34 | } 35 | } 36 | 37 | func (t *Tree) Unmount(mountPoint string) { 38 | outputMountPoint := path.Join(mountPoint, OutputPath) 39 | if _, err := os.Stat(outputMountPoint); os.IsNotExist(err) { 40 | return 41 | } 42 | if !proc.Mounted(outputMountPoint) { 43 | return 44 | } 45 | d.ITEM("unmount output") 46 | result, err := filepath.Abs(outputMountPoint) 47 | if err != nil { 48 | d.WARN(err) 49 | return 50 | } 51 | for proc.Mounted(result) { 52 | err = syscall.Unmount(result, syscall.MNT_FORCE) 53 | if err != nil { 54 | d.WARN(err) 55 | return 56 | } 57 | } 58 | d.WARN(err) 59 | d.ITEM("remove output mount point") 60 | err = os.Remove(outputMountPoint) 61 | d.WARN(err) 62 | } 63 | 64 | func (t *Tree) MountHandler(i abstract.Instance, mount bool) { 65 | if mount { 66 | t.Mount(i.MountPoint()) 67 | } else { 68 | t.Unmount(i.MountPoint()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /internal/packaging/toolchain.go: -------------------------------------------------------------------------------- 1 | package packaging 2 | 3 | import ( 4 | "os" 5 | "path" 6 | 7 | "github.com/AOSC-Dev/ciel/internal/abstract" 8 | 9 | d "github.com/AOSC-Dev/ciel/display" 10 | ) 11 | 12 | const ( 13 | AB3Path = "/usr/bin/autobuild" 14 | ACBSPath = "/usr/bin/acbs-build" 15 | ) 16 | 17 | type ToolChain struct { 18 | AB bool 19 | ACBS bool 20 | } 21 | 22 | func DetectToolChain(global bool, i abstract.Instance, c abstract.Container) *ToolChain { 23 | var root string 24 | if global { 25 | root = c.DistDir() 26 | } else { 27 | root = i.MountPoint() 28 | } 29 | tc := &ToolChain{} 30 | d.ITEM("detect autobuild3") 31 | tc.AB = exists(root, AB3Path) 32 | d.ITEM("detect acbs") 33 | tc.ACBS = exists(root, ACBSPath) 34 | return tc 35 | } 36 | 37 | func exists(root, target string) bool { 38 | _, err := os.Stat(path.Join(root, target)) 39 | if os.IsNotExist(err) { 40 | d.FAILED() 41 | return false 42 | } else if err == nil { 43 | d.OK() 44 | return true 45 | } else { 46 | d.FAILED_BECAUSE(err.Error()) 47 | return false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /internal/pkgtree/bind.go: -------------------------------------------------------------------------------- 1 | package pkgtree 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "syscall" 8 | 9 | d "github.com/AOSC-Dev/ciel/display" 10 | "github.com/AOSC-Dev/ciel/proc-api" 11 | ) 12 | 13 | const ( 14 | TreePath = "/tree" 15 | ) 16 | 17 | func (t *Tree) Mount(mountPoint string) { 18 | if _, err := os.Stat(t.BasePath); os.IsNotExist(err) { 19 | return 20 | } 21 | treeMountPoint := path.Join(mountPoint, TreePath) 22 | os.MkdirAll(treeMountPoint, 0755) 23 | if !proc.Mounted(treeMountPoint) { 24 | syscall.Mount(t.BasePath, treeMountPoint, "", syscall.MS_BIND, "") 25 | } 26 | } 27 | 28 | func (t *Tree) Unmount(mountPoint string) { 29 | treeMountPoint := path.Join(mountPoint, TreePath) 30 | if _, err := os.Stat(treeMountPoint); os.IsNotExist(err) { 31 | return 32 | } 33 | if !proc.Mounted(treeMountPoint) { 34 | return 35 | } 36 | d.ITEM("unmount tree") 37 | result, err := filepath.Abs(treeMountPoint) 38 | if err != nil { 39 | d.WARN(err) 40 | return 41 | } 42 | for proc.Mounted(result) { 43 | err = syscall.Unmount(result, syscall.MNT_FORCE) 44 | if err != nil { 45 | d.WARN(err) 46 | return 47 | } 48 | } 49 | d.WARN(err) 50 | if err != nil { 51 | return 52 | } 53 | d.ITEM("remove tree mount point") 54 | err = os.Remove(treeMountPoint) 55 | d.WARN(err) 56 | } 57 | -------------------------------------------------------------------------------- /internal/pkgtree/callbacks.go: -------------------------------------------------------------------------------- 1 | package pkgtree 2 | 3 | import ( 4 | "github.com/AOSC-Dev/ciel/internal/abstract" 5 | ) 6 | 7 | func (t *Tree) MountHandler(i abstract.Instance, mount bool) { 8 | if mount { 9 | t.Mount(i.MountPoint()) 10 | } else { 11 | t.Unmount(i.MountPoint()) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/pkgtree/clone.go: -------------------------------------------------------------------------------- 1 | package pkgtree 2 | 3 | import ( 4 | "os/exec" 5 | 6 | "log" 7 | "os" 8 | "syscall" 9 | 10 | "github.com/AOSC-Dev/ciel/internal/abstract" 11 | ) 12 | 13 | type Tree struct { 14 | Parent abstract.Ciel 15 | BasePath string 16 | } 17 | 18 | func (t *Tree) Clone(remote string) int { 19 | cmd := exec.Command("git", "clone", remote, t.BasePath) 20 | cmd.Stdin = os.Stdin 21 | cmd.Stdout = os.Stdout 22 | cmd.Stderr = os.Stderr 23 | err := cmd.Run() 24 | if exitErr, ok := err.(*exec.ExitError); ok { 25 | return exitErr.Sys().(syscall.WaitStatus).ExitStatus() 26 | } 27 | if err != nil { 28 | log.Fatalln(err) 29 | } 30 | return 0 31 | } 32 | 33 | func (t *Tree) Pull() int { 34 | cmd := exec.Command("git", "-C", t.BasePath, "pull", "--rebase") 35 | cmd.Stdin = os.Stdin 36 | cmd.Stdout = os.Stdout 37 | cmd.Stderr = os.Stderr 38 | err := cmd.Run() 39 | if exitErr, ok := err.(*exec.ExitError); ok { 40 | return exitErr.Sys().(syscall.WaitStatus).ExitStatus() 41 | } 42 | if err != nil { 43 | log.Fatalln(err) 44 | } 45 | return 0 46 | } 47 | -------------------------------------------------------------------------------- /internal/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log" 5 | "math/rand" 6 | "os" 7 | "time" 8 | ) 9 | 10 | func init() { 11 | rand.Seed(time.Now().UnixNano()) 12 | } 13 | 14 | func MustMkdir(p string) { 15 | if err := os.Mkdir(p, 0755); err != nil { 16 | log.Fatal(err) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ipc/lowlevel.go: -------------------------------------------------------------------------------- 1 | package ipc 2 | 3 | /* 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int get_sem_stat(int semid, struct semid_ds *buf) { 11 | return semctl(semid, 0, IPC_STAT, buf); 12 | } 13 | int init_sem(int semid, int val) { 14 | if (semctl(semid, 0, SETVAL, val-1) == -1) return -1; 15 | struct sembuf sop; 16 | sop.sem_num = 0; 17 | sop.sem_op = 1; 18 | sop.sem_flg = 0; 19 | return semop(semid, &sop, 1); 20 | } 21 | int remove_sem(int semid) { 22 | return semctl(semid, 0, IPC_RMID); 23 | } 24 | int op_sem(int semid, int op, int flag) { 25 | struct sembuf sop; 26 | sop.sem_num = 0; 27 | sop.sem_op = op; 28 | sop.sem_flg = flag; 29 | return semop(semid, &sop, 1); 30 | } 31 | int get_sem(int semid) { 32 | unsigned short i; 33 | semctl(semid, 0, GETALL, &i); 34 | return i; 35 | } 36 | void try_wait_sem_undo(int semid) { 37 | op_sem(semid, -1, SEM_UNDO | IPC_NOWAIT); 38 | } 39 | void wait_sem_undo(int semid) { 40 | op_sem(semid, -1, SEM_UNDO); 41 | } 42 | void signal_sem_undo(int semid) { 43 | op_sem(semid, 1, SEM_UNDO); 44 | } 45 | void try_wait_sem(int semid) { 46 | op_sem(semid, -1, IPC_NOWAIT); 47 | } 48 | void wait_sem(int semid) { 49 | op_sem(semid, -1, 0); 50 | } 51 | void signal_sem(int semid) { 52 | op_sem(semid, 1, 0); 53 | } 54 | */ 55 | import "C" 56 | 57 | import ( 58 | "log" 59 | "syscall" 60 | "time" 61 | "unsafe" 62 | ) 63 | 64 | const ( 65 | BusyWaitTimeout = time.Millisecond * 500 66 | ) 67 | 68 | func GenFileKey(pathName string, projectId int) C.key_t { 69 | c := C.CString(pathName) 70 | defer C.free(unsafe.Pointer(c)) 71 | r, err := C.ftok(c, C.int(projectId)) 72 | if err != nil { 73 | log.Panicln("ftok:", pathName, projectId, err) 74 | } 75 | return r 76 | } 77 | 78 | type Semaphore C.int 79 | 80 | func NewSemaphore(pathName string, projectId int, initVal int) Semaphore { 81 | semKey := GenFileKey(pathName, projectId) 82 | semId, err := C.semget(semKey, 1, 0600|C.IPC_CREAT|C.IPC_EXCL) 83 | if err == nil { 84 | _, err = C.init_sem(semId, C.int(initVal)) 85 | if err != nil { 86 | log.Fatalln(err) 87 | } 88 | } else if err == syscall.EEXIST { 89 | semId, err = C.semget(semKey, 1, 0600) 90 | if err != nil { 91 | log.Fatalln(err) 92 | } 93 | var sds C.struct_semid_ds 94 | var timeout = time.After(BusyWaitTimeout) 95 | for sds.sem_otime == 0 { 96 | select { 97 | case <-timeout: 98 | _, err = C.init_sem(semId, C.int(initVal)) 99 | log.Println("busywait: timeout", pathName, projectId) 100 | if err != nil { 101 | log.Fatalln(err) 102 | } 103 | default: 104 | C.sched_yield() 105 | _, err = C.get_sem_stat(semId, &sds) 106 | if err != nil { 107 | log.Fatalln(err) 108 | } 109 | } 110 | } 111 | } else { 112 | log.Fatalln(err) 113 | } 114 | return Semaphore(semId) 115 | } 116 | func (s Semaphore) Wait() { C.wait_sem_undo(C.int(s)) } 117 | func (s Semaphore) Signal() { C.signal_sem_undo(C.int(s)) } 118 | func (s Semaphore) TryWait() bool { 119 | _, err := C.try_wait_sem_undo(C.int(s)) 120 | return err == nil 121 | } 122 | func (s Semaphore) WaitHold() { C.wait_sem(C.int(s)) } 123 | func (s Semaphore) SignalHold() { C.signal_sem(C.int(s)) } 124 | func (s Semaphore) TryWaitHold() bool { 125 | _, err := C.try_wait_sem(C.int(s)) 126 | return err == nil 127 | } 128 | func (s Semaphore) Get() (int, error) { 129 | i, err := C.get_sem(C.int(s)) 130 | return int(i), err 131 | } 132 | func (s Semaphore) Remove() error { 133 | _, err := C.remove_sem(C.int(s)) 134 | return err 135 | } 136 | 137 | type Mutex struct { 138 | S Semaphore 139 | hold bool 140 | } 141 | 142 | func NewMutex(pathName string, projectId int, symmetric bool) Mutex { 143 | return Mutex{NewSemaphore(pathName, projectId, 1), !symmetric} 144 | } 145 | func (m Mutex) Lock() { 146 | if m.hold { 147 | m.S.WaitHold() 148 | } else { 149 | m.S.Wait() 150 | } 151 | } 152 | func (m Mutex) Unlock() { 153 | if m.hold { 154 | m.S.SignalHold() 155 | } else { 156 | m.S.Signal() 157 | } 158 | } 159 | func (m Mutex) TryLock() bool { 160 | if m.hold { 161 | return m.S.TryWaitHold() 162 | } else { 163 | return m.S.TryWait() 164 | } 165 | } 166 | func (m Mutex) Remove() error { 167 | return m.S.Remove() 168 | } 169 | func (m Mutex) Get() (int, error) { 170 | return m.S.Get() 171 | } 172 | -------------------------------------------------------------------------------- /overlayfs/ciel.go: -------------------------------------------------------------------------------- 1 | package overlayfs 2 | 3 | import ( 4 | "os" 5 | "path" 6 | ) 7 | 8 | func Create(layerPath string) error { 9 | var Layers = []string{ 10 | "local", 11 | "diff", 12 | } 13 | if err := os.Mkdir(layerPath, 0755); err != nil { 14 | return err 15 | } 16 | for _, layer := range Layers { 17 | if err := os.Mkdir(path.Join(layerPath, layer), 0755); err != nil { 18 | return err 19 | } 20 | } 21 | return nil 22 | } 23 | 24 | func FromPath(basePath, layerPath string) *Instance { 25 | var Layers = []string{ 26 | "local", 27 | "diff", 28 | } 29 | var layers = []string{basePath} 30 | for _, layer := range Layers { 31 | layers = append(layers, path.Join(layerPath, layer)) 32 | } 33 | return &Instance{ 34 | Layers: layers, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /overlayfs/merge.go: -------------------------------------------------------------------------------- 1 | package overlayfs 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "syscall" 9 | ) 10 | 11 | func removeIfExist(path string) error { 12 | err := os.RemoveAll(path) 13 | if err != nil && !os.IsNotExist(err) { 14 | return err 15 | } 16 | return nil 17 | } 18 | 19 | func override(upPath, lowPath string) error { 20 | if err := removeIfExist(lowPath); err != nil { 21 | return err 22 | } 23 | return os.Rename(upPath, lowPath) 24 | } 25 | 26 | func removeBoth(upPath, lowPath string) error { 27 | if err := removeIfExist(lowPath); err != nil { 28 | return err 29 | } 30 | return os.Remove(upPath) 31 | } 32 | 33 | // MergeFile is the method to merge a file or directory from an upper layer 34 | // to a lower layer. 35 | func (i *Instance) Merge() error { 36 | upRoot, lowRoot := i.Layers[len(i.Layers)-1], i.Layers[0] 37 | err := filepath.Walk(upRoot, func(upPath string, info os.FileInfo, err error) error { 38 | relPath, _ := filepath.Rel(upRoot, upPath) 39 | lowPath := filepath.Join(lowRoot, relPath) 40 | 41 | upType, err := overlayTypeByInfo(info, err) 42 | if err != nil { 43 | return err 44 | } 45 | lowType, err := overlayTypeByLstat(lowPath) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | // n f w d 51 | // n - o r r (skip sub-directories) 52 | // f - o r o 53 | // w - o r o 54 | // d - o r c 55 | 56 | // o = override 57 | // r = remove both 58 | // m = move 59 | // c = copy attributes 60 | // s = skip sub-directories 61 | 62 | switch upType { 63 | case overlayTypeNothing: 64 | return nil 65 | 66 | case overlayTypeFile: 67 | return override(upPath, lowPath) 68 | 69 | case overlayTypeWhiteout: 70 | return removeBoth(upPath, lowPath) 71 | 72 | case overlayTypeDir: 73 | switch lowType { 74 | case overlayTypeNothing: 75 | if err := override(upPath, lowPath); err != nil { 76 | return err 77 | } 78 | return filepath.SkipDir 79 | 80 | case overlayTypeFile: 81 | if err := override(upPath, lowPath); err != nil { 82 | return err 83 | } 84 | return filepath.SkipDir 85 | 86 | case overlayTypeWhiteout: 87 | // strange case. a whiteout file in the lowest layer? 88 | if err := override(upPath, lowPath); err != nil { 89 | return err 90 | } 91 | return filepath.SkipDir 92 | 93 | case overlayTypeDir: 94 | err := copyAttributes(upPath, lowPath) 95 | // remove empty directory 96 | if err := os.Remove(upPath); err == nil { 97 | return filepath.SkipDir 98 | } 99 | return err 100 | } 101 | } 102 | panic("unexpected type") 103 | }) 104 | // end of walk-function 105 | if err == nil { 106 | list, err := ioutil.ReadDir(upRoot) 107 | if err != nil { 108 | return err 109 | } 110 | if len(list) != 0 { 111 | for _, info := range list { 112 | os.RemoveAll(info.Name()) 113 | } 114 | } 115 | } 116 | return err 117 | } 118 | 119 | type overlayType int 120 | 121 | const ( 122 | overlayTypeNothing overlayType = iota 123 | overlayTypeFile 124 | overlayTypeWhiteout 125 | overlayTypeDir 126 | ) 127 | 128 | func copyAttributes(src, dst string) error { 129 | // TODO: copying attributes, NOT recursively 130 | args := []string{ 131 | "--preserve=all", 132 | "--attributes-only", 133 | "--no-target-directory", 134 | "--recursive", // BUG: cp cannot do this not recursively 135 | "--no-clobber", 136 | src, 137 | dst, 138 | } 139 | exec.Command("/bin/cp", args...).Run() 140 | return nil 141 | } 142 | 143 | func overlayTypeByLstat(path string) (overlayType, error) { 144 | return overlayTypeByInfo(os.Lstat(path)) 145 | } 146 | 147 | func overlayTypeByInfo(info os.FileInfo, err error) (overlayType, error) { 148 | if os.IsNotExist(err) { 149 | return overlayTypeNothing, nil 150 | } else if err != nil { 151 | return overlayTypeNothing, err 152 | } 153 | if info.IsDir() { 154 | return overlayTypeDir, nil 155 | } 156 | if isWhiteout(info) { 157 | return overlayTypeWhiteout, nil 158 | } 159 | return overlayTypeFile, nil 160 | } 161 | 162 | func isWhiteout(fi os.FileInfo) bool { 163 | const mask = os.ModeDevice | os.ModeCharDevice 164 | if fi.Mode()&mask != mask { 165 | return false 166 | } 167 | return fi.Sys().(*syscall.Stat_t).Rdev == 0 168 | } 169 | -------------------------------------------------------------------------------- /overlayfs/overlayfs.go: -------------------------------------------------------------------------------- 1 | package overlayfs 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "syscall" 9 | 10 | d "github.com/AOSC-Dev/ciel/display" 11 | ) 12 | 13 | type Instance struct { 14 | MountPoint string 15 | Layers []string 16 | } 17 | 18 | const TmpDirSuffix = ".tmp" 19 | 20 | // https://www.kernel.org/doc/Documentation/filesystems/overlayfs.txt 21 | // 22 | // Multiple lower layers 23 | // --------------------- 24 | // 25 | // Multiple lower layers can now be given using the the colon (":") as a 26 | // separator character between the directory names. For example: 27 | // 28 | // mount -t overlay overlay -olowerdir=/lower1:/lower2:/lower3 /merged 29 | // 30 | // As the example shows, "upperdir=" and "workdir=" may be omitted. In 31 | // that case the overlay will be read-only. 32 | // 33 | // The specified lower directories will be stacked beginning from the 34 | // rightmost one and going left. In the above example lower1 will be the 35 | // top, lower2 the middle and lower3 the bottom layer. 36 | 37 | func (i *Instance) MountLocal() error { 38 | var localInst Instance 39 | localInst = *i 40 | localInst.Layers = localInst.Layers[:len(localInst.Layers)-1] 41 | return localInst.Mount(false) 42 | } 43 | 44 | func (i *Instance) Mount(readOnly bool) error { 45 | var option string 46 | var layers = make([]string, len(i.Layers)) 47 | for index := range layers { 48 | layers[index] = filepath.Clean(i.Layers[len(layers)-1-index]) // reverse i.Layers and assign it to layers 49 | } 50 | if readOnly { 51 | option = "lowerdir=" + strings.Join(layers, ":") 52 | } else { 53 | olfsLowerdirs := layers[1:] 54 | olfsUpperdir := layers[0] 55 | olfsWorkdir := olfsUpperdir + TmpDirSuffix 56 | os.MkdirAll(olfsWorkdir, 0755) 57 | option = 58 | "lowerdir=" + strings.Join(olfsLowerdirs, ":") + 59 | ",upperdir=" + olfsUpperdir + 60 | ",workdir=" + olfsWorkdir 61 | } 62 | os.MkdirAll(i.MountPoint, 0755) 63 | err := syscall.Mount("overlay", i.MountPoint, "overlay", 0, option) 64 | return err 65 | } 66 | 67 | func (i *Instance) Unmount() error { 68 | err := syscall.Unmount(i.MountPoint, 0) 69 | if err == nil { 70 | if len(i.Layers) > 0 { 71 | os.RemoveAll(filepath.Clean(i.Layers[len(i.Layers)-2]) + TmpDirSuffix) 72 | os.RemoveAll(filepath.Clean(i.Layers[len(i.Layers)-1]) + TmpDirSuffix) 73 | } 74 | } 75 | return err 76 | } 77 | 78 | func (i *Instance) Rollback() error { 79 | d.ITEM("get diff dir") 80 | layers := i.Layers 81 | dir := layers[len(layers)-1] 82 | d.Println(d.C(d.WHITE, dir)) 83 | 84 | d.ITEM("clean diff dir") 85 | fi, err := ioutil.ReadDir(dir) 86 | if err == nil { 87 | for _, f := range fi { 88 | err = os.RemoveAll(filepath.Join(dir, f.Name())) 89 | } 90 | d.ERR(err) 91 | } else if os.IsNotExist(err) { 92 | d.SKIPPED() 93 | } else { 94 | d.FAILED_BECAUSE(err.Error()) 95 | return err 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /plugin/ciel-generate: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: ciel-generate , one recipe at a time. 3 | 4 | export CIEL_INST="ciel--generate--" 5 | export CIEL_BATCH_MODE="true" 6 | 7 | # Determine architecture of build host. 8 | BUILD_ARCH="$(dpkg-architecture -qDEB_BUILD_ARCH)" 9 | 10 | # Determine the recipe to build. 11 | VARIANT="${1/+/_}" 12 | 13 | # FIXME: Handy variable. 14 | CALCULATED_RECIPE="${VARIANT^^}_RECIPE" 15 | 16 | # Common denominators. 17 | PREREQ_RECIPE="systemd" 18 | if [[ "${VARIANT}" = *sunxi* && "${VARIANT}" = *rk* ]]; then 19 | BOOTABLE_RECIPE="boot-base aosc-os-presets-base" 20 | else 21 | BOOTABLE_RECIPE="boot-base kernel-base aosc-os-presets-base" 22 | fi 23 | FONT_RECIPE="noto-cjk-fonts noto-fonts hanazono-fonts adobe-source-code-pro \ 24 | adobe-source-serif-pro croscore-fonts crosextra-fonts \ 25 | dejavu-fonts freefont liberation-fonts roboto-fonts \ 26 | ttf-tibetan-machine-unicode ttf-ancient-fonts unifont" 27 | 28 | # Define default browser. 29 | # Firefox does not work well on PPC64. 30 | if [[ "${BUILD_ARCH}" = "ppc64" ]]; then 31 | BROWSER_RECIPE="palemoon" 32 | else 33 | BROWSER_RECIPE="firefox" 34 | fi 35 | BROWSER_RECIPE+=" thunderbird" 36 | 37 | # Available from all desktop variants. 38 | DESKTOP_RECIPE="${BROWSER_RECIPE} ${FONT_RECIPE} productivity-base \ 39 | print-base codec-base proofread-base xdg-user-dirs \ 40 | xdg-user-dirs-gtk imchooser aosc-os-presets-desktop \ 41 | aosc-community-wallpapers" 42 | 43 | USE_DKMS=0 44 | # Special case for sunxi-base. 45 | if [[ "$VARIANT" == sunxi-* ]]; then 46 | if [[ ! "$DEVICE_NAME" ]]; then 47 | echo "DEVICE_NAME environment variable not set!" >&2 48 | exit 1 49 | fi 50 | case $DEVICE_NAME in 51 | sun[45]i*) 52 | BOOTABLE_RECIPE="linux+kernel+sunxi+nokvm" ;; 53 | sun[6789]i*) 54 | BOOTABLE_RECIPE="linux+kernel+sunxi+kvm" ;; 55 | *) 56 | echo "Not recognized DEVICE_NAME!" >&2 57 | exit 1 ;; 58 | esac 59 | BOOTABLE_RECIPE+=" u-boot-aosc-utils u-boot-$DEVICE_NAME" 60 | case $DEVICE_NAME in 61 | sun7i-a20-cubietruck|sun7i-a20-bananapi-m1-plus) 62 | BOOTABLE_RECIPE+=" firmware-wifi-ap6210" ;; 63 | sun8i-h2-plus-bananapi-m2-zero|sun8i-h3-bananapi-m2-plus|sun8i-r40-bananapi-m2-ultra) 64 | BOOTABLE_RECIPE+=" firmware-wifi-ap6212" ;; 65 | sun8i-h3-orangepi-plus) 66 | BOOTABLE_RECIPE+=" rtl8189es" 67 | USE_DKMS=1 ;; 68 | esac 69 | BOOTABLE_RECIPE+=" firmware-free firmware-nonfree" 70 | BASE_VARIANT="$(echo $VARIANT | cut -d - -f 2-)" 71 | CALCULATED_RECIPE="${BASE_VARIANT^^}_RECIPE" 72 | fi 73 | 74 | # Special case for sunxi64-base. 75 | if [[ "$VARIANT" == sunxi64-* ]]; then 76 | if [[ ! "$DEVICE_NAME" ]]; then 77 | echo "DEVICE_NAME environment variable not set!" >&2 78 | exit 1 79 | fi 80 | BOOTABLE_RECIPE="linux+kernel+sunxi64 u-boot-aosc-utils u-boot-$DEVICE_NAME" 81 | case $DEVICE_NAME in 82 | sun50i-a64-bananapi-m64) 83 | BOOTABLE_RECIPE+=" firmware-wifi-ap6212" ;; 84 | sun50i-a64-pinebook|sun50i-a64-pinetab*|sun50i-a64-pinephone*) 85 | BOOTABLE_RECIPE+=" rtl8723cs" 86 | USE_DKMS=1 ;; 87 | esac 88 | BOOTABLE_RECIPE+=" firmware-free firmware-nonfree" 89 | BASE_VARIANT="$(echo $VARIANT | cut -d - -f 2-)" 90 | CALCULATED_RECIPE="${BASE_VARIANT^^}_RECIPE" 91 | fi 92 | 93 | # Special case for rk64-base 94 | if [[ "$VARIANT" == rk64-* ]]; then 95 | if [[ ! "$DEVICE_NAME" ]]; then 96 | echo "DEVICE_NAME environment variable not set!" >&2 97 | exit 1 98 | fi 99 | BOOTABLE_RECIPE="linux+kernel+rk64 u-boot-aosc-utils u-boot-$DEVICE_NAME" 100 | BOOTABLE_RECIPE+=" firmware-free firmware-nonfree" 101 | BASE_VARIANT="$(echo $VARIANT | cut -d - -f 2-)" 102 | CALCULATED_RECIPE="${BASE_VARIANT^^}_RECIPE" 103 | fi 104 | 105 | if (($USE_DKMS)); then 106 | BOOTABLE_RECIPE+=" gcc dkms" 107 | fi 108 | 109 | # Specific recipes of all variants. 110 | CONTAINER_RECIPE="admin-base core-base editor-base python-base network-base \ 111 | systemd-base web-base util-base" 112 | BASE_RECIPE="${CONTAINER_RECIPE} ${BOOTABLE_RECIPE} alsa-utils" 113 | BUILDKIT_RECIPE="${CONTAINER_RECIPE} devel-base debug-base git autobuild3 acbs" 114 | SERVER_RECIPE="${BASE_RECIPE} server-base" 115 | CINNAMON_RECIPE="${BASE_RECIPE} ${DESKTOP_RECIPE} cinnamon-base cinnamon-distro-base ibus-base \ 116 | lightdm-gtk-greeter-settings gnome-packagekit gnome-software \ 117 | cinnamon-default-settings alsa-utils aosc-os-presets-cinnamon" 118 | CINNAMON_NVIDIA_RECIPE="${CINNAMON_RECIPE} nvidia" 119 | CINNAMON_NVIDIA340_RECIPE="${CINNAMON_RECIPE} nvidia+340 nvidia-libgl+340" 120 | CINNAMON_NVIDIA390_RECIPE="${CINNAMON_RECIPE} nvidia+390" 121 | GNOME_RECIPE="${BASE_RECIPE} ${DESKTOP_RECIPE} gnome-base ibus-base gnome-distro-base \ 122 | gnome-default-settings alsa-utils aosc-os-presets-gnome" 123 | GNOME_NVIDIA_RECIPE="${GNOME_RECIPE} nvidia" 124 | GNOME_NVIDIA340_RECIPE="${GNOME_RECIPE} nvidia+340 nvidia-libgl+340" 125 | GNOME_NVIDIA390_RECIPE="${GNOME_RECIPE} nvidia+390" 126 | KDE_RECIPE="${BASE_RECIPE} ${DESKTOP_RECIPE} kde-base fcitx-base sddm mpv kdeplasma-addons \ 127 | alsa-utils plasma-distro-base aosc-os-presets-plasma" 128 | KDE_NVIDIA_RECIPE="${KDE_RECIPE} nvidia" 129 | KDE_NVIDIA340_RECIPE="${KDE_RECIPE} nvidia+340 nvidia-libgl+340" 130 | KDE_NVIDIA390_RECIPE="${KDE_RECIPE} nvidia+390" 131 | LXDE_RECIPE="${BASE_RECIPE} ${DESKTOP_RECIPE} lxde-base lxde-distro-base ibus-base \ 132 | lightdm-gtk-greeter-settings lxde-default-settings \ 133 | arc-icon-theme network-manager-applet \ 134 | gnome-screenshot volumeicon alsa-utils aosc-os-presets-lxde" 135 | LXDE_NVIDIA_RECIPE="${LXDE_RECIPE} nvidia" 136 | LXDE_NVIDIA340_RECIPE="${LXDE_RECIPE} nvidia+340 nvidia-libgl+340" 137 | LXDE_NVIDIA390_RECIPE="${LXDE_RECIPE} nvidia+390" 138 | MATE_RECIPE="${BASE_RECIPE} ${DESKTOP_RECIPE} mate-base mate-distro-base ibus-base \ 139 | lightdm-gtk-greeter-settings mpv mate-default-settings alsa-utils \ 140 | aosc-os-presets-mate" 141 | MATE_NVIDIA_RECIPE="${MATE_RECIPE} nvidia" 142 | MATE_NVIDIA340_RECIPE="${MATE_RECIPE} nvidia+340 nvidia-libgl+340" 143 | MATE_NVIDIA390_RECIPE="${MATE_RECIPE} nvidia+390" 144 | XFCE_RECIPE="${BASE_RECIPE} ${DESKTOP_RECIPE} xfce-base xfce-distro-base ibus-base \ 145 | lightdm-gtk-greeter-settings mpv xfce4-default-settings alsa-utils \ 146 | aosc-os-presets-xfce" 147 | XFCE_NVIDIA_RECIPE="${XFCE_RECIPE} nvidia" 148 | XFCE_NVIDIA340_RECIPE="${XFCE_RECIPE} nvidia+340 nvidia-libgl+340" 149 | XFCE_NVIDIA390_RECIPE="${XFCE_RECIPE} nvidia+390" 150 | 151 | # Special case for Retro. 152 | if [[ "$VARIANT" == retro-* ]]; then 153 | BOOTABLE_RECIPE="boot-base kernel-base aosc-os-presets-base" 154 | FONT_RECIPE="unifont" 155 | BROWSER_RECIPE="netsurf sylpheed" 156 | DESKTOP_RECIPE="${BROWSER_RECIPE} ${FONT_RECIPE} \ 157 | xdg-user-dirs aosc-os-presets-desktop" 158 | BASE_RECIPE="${BOOTABLE_RECIPE} admin-base core-base editor-base \ 159 | network-base systemd-base web-base util-base" 160 | X11_RECIPE="${BASE_RECIPE} ${DESKTOP_RECIPE} retro-x11-base" 161 | fi 162 | 163 | # Common functions. 164 | _recipe_post() { 165 | ciel factory-reset 166 | ciel commit 167 | ciel del $CIEL_INST 168 | } 169 | 170 | _recipe_pre_install() { 171 | ciel update-os 172 | ciel add $CIEL_INST 173 | ciel shell "apt-get -o Dpkg::Options::=\"--force-confnew\" install --yes ${PREREQ_RECIPE}" 174 | } 175 | 176 | # Recipe-specific functions. 177 | _recipe_install() { 178 | ciel shell "apt-get -o Dpkg::Options::=\"--force-confnew\" install --yes ${!CALCULATED_RECIPE}" 179 | ciel shell "apt-get autoremove --purge --yes" 180 | } 181 | _recipe_base_config() { 182 | ciel shell "systemctl preset-all" 183 | } 184 | _recipe_sunxi-base_pre_install() { 185 | ciel shell "apt-gen-list c +bsp-sunxi" 186 | ciel commit 187 | ciel shell "apt-get update" 188 | } 189 | _recipe_sunxi-base_config(){ 190 | ciel shell "systemctl preset-all" 191 | } 192 | _recipe_sunxi64-base_pre_install() { 193 | ciel shell "apt-gen-list c +bsp-sunxi" 194 | ciel commit 195 | ciel shell "apt-get update" 196 | } 197 | _recipe_sunxi64-base_config(){ 198 | ciel shell "systemctl preset-all" 199 | if (($USE_DKMS)); then 200 | ciel shell "systemctl enable dkms.service" 201 | fi 202 | } 203 | _recipe_sunxi64-cinnamon_pre_install() { 204 | ciel shell "apt-gen-list c +bsp-sunxi" 205 | ciel commit 206 | ciel shell "apt-get update" 207 | } 208 | _recipe_sunxi64-cinnamon_config(){ 209 | ciel shell "systemctl preset-all" 210 | if (($USE_DKMS)); then 211 | ciel shell "systemctl enable dkms.service" 212 | fi 213 | } 214 | _recipe_sunxi64-gnome_pre_install() { 215 | ciel shell "apt-gen-list c +bsp-sunxi" 216 | ciel commit 217 | ciel shell "apt-get update" 218 | } 219 | _recipe_sunxi64-gnome_config(){ 220 | ciel shell "systemctl preset-all" 221 | if (($USE_DKMS)); then 222 | ciel shell "systemctl enable dkms.service" 223 | fi 224 | } 225 | _recipe_sunxi64-kde_pre_install() { 226 | ciel shell "apt-gen-list c +bsp-sunxi" 227 | ciel commit 228 | ciel shell "apt-get update" 229 | } 230 | _recipe_sunxi64-kde_config(){ 231 | ciel shell "systemctl preset-all" 232 | if (($USE_DKMS)); then 233 | ciel shell "systemctl enable dkms.service" 234 | fi 235 | } 236 | _recipe_sunxi64-lxde_pre_install() { 237 | ciel shell "apt-gen-list c +bsp-sunxi" 238 | ciel commit 239 | ciel shell "apt-get update" 240 | } 241 | _recipe_sunxi64-lxde_config(){ 242 | ciel shell "systemctl preset-all" 243 | if (($USE_DKMS)); then 244 | ciel shell "systemctl enable dkms.service" 245 | fi 246 | } 247 | _recipe_sunxi64-mate_pre_install() { 248 | ciel shell "apt-gen-list c +bsp-sunxi" 249 | ciel commit 250 | ciel shell "apt-get update" 251 | } 252 | _recipe_sunxi64-mate_config(){ 253 | ciel shell "systemctl preset-all" 254 | if (($USE_DKMS)); then 255 | ciel shell "systemctl enable dkms.service" 256 | fi 257 | } 258 | _recipe_sunxi64-xfce_pre_install() { 259 | ciel shell "apt-gen-list c +bsp-sunxi" 260 | ciel commit 261 | ciel shell "apt-get update" 262 | } 263 | _recipe_sunxi64-xfce_config(){ 264 | ciel shell "systemctl preset-all" 265 | if (($USE_DKMS)); then 266 | ciel shell "systemctl enable dkms.service" 267 | fi 268 | } 269 | _recipe_rk64-base_pre_install() { 270 | ciel shell "apt-gen-list c +bsp-rk" 271 | ciel commit 272 | ciel shell "apt-get update" 273 | } 274 | _recipe_rk64-base_config(){ 275 | ciel shell "systemctl preset-all" 276 | if (($USE_DKMS)); then 277 | ciel shell "systemctl enable dkms.service" 278 | fi 279 | } 280 | _recipe_rk64-cinnamon_pre_install() { 281 | ciel shell "apt-gen-list c +bsp-rk" 282 | ciel commit 283 | ciel shell "apt-get update" 284 | } 285 | _recipe_rk64-cinnamon_config(){ 286 | ciel shell "systemctl preset-all" 287 | if (($USE_DKMS)); then 288 | ciel shell "systemctl enable dkms.service" 289 | fi 290 | } 291 | _recipe_rk64-gnome_pre_install() { 292 | ciel shell "apt-gen-list c +bsp-rk" 293 | ciel commit 294 | ciel shell "apt-get update" 295 | } 296 | _recipe_rk64-gnome_config(){ 297 | ciel shell "systemctl preset-all" 298 | if (($USE_DKMS)); then 299 | ciel shell "systemctl enable dkms.service" 300 | fi 301 | } 302 | _recipe_rk64-kde_pre_install() { 303 | ciel shell "apt-gen-list c +bsp-rk" 304 | ciel commit 305 | ciel shell "apt-get update" 306 | } 307 | _recipe_rk64-kde_config(){ 308 | ciel shell "systemctl preset-all" 309 | if (($USE_DKMS)); then 310 | ciel shell "systemctl enable dkms.service" 311 | fi 312 | } 313 | _recipe_rk64-lxde_pre_install() { 314 | ciel shell "apt-gen-list c +bsp-rk" 315 | ciel commit 316 | ciel shell "apt-get update" 317 | } 318 | _recipe_rk64-lxde_config(){ 319 | ciel shell "systemctl preset-all" 320 | if (($USE_DKMS)); then 321 | ciel shell "systemctl enable dkms.service" 322 | fi 323 | } 324 | _recipe_rk64-mate_pre_install() { 325 | ciel shell "apt-gen-list c +bsp-rk" 326 | ciel commit 327 | ciel shell "apt-get update" 328 | } 329 | _recipe_rk64-mate_config(){ 330 | ciel shell "systemctl preset-all" 331 | if (($USE_DKMS)); then 332 | ciel shell "systemctl enable dkms.service" 333 | fi 334 | } 335 | _recipe_rk64-xfce_pre_install() { 336 | ciel shell "apt-gen-list c +bsp-rk" 337 | ciel commit 338 | ciel shell "apt-get update" 339 | } 340 | _recipe_rk64-xfce_config(){ 341 | ciel shell "systemctl preset-all" 342 | if (($USE_DKMS)); then 343 | ciel shell "systemctl enable dkms.service" 344 | fi 345 | } 346 | _recipe_buildkit_config() { 347 | ciel shell "systemctl preset-all" 348 | } 349 | _recipe_container_config() { 350 | ciel shell "systemctl preset-all" 351 | } 352 | _recipe_server_config() { 353 | ciel shell "systemctl preset-all" 354 | } 355 | _recipe_cinnamon_config() { 356 | ciel shell "systemctl preset-all" 357 | } 358 | _recipe_cinnamon_nvidia_config() { 359 | ciel shell "systemctl preset-all" 360 | ciel shell "systemctl enable nvidia-persistenced" 361 | } 362 | _recipe_cinnamon_nvidia340_config() { 363 | ciel shell "systemctl preset-all" 364 | ciel shell "systemctl enable nvidia-persistenced" 365 | } 366 | _recipe_cinnamon_nvidia390_config() { 367 | ciel shell "systemctl preset-all" 368 | ciel shell "systemctl enable nvidia-persistenced" 369 | } 370 | _recipe_gnome_config() { 371 | ciel shell "systemctl preset-all" 372 | } 373 | _recipe_gnome_nvidia_config() { 374 | ciel shell "systemctl preset-all" 375 | ciel shell "systemctl enable nvidia-persistenced" 376 | } 377 | _recipe_gnome_nvidia340_config() { 378 | ciel shell "systemctl preset-all" 379 | ciel shell "systemctl enable nvidia-persistenced" 380 | } 381 | _recipe_gnome_nvidia390_config() { 382 | ciel shell "systemctl preset-all" 383 | ciel shell "systemctl enable nvidia-persistenced" 384 | } 385 | _recipe_kde_config() { 386 | ciel shell "systemctl preset-all" 387 | } 388 | _recipe_kde_nvidia_config() { 389 | ciel shell "systemctl preset-all" 390 | ciel shell "systemctl enable nvidia-persistenced" 391 | } 392 | _recipe_kde_nvidia340_config() { 393 | ciel shell "systemctl preset-all" 394 | ciel shell "systemctl enable nvidia-persistenced" 395 | } 396 | _recipe_kde_nvidia390_config() { 397 | ciel shell "systemctl preset-all" 398 | ciel shell "systemctl enable nvidia-persistenced" 399 | } 400 | _recipe_lxde_config() { 401 | ciel shell "systemctl preset-all" 402 | } 403 | _recipe_lxde_nvidia_config() { 404 | ciel shell "systemctl preset-all" 405 | ciel shell "systemctl enable nvidia-persistenced" 406 | } 407 | _recipe_lxde_nvidia340_config() { 408 | ciel shell "systemctl preset-all" 409 | ciel shell "systemctl enable nvidia-persistenced" 410 | } 411 | _recipe_lxde_nvidia390_config() { 412 | ciel shell "systemctl preset-all" 413 | ciel shell "systemctl enable nvidia-persistenced" 414 | } 415 | _recipe_mate_config() { 416 | ciel shell "systemctl preset-all" 417 | } 418 | _recipe_mate_nvidia_config() { 419 | ciel shell "systemctl preset-all" 420 | ciel shell "systemctl enable nvidia-persistenced" 421 | } 422 | _recipe_mate_nvidia340_config() { 423 | ciel shell "systemctl preset-all" 424 | ciel shell "systemctl enable nvidia-persistenced" 425 | } 426 | _recipe_mate_nvidia390_config() { 427 | ciel shell "systemctl preset-all" 428 | ciel shell "systemctl enable nvidia-persistenced" 429 | } 430 | _recipe_xfce_config() { 431 | ciel shell "systemctl preset-all" 432 | } 433 | _recipe_xfce_nvidia_config() { 434 | ciel shell "systemctl preset-all" 435 | ciel shell "systemctl enable nvidia-persistenced" 436 | } 437 | _recipe_xfce_nvidia340_config() { 438 | ciel shell "systemctl preset-all" 439 | ciel shell "systemctl enable nvidia-persistenced" 440 | } 441 | _recipe_xfce_nvidia390_config() { 442 | ciel shell "systemctl preset-all" 443 | ciel shell "systemctl enable nvidia-persistenced" 444 | } 445 | 446 | # Nike. 447 | set -e 448 | _recipe_pre_install 449 | if type _recipe_${VARIANT}_pre_install >/dev/null 2>&1; then 450 | _recipe_${VARIANT}_pre_install 451 | fi 452 | _recipe_install 453 | _recipe_${VARIANT}_config 454 | _recipe_post 455 | set +e 456 | -------------------------------------------------------------------------------- /plugin/ciel-localrepo: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Usage: ciel-localrepo , one directory at a time. 3 | # Not recommended for manual use. 4 | 5 | REPO_ROOT="$1" 6 | GPG_KEYRING="/etc/ciel/ciel.gpg" 7 | APT_KEYRING="/etc/apt/trusted.gpg" 8 | 9 | function generate_key() { 10 | gpg --no-default-keyring --keyring "$GPG_KEYRING" --fingerprint 11 | gpg --no-default-keyring --keyring "$GPG_KEYRING" --batch --gen-key - < '/etc/ciel/ciel.pub' 23 | } 24 | 25 | [[ -d $(dirname "$GPG_KEYRING") ]] || mkdir -p $(dirname "$GPG_KEYRING") 26 | [[ -f "$GPG_KEYRING" ]] || generate_key 27 | if [[ "$CIEL_LR_FIRST" == '1' ]]; then 28 | gpg --no-default-keyring --keyring "$REPO_ROOT/$APT_KEYRING" --import -a < '/etc/ciel/ciel.pub' || true 29 | exit 0 30 | fi 31 | 32 | pushd "$REPO_ROOT/../" > /dev/null 33 | dpkg-scanpackages './debs' > "./debs/Packages" 34 | popd > /dev/null 35 | 36 | gpg --pinentry-mode loopback --passphrase='CIEL' --no-default-keyring --keyring "$GPG_KEYRING" --clearsign -az3 < "$REPO_ROOT/InRelease" 37 | Origin: AOSC 38 | Label: AOSC OS 39 | Suite: local 40 | Date: $(date -u -R) 41 | Valid-Until: $(date -u -R --date='fortnight') 42 | Description: AOSC OS Repository - Local 43 | Architectures: all $(uname -p) 44 | SHA256: 45 | $(sha256sum "${REPO_ROOT}/Packages" | cut -f 1 -d ' ') $(du -b "${REPO_ROOT}/Packages" | cut -f-1) Packages 46 | EOF 47 | -------------------------------------------------------------------------------- /plugin/ciel-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: ciel-release , where is the name of the variant. 3 | # This command will also invoke ciel-generate, so should be one of 4 | # which defined in ciel-generate. 5 | export CIEL_INST="ciel--release--" 6 | 7 | XZ_PARAM="-9 -e --lzma2=preset=9e,nice=273" 8 | DATE="$(TZ=UTC date +'%Y%m%d')" 9 | ciel add ciel--release-- 10 | ARCH="$(ciel shell -i ciel--release-- -n "dpkg-architecture -qDEB_BUILD_ARCH" | dos2unix)" 11 | ciel factory-reset -i ciel--release-- 12 | ciel commit -i ciel--release-- 13 | ciel del ciel--release-- 14 | VARIANT="$1" 15 | OS_PATH="$CIEL_DIR/.ciel/container/dist" 16 | WORKDIR="$PWD" 17 | 18 | if [[ ! "$CIEL_DIR" ]]; then 19 | echo "\$CIEL_DIR cannot be null." 20 | exit 127 21 | fi 22 | 23 | if [[ ! "$TARBALL" ]]; then 24 | TARBALL=aosc-os_${VARIANT}_"${DATE}"_"${ARCH}".tar.xz 25 | fi 26 | 27 | pushd "$OS_PATH" || exit $? 28 | 29 | _ciel_tarball() { 30 | # Make a tarball 31 | tar cf - * | $COMPRESSOR > "$WORKDIR/$TARBALL" || exit $? 32 | # Generate SHA256 checksum. 33 | sha256sum "$WORKDIR/$TARBALL" > "$WORKDIR/$TARBALL".sha256sum || exit $? 34 | } 35 | 36 | if [[ ! "$COMPRESSOR" ]]; then 37 | if [ -z "$2" ]; then 38 | echo "Usage: ciel release " 39 | echo "" 40 | echo " not defined or not a natural integer!" 41 | echo "Defaulting to 0, using as many threads as possible!" 42 | echo "This could result in out-of-memory conditions" 43 | echo "" 44 | echo "Please declare a natural integer for XZ thread count." 45 | echo "The higher the thread count, the higher the memory requirement." 46 | export XZ_THREADS=0 47 | else 48 | export XZ_THREADS=$2 49 | fi 50 | COMPRESSOR="xz $XZ_PARAM -T $XZ_THREADS" 51 | fi 52 | 53 | _ciel_tarball 54 | 55 | popd 56 | -------------------------------------------------------------------------------- /plugin/ciel-switchmirror: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage: ciel-switchmirror 3 | 4 | export CIEL_INST="ciel--switchmirror--" 5 | export CIEL_BATCH_MODE="true" 6 | 7 | ciel add "$CIEL_INST" 8 | ciel shell "apt-gen-list m $1" 9 | ciel factory-reset 10 | ciel commit 11 | ciel del "$CIEL_INST" 12 | -------------------------------------------------------------------------------- /proc-api/proc.go: -------------------------------------------------------------------------------- 1 | package proc 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "log" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func procPath(pid uint32) string { 13 | return "/proc/" + strconv.FormatUint(uint64(pid), 10) 14 | } 15 | 16 | func GetParentProcessID(pid uint32) (uint32, error) { 17 | b, err := ioutil.ReadFile(procPath(pid) + "/stat") 18 | if err != nil { 19 | return 0, err 20 | } 21 | dataSet := bytes.Split(b, []byte{'\x20'}) 22 | ppid, err := strconv.ParseUint(string(dataSet[3]), 10, 32) 23 | return uint32(ppid), err 24 | } 25 | 26 | func GetCommandLineByPID(pid uint32) ([]string, error) { 27 | b, err := ioutil.ReadFile(procPath(pid) + "/cmdline") 28 | if err != nil { 29 | return nil, err 30 | } 31 | b = bytes.TrimSuffix(b, []byte{'\x00'}) 32 | return strings.Split(string(b), string('\x00')), nil 33 | } 34 | 35 | func Mounted(target string) bool { 36 | a, err := ioutil.ReadFile("/proc/self/mountinfo") 37 | s := string(a) 38 | list := strings.Split(s, "\n") 39 | absPath, _ := filepath.Abs(target) 40 | match, _ := filepath.EvalSymlinks(absPath) 41 | for _, item := range list { 42 | if item == "" { 43 | continue 44 | } 45 | fields := strings.Split(item, " ") 46 | if fields[4] == match { 47 | return true 48 | } 49 | } 50 | if err != nil { 51 | log.Panicln(err) 52 | } 53 | return false 54 | } 55 | -------------------------------------------------------------------------------- /systemd-api/dbus.go: -------------------------------------------------------------------------------- 1 | package systemd 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/godbus/dbus/v5" 9 | ) 10 | 11 | var Conn *dbus.Conn 12 | 13 | func init() { 14 | conn, err := dbus.SystemBusPrivate() 15 | if err != nil { 16 | log.Panicln(err) 17 | } 18 | authMethods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} 19 | err = conn.Auth(authMethods) 20 | if err != nil { 21 | log.Panicln(err) 22 | } 23 | err = conn.Hello() 24 | if err != nil { 25 | log.Panicln(err) 26 | } 27 | Conn = conn 28 | } 29 | -------------------------------------------------------------------------------- /systemd-api/machined/dbus.go: -------------------------------------------------------------------------------- 1 | package machined 2 | 3 | import ( 4 | "github.com/AOSC-Dev/ciel/systemd-api" 5 | "github.com/godbus/dbus/v5" 6 | ) 7 | 8 | const Dest = "org.freedesktop.machine1" 9 | 10 | func Object(path dbus.ObjectPath) dbus.BusObject { 11 | return systemd.Conn.Object(Dest, path) 12 | } 13 | -------------------------------------------------------------------------------- /systemd-api/machined/machine.go: -------------------------------------------------------------------------------- 1 | package machined 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | ) 6 | 7 | type Machine struct { 8 | Obj dbus.BusObject 9 | } 10 | 11 | const MachineInterface = "org.freedesktop.machine1.Machine" 12 | 13 | func (m Machine) Leader() (uint32, error) { 14 | v, err := m.GetProperty(".Leader") 15 | if err != nil { 16 | return 0, err 17 | } 18 | return v.(uint32), err 19 | } 20 | 21 | func (m Machine) GetProperty(name string) (value interface{}, err error) { 22 | v, err := m.Obj.GetProperty(MachineInterface + name) 23 | if err != nil { 24 | return nil, err 25 | } 26 | return v.Value(), err 27 | } 28 | -------------------------------------------------------------------------------- /systemd-api/machined/manager.go: -------------------------------------------------------------------------------- 1 | package machined 2 | 3 | import ( 4 | "github.com/godbus/dbus/v5" 5 | ) 6 | 7 | const ManagerPath = "/org/freedesktop/machine1" 8 | 9 | type Manager struct { 10 | Obj dbus.BusObject 11 | } 12 | 13 | const ManagerInterface = "org.freedesktop.machine1.Manager" 14 | 15 | func (m Manager) GetMachine(name string) (machine *Machine, err error) { 16 | result := m.Obj.Call(ManagerInterface+".GetMachine", 0, name) 17 | if result.Err != nil { 18 | return nil, result.Err 19 | } 20 | return &Machine{Object(result.Body[0].(dbus.ObjectPath))}, nil 21 | } 22 | 23 | func NewManager() *Manager { 24 | return &Manager{Object(ManagerPath)} 25 | } 26 | -------------------------------------------------------------------------------- /systemd-api/nspawn/api.go: -------------------------------------------------------------------------------- 1 | package nspawn 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "log" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "syscall" 12 | "time" 13 | ) 14 | 15 | var BootableFiles = []string{ 16 | "/usr/lib/systemd/systemd", 17 | "/lib/systemd/systemd", 18 | "/sbin/init", 19 | } 20 | 21 | const PowerOffTimeout = 30 * time.Second 22 | 23 | type ErrCancelled struct { 24 | reason string 25 | } 26 | 27 | func (e ErrCancelled) Error() string { 28 | return "cancelled: " + e.reason 29 | } 30 | 31 | func SystemdNspawnRun(ctx context.Context, machineId string, dir string, ctnInfo *ContainerInfo, runInfo *RunInfo) (int, error) { 32 | a := nspawnArgs(machineId, dir, ctnInfo, runInfo) 33 | cmd := exec.CommandContext(ctx, "systemd-nspawn", a...) 34 | setCmdStdDev(cmd, runInfo.StdDev) 35 | 36 | err := cmd.Run() 37 | waitUntilShutdown(ctx, machineId) 38 | return unpackExecErr(err) 39 | } 40 | func SystemdNspawnBoot(ctx context.Context, machineId string, dir string, ctnInfo *ContainerInfo) error { 41 | a := nspawnArgs(machineId, dir, ctnInfo, nil) 42 | cmd := exec.CommandContext(ctx, "systemd-nspawn", a...) 43 | 44 | var debug = true 45 | _, e := os.Lstat("/tmp/ciel.debug") 46 | if os.IsNotExist(e) { 47 | debug = false 48 | } 49 | 50 | var err error 51 | waitCtx, cancelFunc := context.WithCancel(context.Background()) 52 | go func() { 53 | errBuf := &bytes.Buffer{} 54 | cmd.Stderr = errBuf 55 | if debug { 56 | cmd.Stdout, _ = os.Create("/tmp/ciel." + machineId) 57 | } 58 | cmd.Run() 59 | output := errBuf.String() 60 | err = ErrCancelled{reason: string(output)} 61 | cancelFunc() 62 | }() 63 | cancelled := waitUntilRunningOrDegraded(waitCtx, machineId) 64 | if cancelled { 65 | return err 66 | } 67 | return nil 68 | } 69 | 70 | func SystemdRun(ctx context.Context, machineId string, runInfo *RunInfo) (int, error) { 71 | a := runArgs(machineId, runInfo) 72 | cmd := exec.CommandContext(ctx, "systemd-run", a...) 73 | setCmdStdDev(cmd, runInfo.StdDev) 74 | 75 | err := cmd.Run() 76 | defer func() { 77 | // shutting down... 78 | if !MachineRunning(MachineStatus(ctx, machineId)) { 79 | waitUntilShutdown(ctx, machineId) 80 | } 81 | }() 82 | return unpackExecErr(err) 83 | } 84 | 85 | func MachinectlShell(ctx context.Context, machineId string, runInfo *RunInfo) (int, error) { 86 | a := msArgs(machineId, runInfo) 87 | cmd := exec.CommandContext(ctx, "machinectl", a...) 88 | setCmdStdDev(cmd, runInfo.StdDev) 89 | 90 | err := cmd.Run() 91 | defer func() { 92 | // shutting down... 93 | if !MachineRunning(MachineStatus(ctx, machineId)) { 94 | waitUntilShutdown(ctx, machineId) 95 | } 96 | }() 97 | // FIXME: machinectl shell cannot return the exit status of executable 98 | return unpackExecErr(err) 99 | } 100 | 101 | func MachinectlTerminate(ctx context.Context, machineId string) error { 102 | err := machinectlTerminate(ctx, machineId) 103 | waitUntilShutdown(ctx, machineId) 104 | return err 105 | } 106 | 107 | func MachinectlPowerOff(ctx context.Context, machineId string) error { 108 | a := []string{ 109 | "poweroff", 110 | "--quiet", 111 | machineId, 112 | } 113 | cmd := exec.CommandContext(ctx, "machinectl", a...) 114 | output, err := cmd.CombinedOutput() 115 | if _, ok := err.(*exec.ExitError); ok { 116 | return errors.New(strings.TrimSpace(string(output))) 117 | } else if err != nil { 118 | return err 119 | } 120 | waitCtx, _ := context.WithTimeout(ctx, PowerOffTimeout) 121 | if waitUntilShutdown(waitCtx, machineId) { // cancelled 122 | machinectlTerminate(context.Background(), machineId) 123 | } 124 | return nil 125 | } 126 | 127 | func machinectlTerminate(ctx context.Context, machineId string) error { 128 | a := []string{ 129 | "terminate", 130 | "--quiet", 131 | machineId, 132 | } 133 | cmd := exec.CommandContext(ctx, "machinectl", a...) 134 | output, err := cmd.CombinedOutput() 135 | if _, ok := err.(*exec.ExitError); ok { 136 | return errors.New(strings.TrimSpace(string(output))) 137 | } else { 138 | return err 139 | } 140 | } 141 | 142 | func nspawnArgs(machineId string, dir string, ctnInfo *ContainerInfo, runInfo *RunInfo) []string { 143 | if machineId == "" { 144 | log.Panicln("no machineId specified") 145 | } 146 | 147 | a := []string{ 148 | "--quiet", 149 | "--system-call-filter=swapcontext", 150 | "--capability=CAP_IPC_LOCK", 151 | "-D", dir, 152 | "-M", machineId, 153 | } 154 | 155 | for _, v := range ctnInfo.Properties { 156 | a = append(a, "--property="+v) 157 | } 158 | 159 | if ctnInfo.Init { 160 | a = append(a, "--boot") 161 | } 162 | if ctnInfo.Network != nil { 163 | netInfo := ctnInfo.Network 164 | if netInfo.Zone != "" { 165 | a = append(a, "--network-zone="+netInfo.Zone) 166 | } 167 | } 168 | 169 | a = append(a, "--") 170 | 171 | if ctnInfo.Init { 172 | a = append(a, ctnInfo.InitArgs...) 173 | } else { 174 | a = append(a, runInfo.App) 175 | a = append(a, runInfo.Args...) 176 | } 177 | 178 | return a 179 | } 180 | 181 | func runArgs(machineId string, runInfo *RunInfo) []string { 182 | a := []string{ 183 | "--quiet", 184 | "--wait", 185 | "--pty", 186 | "--uid=root", // for login sessions 187 | "--send-sighup", 188 | "-M", machineId, 189 | } 190 | a = append(a, "--") 191 | a = append(a, runInfo.App) 192 | a = append(a, runInfo.Args...) 193 | return a 194 | } 195 | 196 | func msArgs(machineId string, runInfo *RunInfo) []string { 197 | a := []string{ 198 | "shell", 199 | "--quiet", 200 | machineId, 201 | } 202 | a = append(a, runInfo.App) 203 | a = append(a, runInfo.Args...) 204 | return a 205 | } 206 | 207 | func setCmdStdDev(cmd *exec.Cmd, stdDev *StdDevInfo) { 208 | if stdDev == nil { 209 | cmd.Stdin = os.Stdin 210 | cmd.Stdout = os.Stdout 211 | cmd.Stderr = os.Stderr 212 | } else { 213 | cmd.Stdin = stdDev.Stdin 214 | cmd.Stdout = stdDev.Stdout 215 | cmd.Stderr = stdDev.Stderr 216 | } 217 | } 218 | 219 | func unpackExecErr(err error) (int, error) { 220 | if exitErr, ok := err.(*exec.ExitError); ok { 221 | return exitErr.Sys().(syscall.WaitStatus).ExitStatus(), nil 222 | } 223 | if err != nil { 224 | return -1, err 225 | } 226 | return 0, nil 227 | } 228 | -------------------------------------------------------------------------------- /systemd-api/nspawn/backport.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2009 The Go Authors. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | package nspawn 32 | 33 | import "strings" 34 | 35 | // copied from os/exec/exec.go, for versions before 1.9 36 | func dedupEnv(env []string) []string { 37 | out := make([]string, 0, len(env)) 38 | saw := map[string]int{} // key => index into out 39 | for _, kv := range env { 40 | eq := strings.Index(kv, "=") 41 | if eq < 0 { 42 | out = append(out, kv) 43 | continue 44 | } 45 | k := kv[:eq] 46 | if dupIdx, isDup := saw[k]; isDup { 47 | out[dupIdx] = kv 48 | continue 49 | } 50 | saw[k] = len(out) 51 | out = append(out, kv) 52 | } 53 | return out 54 | } 55 | -------------------------------------------------------------------------------- /systemd-api/nspawn/helper.go: -------------------------------------------------------------------------------- 1 | package nspawn 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "path" 8 | "strings" 9 | ) 10 | 11 | func IsBootable(p string) bool { 12 | for _, file := range BootableFiles { 13 | _, err := os.Stat(path.Join(p, file)) 14 | if err == nil { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | func MachineStatus(ctx context.Context, machineId string) string { 22 | a := []string{ 23 | "is-system-running", 24 | "-M", machineId, 25 | } 26 | cmd := exec.CommandContext(ctx, "systemctl", a...) 27 | cmd.Env = dedupEnv(append(os.Environ(), "LC_ALL=C")) 28 | output, _ := cmd.CombinedOutput() 29 | return strings.TrimSpace(string(output)) 30 | } 31 | 32 | func MachineRunning(status string) bool { 33 | switch status { 34 | case "running": 35 | return true 36 | case "degraded": 37 | return true 38 | default: 39 | return false 40 | } 41 | } 42 | 43 | func MachineDead(status string) bool { 44 | switch status { 45 | case "Failed to connect to bus: Host is down": 46 | return true 47 | default: 48 | return false 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /systemd-api/nspawn/structs.go: -------------------------------------------------------------------------------- 1 | package nspawn 2 | 3 | import "io" 4 | 5 | type RunInfo struct { 6 | App string 7 | Args []string 8 | StdDev *StdDevInfo 9 | 10 | UseSystemdRun bool 11 | } 12 | 13 | type ContainerInfo struct { 14 | Init bool 15 | InitArgs []string 16 | Properties []string 17 | Network *NetworkInfo 18 | } 19 | 20 | type StdDevInfo struct { 21 | Stdin io.Reader 22 | Stdout io.Writer 23 | Stderr io.Writer 24 | } 25 | 26 | // TODO: Network Configuration 27 | // NOTE: NetworkInfo is not used for now. 28 | type NetworkInfo struct { 29 | //Private bool 30 | //IfaceMoveIn []string 31 | //MacVlan []string 32 | //IpVlan []string 33 | Zone string 34 | } 35 | -------------------------------------------------------------------------------- /systemd-api/nspawn/utils.go: -------------------------------------------------------------------------------- 1 | package nspawn 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | func waitUntilRunningOrDegraded(ctx context.Context, machineId string) (cancelled bool) { 9 | for { 10 | switch { 11 | case MachineRunning(MachineStatus(context.TODO(), machineId)): 12 | return false 13 | default: 14 | if ctx != nil { 15 | select { 16 | case <-ctx.Done(): 17 | return true 18 | default: 19 | } 20 | } 21 | } 22 | time.Sleep(50 * time.Millisecond) 23 | } 24 | } 25 | 26 | func waitUntilShutdown(ctx context.Context, machineId string) (cancelled bool) { 27 | for { 28 | switch { 29 | case MachineDead(MachineStatus(ctx, machineId)): 30 | return false 31 | default: 32 | if ctx != nil { 33 | select { 34 | case <-ctx.Done(): 35 | return true 36 | default: 37 | } 38 | } 39 | } 40 | time.Sleep(50 * time.Millisecond) 41 | } 42 | } 43 | --------------------------------------------------------------------------------