summaryrefslogtreecommitdiff
path: root/image/creator.cc
blob: 0d97ff16a3a76d2f91d16d23e917e5333322811e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
/*
 * creator.cc - Implementation of the HorizonScript image creator
 * image, the image processing utilities for
 * Project Horizon
 *
 * Copyright (c) 2020 Adélie Linux and contributors.  All rights reserved.
 * This code is licensed under the AGPL 3.0 license, as noted in the
 * LICENSE-code file in the root directory of this repository.
 *
 * SPDX-License-Identifier: AGPL-3.0-only
 */

#include <cstdlib>              /* EXIT_* */
#include <functional>
#include <map>
#include <string>
#include <vector>

#include <boost/program_options.hpp>
#include <sys/mount.h>

#include "backends/basic.hh"
#include "hscript/meta.hh"
#include "hscript/script.hh"
#include "hscript/util.hh"
#include "util/filesystem.hh"
#include "util/output.hh"

bool pretty = true;     /*! Controls ASCII colour output */


const std::string arch_xlate(const std::string &arch) {
    if(arch == "pmmx") return "i386";
    if(arch == "armv7") return "arm";
    return arch;
}


#define DESCR_TEXT "Usage: hscript-image [OPTION]... [INSTALLFILE]\n"\
                   "Write an operating system image configured per INSTALLFILE"
/*! Text used at the top of usage output */


/*! Entry-point for the image creation utility. */
int main(int argc, char *argv[]) {
    using namespace boost::program_options;
    using namespace Horizon::Image;

    bool needs_help{}, disable_pretty{}, version_only{};
    int exit_code = EXIT_SUCCESS;
    std::string if_path{"/etc/horizon/installfile"}, ir_dir{"/tmp/horizon-image"},
                output_path{"image.tar"}, type_code{"tar"};
    BasicBackend *backend = nullptr;
    std::map<std::string, std::string> backend_opts;
    Horizon::ScriptOptions opts;
    Horizon::Script *my_script;

    options_description ui{DESCR_TEXT};
    options_description general{"General options"};
    general.add_options()
            ("help,h", bool_switch(&needs_help), "Display this message.")
            ("no-colour,n", bool_switch(&disable_pretty), "Do not 'prettify' output.")
            ("version,v", bool_switch(&version_only), "Show program version information.")
            ;
    options_description target{"Target control options"};
    target.add_options()
            ("output,o", value<std::string>()->default_value("image.tar"), "Desired filename for the output file.")
            ("ir-dir,i", value<std::string>()->default_value("/tmp/horizon-image"), "Where to store intermediate files.")
            ;
    options_description backconfig{"Backend configuration options"};
    backconfig.add_options()
            ("type,t", value<std::string>()->default_value("tar"), "Type of output file to generate.  Use 'list' for a list of supported types.")
            ("backconfig,b", value<std::vector<std::string>>(), "Set a backend configuration option.  You may specify this multiple times for multiple options.")
            ;
    ui.add(general).add(target).add(backconfig);

    options_description all;
    all.add(ui).add_options()("installfile", value<std::string>()->default_value(if_path), "The HorizonScript to use for configuring the image.");

    positional_options_description positional;
    positional.add("installfile", 1);

    command_line_parser parser{argc, argv};
    parser.options(all);
    parser.positional(positional);

    variables_map vm;
    try {
        auto result = parser.run();
        store(result, vm);
        notify(vm);
    } catch(const std::exception &ex) {
        std::cerr << ex.what() << std::endl;
        exit_code = EXIT_FAILURE;
        needs_help = true;
    }

    /* --help, or usage failure */
    if(needs_help) {
        std::cout << ui << std::endl;
        return exit_code;
    }

    /* -n/--no-colour, or logging to file */
    if(disable_pretty || !isatty(1)) {
        pretty = false;
    } else {
        opts.set(Horizon::Pretty);
    }

    if(!vm["type"].empty()) {
        type_code = vm["type"].as<std::string>();
    }

    if(type_code == "list") {
        std::cout << "Type codes known by this build of Image Creation:"
                  << std::endl << std::endl;
        for(const auto &candidate : BackendManager::available_backends()) {
            std::cout << std::setw(10) << std::left << candidate.type_code
                      << candidate.description << std::endl;
        }
        return EXIT_SUCCESS;
    }

    if(!vm["installfile"].empty()) {
        if_path = vm["installfile"].as<std::string>();
    }

    if(!vm["ir-dir"].empty()) {
        ir_dir = vm["ir-dir"].as<std::string>();
    }

    if(fs::path(ir_dir).is_relative()) {
        ir_dir = fs::absolute(ir_dir).string();
    }

    if(!vm["output"].empty()) {
        output_path = vm["output"].as<std::string>();
    }

    if(fs::path(output_path).is_relative()) {
        output_path = fs::absolute(output_path).string();
    }

    if(!vm["backconfig"].empty()) {
        for(const auto &confpart :
                vm["backconfig"].as<std::vector<std::string>>()) {
            std::string::size_type equals = confpart.find_first_of("=");
            if(equals != std::string::npos) {
                backend_opts[confpart.substr(0, equals)] =
                        confpart.substr(equals + 1);
            } else {
                backend_opts[confpart] = "";
            }
        }
    }

    /* Announce our presence */
    bold_if_pretty(std::cout);
    std::cout << "HorizonScript Image Creation Utility version " << VERSTR
              << std::endl;
    reset_if_pretty(std::cout);
    std::cout << "Copyright (c) 2020 Adélie Linux and contributors."
              << std::endl
              << "This software is licensed to you under the terms of the "
              << std::endl << "AGPL 3.0 license, unless otherwise noted."
              << std::endl << std::endl;

    if(version_only) {
        return EXIT_SUCCESS;
    }

    /* Load the proper backend. */
    for(const auto &candidate : BackendManager::available_backends()) {
        if(candidate.type_code == type_code) {
            backend = candidate.creation_fn(ir_dir, output_path, backend_opts);
            break;
        }
    }
    if(backend == nullptr) {
        output_error("command-line", "unsupported backend or internal error",
                     type_code);
        return EXIT_FAILURE;
    }

    opts.set(Horizon::InstallEnvironment);
    opts.set(Horizon::ImageOnly);

    if(if_path == "-") {
        /* Unix-style stdin specification */
        my_script = Horizon::Script::load(std::cin, opts, "/dev/stdin");
    } else {
        my_script = Horizon::Script::load(if_path, opts);
    }

    /* if an error occurred during parsing, bail out now */
    if(!my_script) {
        exit_code = EXIT_FAILURE;
        goto early_trouble;
    } else {
        int ret;

#define RUN_PHASE_OR_TROUBLE(_PHASE, _FRIENDLY) \
    ret = backend->_PHASE();\
    if(ret != 0) {\
        output_error("internal", "error during output " _FRIENDLY,\
                     std::to_string(ret));\
        exit_code = EXIT_FAILURE;\
        goto trouble;\
    }

        RUN_PHASE_OR_TROUBLE(prepare, "preparation");

        /* Attempt to make images work cross-architecture.
         * This requires binfmt_misc to be configured properly.
         * It also requires qemu-user to be installed to /usr/bin on the host.
         * It may not work all the time, so we ignore errors.
         * But we try anyway, because it can work and make things easier.
         */
        const Horizon::Keys::Key *archkey = my_script->getOneValue("arch");
        std::string qpath;
        if(archkey) {
            const Horizon::Keys::Arch *arch =
                    dynamic_cast<const Horizon::Keys::Arch *>(archkey);
            qpath = "/usr/bin/qemu-" + arch_xlate(arch->value());
            error_code ec;
            if(fs::exists(qpath, ec)) {
                fs::create_directories(ir_dir + "/target/usr/bin", ec);
                if(!ec) fs::copy_file(qpath, ir_dir + "/target/" + qpath, ec);
            }
        }

        my_script->setTargetDirectory(ir_dir + "/target");

        if(!my_script->execute()) {
            exit_code = EXIT_FAILURE;
            goto trouble;
        }

        if(!qpath.empty() && fs::exists(ir_dir + "/target" + qpath)) {
            error_code ec;
            fs::remove(ir_dir + "/target" + qpath, ec);
        }

        RUN_PHASE_OR_TROUBLE(create, "creation");
        RUN_PHASE_OR_TROUBLE(finalise, "finalisation");
    }

trouble:        /* delete the Script and exit */
    /* ensure that our target mounts are unmounted */
    run_command("umount", {"-R", (ir_dir + "/target/sys")});
    ::umount((ir_dir + "/target/proc").c_str());
    run_command("umount", {"-R", (ir_dir + "/target/dev")});

    delete my_script;
early_trouble:  /* no script yet */
    delete backend;

    return exit_code;
}