Rewrite febootstrap as a general supermin appliance building tool.
[febootstrap.git] / febootstrap_yum_rpm.ml
1 (* febootstrap 3
2  * Copyright (C) 2009-2010 Red Hat Inc.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17  *)
18
19 (* Yum and RPM support. *)
20
21 open Unix
22 open Printf
23
24 open Febootstrap_package_handlers
25 open Febootstrap_utils
26 open Febootstrap_cmdline
27
28 (* Create a temporary directory for use by all the functions in this file. *)
29 let tmpdir = tmpdir ()
30
31 let yum_rpm_detect () =
32   (file_exists "/etc/redhat-release" || file_exists "/etc/fedora-release") &&
33     Config.yum <> "no" && Config.rpm <> "no"
34
35 let yum_rpm_resolve_dependencies_and_download names =
36   (* Liberate this data from python. *)
37   let py = "
38 import yum
39 import yum.misc
40 import sys
41
42 yb = yum.YumBase ()
43 #yum.logginglevels.setDebugLevel(0) -- doesn't work?
44
45 # Look up the base packages from the command line.
46 deps = dict ()
47 pkgs = yb.pkgSack.returnPackages (patterns=sys.argv[2:])
48 for pkg in pkgs:
49     deps[pkg] = False
50
51 # Recursively find all the dependencies.
52 stable = False
53 while not stable:
54     stable = True
55     for pkg in deps.keys():
56         if deps[pkg] == False:
57             deps[pkg] = []
58             stable = False
59             for r in pkg.requires:
60                 ps = yb.whatProvides (r[0], r[1], r[2])
61                 best = yb._bestPackageFromList (ps.returnPackages ())
62                 if best.name != pkg.name:
63                     deps[pkg].append (best)
64                     if not deps.has_key (best):
65                         deps[best] = False
66             deps[pkg] = yum.misc.unique (deps[pkg])
67
68 # Write it to a file because yum spews garbage on stdout.
69 f = open (sys.argv[1], \"w\")
70 for pkg in deps.keys ():
71     f.write (\"%s %s %s %s %s\\n\" %
72              (pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch))
73 f.close ()
74 " in
75   let tmpfile = tmpdir // "names.tmp" in
76   run_python py (tmpfile :: names);
77   let chan = open_in tmpfile in
78   let lines = input_all_lines chan in
79   close_in chan;
80
81   (* Get fields. *)
82   let pkgs =
83     List.map (
84       fun line ->
85         match string_split " " line with
86         | [name; epoch; version; release; arch] ->
87             name, int_of_string epoch, version, release, arch
88         | _ ->
89             eprintf "febootstrap: bad output from python script: '%s'" line;
90             exit 1
91     ) lines in
92
93   (* Something of a hack for x86_64: exclude all i[3456]86 packages. *)
94   let pkgs =
95     if Config.host_cpu = "x86_64" then (
96       List.filter (
97         function (_, _, _, _, ("i386"|"i486"|"i586"|"i686")) -> false
98         | _ -> true
99       ) pkgs
100     )
101     else pkgs in
102
103   (* Drop the kernel package to save time. *)
104   let pkgs =
105     List.filter (function ("kernel",_,_,_,_) -> false | _ -> true) pkgs in
106
107   (* Exclude packages matching [--exclude] regexps on the command line. *)
108   let pkgs =
109     List.filter (
110       fun (name, _, _, _, _) ->
111         not (List.exists (fun re -> Str.string_match re name 0) excludes)
112     ) pkgs in
113
114   (* Sort the list of packages, and remove duplicates (by name).
115    * XXX This is not quite right: we really want to keep the latest
116    * package if duplicates are found, but that would require a full
117    * version compare function.
118    *)
119   let pkgs = List.sort (fun a b -> compare b a) pkgs in
120   let pkgs =
121     let cmp (name1, _, _, _, _) (name2, _, _, _, _) = compare name1 name2 in
122     uniq ~cmp pkgs in
123   let pkgs = List.sort compare pkgs in
124
125   (* Construct package names. *)
126   let pkgnames = List.map (
127     function
128     | name, 0, version, release, arch ->
129         sprintf "%s-%s-%s.%s" name version release arch
130     | name, epoch, version, release, arch ->
131         sprintf "%d:%s-%s-%s.%s" epoch name version release arch
132   ) pkgs in
133
134   if pkgnames = [] then (
135     eprintf "febootstrap: yum-rpm: error: no packages to download\n";
136     exit 1
137   );
138
139   let cmd = sprintf "yumdownloader --destdir %s %s"
140     (Filename.quote tmpdir)
141     (String.concat " " (List.map Filename.quote pkgnames)) in
142   run_command cmd;
143
144   (* Return list of package filenames. *)
145   List.map (
146     (* yumdownloader doesn't include epoch in the filename *)
147     fun (name, _, version, release, arch) ->
148       sprintf "%s/%s-%s-%s.%s.rpm" tmpdir name version release arch
149   ) pkgs
150
151 let rec yum_rpm_list_files pkg =
152   (* Run rpm -qlp with some extra magic. *)
153   let cmd =
154     sprintf "rpm -q --qf '[%%{FILENAMES} %%{FILEFLAGS:fflags} %%{FILEMODES}\\n]' -p %s"
155       pkg in
156   let lines = run_command_get_lines cmd in
157
158   let files =
159     filter_map (
160       fun line ->
161         match string_split " " line with
162         | [filename; flags; mode] ->
163             let test_flag = String.contains flags in
164             let mode = int_of_string mode in
165             if test_flag 'd' then None  (* ignore documentation *)
166             else
167               Some (filename, {
168                       ft_dir = mode land 0o40000 <> 0;
169                       ft_ghost = test_flag 'g'; ft_config = test_flag 'c';
170                       ft_mode = mode;
171                     })
172         | _ ->
173             eprintf "febootstrap: bad output from rpm command: '%s'" line;
174             exit 1
175     ) lines in
176
177   (* I've never understood why the base packages like 'filesystem' don't
178    * contain any /dev nodes at all.  This leaves every program that
179    * bootstraps RPMs to create a varying set of device nodes themselves.
180    * This collection was copied from mock/backend.py.
181    *)
182   let files =
183     let b = Filename.basename pkg in
184     if string_prefix "filesystem-" b then (
185       let dirs = [ "/proc"; "/sys"; "/dev"; "/dev/pts"; "/dev/shm";
186                    "/dev/mapper" ] in
187       let dirs =
188         List.map (fun name ->
189                     name, { ft_dir = true; ft_ghost = false;
190                             ft_config = false; ft_mode = 0o40755 }) dirs in
191       let devs = [ "/dev/null"; "/dev/full"; "/dev/zero"; "/dev/random";
192                    "/dev/urandom"; "/dev/tty"; "/dev/console";
193                    "/dev/ptmx"; "/dev/stdin"; "/dev/stdout"; "/dev/stderr" ] in
194       (* No need to set the mode because these will go into hostfiles. *)
195       let devs =
196         List.map (fun name ->
197                     name, { ft_dir = false; ft_ghost = false;
198                             ft_config = false; ft_mode = 0o644 }) devs in
199       dirs @ devs @ files
200     ) else files in
201
202   files
203
204 let yum_rpm_get_file_from_package pkg file =
205   debug "extracting %s from %s ..." file (Filename.basename pkg);
206
207   let outfile = tmpdir // file in
208   let cmd =
209     sprintf "rpm2cpio %s | (cd %s && cpio --quiet -id .%s)"
210       (Filename.quote pkg) (Filename.quote tmpdir) (Filename.quote file) in
211   run_command cmd;
212   outfile
213
214 let () =
215   let ph = {
216     ph_detect = yum_rpm_detect;
217     ph_resolve_dependencies_and_download =
218       yum_rpm_resolve_dependencies_and_download;
219     ph_list_files = yum_rpm_list_files;
220     ph_get_file_from_package = yum_rpm_get_file_from_package;
221   } in
222   register_package_handler "yum-rpm" ph