debian: Don't always try to run dpkg-query command.
[febootstrap.git] / src / febootstrap_debian.ml
1 (* febootstrap 3
2  * Copyright (C) 2009-2011 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 (* Debian 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 get_installed_pkgs =
32   let pkgs = ref None in
33   let rec f () =
34     match !pkgs with
35     | None ->
36       pkgs :=
37         Some (run_command_get_lines
38                 "dpkg-query --show --showformat='${Package}\\n'");
39       f ()
40     | Some pkgs -> pkgs
41   in
42   f
43
44 let debian_detect () =
45   file_exists "/etc/debian_version" &&
46     Config.aptitude <> "no" && Config.apt_cache <> "no" && Config.dpkg <> "no"
47
48 let rec debian_resolve_dependencies_and_download names =
49   let cmd =
50     sprintf "%s depends --recurse -i %s | grep -v '^[<[:space:]]'"
51       Config.apt_cache
52       (String.concat " " (List.map Filename.quote names)) in
53   let pkgs = run_command_get_lines cmd in
54   let pkgs =
55     if Config.apt_cache_depends_recurse_broken then
56       workaround_broken_apt_cache_depends_recurse (sort_uniq pkgs)
57     else
58       pkgs in
59
60   (* Exclude packages matching [--exclude] regexps on the command line. *)
61   let pkgs =
62     List.filter (
63       fun name ->
64         not (List.exists (fun re -> Str.string_match re name 0) excludes)
65     ) pkgs in
66
67   let present_pkgs, download_pkgs = List.partition (
68     fun pkg -> List.exists ((=) pkg) (get_installed_pkgs ())
69   ) pkgs in
70
71   debug "wanted packages (present / download): %s / %s\n"
72     (String.concat " " present_pkgs)
73     (String.concat " " download_pkgs);
74
75   (* Download the packages. *)
76   if (List.length download_pkgs > 0)
77   then (
78     let cmd =
79       sprintf "umask 0000; cd %s && %s download %s"
80         (Filename.quote tmpdir)
81         Config.aptitude
82         (String.concat " " (List.map Filename.quote download_pkgs)) in
83     run_command cmd
84   );
85
86   (* Find out what aptitude downloaded. *)
87   let files = Sys.readdir tmpdir in
88
89   let download_pkgs = List.map (
90     fun pkg ->
91       (* Look for 'pkg_*.deb' in the list of files. *)
92       let pre = pkg ^ "_" in
93       let r = ref "" in
94       try
95         for i = 0 to Array.length files - 1 do
96           if string_prefix pre files.(i) then (
97             r := files.(i);
98             files.(i) <- "";
99             raise Exit
100           )
101         done;
102         eprintf "febootstrap: aptitude: error: no file was downloaded corresponding to package %s\n" pkg;
103         exit 1
104       with
105           Exit -> !r
106   ) download_pkgs in
107
108   List.sort compare (List.append present_pkgs download_pkgs)
109
110 (* On Ubuntu 10.04 LTS, apt-cache depends --recurse is broken.  It
111  * doesn't return the full list of dependencies.  Therefore recurse
112  * into these dependencies one by one until we reach a fixpoint.
113  *)
114 and workaround_broken_apt_cache_depends_recurse names =
115   debug "workaround for broken 'apt-cache depends --recurse' command:\n  %s"
116     (String.concat " " names);
117
118   let names' =
119     List.map (
120       fun name ->
121         let cmd =
122           sprintf "%s depends --recurse -i %s | grep -v '^[<[:space:]]'"
123             Config.apt_cache (Filename.quote name) in
124         run_command_get_lines cmd
125     ) names in
126   let names' = List.flatten names' in
127   let names' = sort_uniq names' in
128   if names <> names' then
129     workaround_broken_apt_cache_depends_recurse names'
130   else
131     names
132
133 let debian_list_files_downloaded pkg =
134   debug "unpacking %s ..." pkg;
135
136   (* We actually need to extract the file in order to get the
137    * information about modes etc.
138    *)
139   let pkgdir = tmpdir // pkg ^ ".d" in
140   mkdir pkgdir 0o755;
141   let cmd =
142     sprintf "umask 0000; dpkg-deb --fsys-tarfile %s | (cd %s && tar xf -)"
143       (tmpdir // pkg) pkgdir in
144   run_command cmd;
145
146   let cmd = sprintf "cd %s && find ." pkgdir in
147   let lines = run_command_get_lines cmd in
148
149   let files = List.map (
150     fun path ->
151       assert (path.[0] = '.');
152       (* No leading '.' *)
153       let path =
154         if path = "." then "/"
155         else String.sub path 1 (String.length path - 1) in
156
157       (* Find out what it is and get the canonical filename. *)
158       let statbuf = lstat (pkgdir // path) in
159       let is_dir = statbuf.st_kind = S_DIR in
160
161       (* No per-file metadata like in RPM, but we can synthesize it
162        * from the path.
163        *)
164       let config = statbuf.st_kind = S_REG && string_prefix "/etc/" path in
165
166       let mode = statbuf.st_perm in
167
168       (path, { ft_dir = is_dir; ft_config = config; ft_mode = mode;
169                ft_ghost = false; ft_size = statbuf.st_size })
170   ) lines in
171
172   files
173
174 let debian_list_files_installed pkg =
175   debug "using installed package %s ..." pkg;
176   let cmd = sprintf "dpkg-query --listfiles %s" pkg in
177   let lines = run_command_get_lines cmd in
178   (* filter out lines not directly describing fs objects such as
179      "package diverts others to: /path/to/..." *)
180   let lines = List.filter (
181     fun l -> l.[0] = '/' && l.[1] != '.'
182   ) lines in
183   let files = List.map (
184     fun path ->
185       let statbuf = lstat path in
186       let is_dir = statbuf.st_kind = S_DIR in
187       let config = statbuf.st_kind = S_REG && string_prefix "/etc/" path in
188       let mode = statbuf.st_perm in
189       (path, { ft_dir = is_dir; ft_config = config; ft_mode = mode;
190                ft_ghost = false; ft_size = statbuf.st_size })
191   ) lines in
192   files
193
194 let debian_list_files ?(use_installed=false) pkg =
195   if use_installed && List.exists ((=) pkg) (get_installed_pkgs ()) then
196     debian_list_files_installed pkg
197   else
198     debian_list_files_downloaded pkg
199
200 (* Easy because we already unpacked the archive above. *)
201 let debian_get_file_from_package ?(use_installed=false) pkg file =
202   if use_installed && List.exists (fun p -> p = pkg) (get_installed_pkgs ())
203   then
204     file
205   else
206     tmpdir // pkg ^ ".d" // file
207
208 let () =
209   let ph = {
210     ph_detect = debian_detect;
211     ph_resolve_dependencies_and_download =
212       debian_resolve_dependencies_and_download;
213     ph_list_files = debian_list_files;
214     ph_get_file_from_package = debian_get_file_from_package;
215   } in
216   register_package_handler "debian" ph