Skip to content

Instantly share code, notes, and snippets.

@maxlapshin

maxlapshin/fpm.erl

Created Aug 16, 2013
Embed
What would you like to do?
Prepare debian package with simple erlang script
#!/usr/bin/env escript
-mode(compile).
-record(fpm, {
target,
category,
url,
description = "no description",
maintainer,
post_install,
pre_uninstall,
post_uninstall,
config_files = [],
name,
replaces = [],
conflicts = [],
version,
arch,
paths = []
}).
main(Args) ->
State = parse_args(Args, #fpm{}),
make_package(State),
ok.
fpm_error(Format) ->
fpm_error(Format, []).
fpm_error(Format, Args) ->
io:format(Format, Args),
halt(1).
parse_args(["-s", "dir" | Args], State) ->
parse_args(Args, State);
parse_args(["-s", Source | _Args], _State) ->
fpm_error("-s '~s' is not supported\n", [Source]);
parse_args(["--url", URL|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{url = URL});
parse_args(["--description", Desc|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{description = Desc});
parse_args(["--category", Desc|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{category = Desc});
parse_args(["-m", Desc|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{maintainer = Desc});
parse_args(["--post-install", V|Args], #fpm{} = State) ->
case file:read_file(V) of
{ok, Bin} -> parse_args(Args, State#fpm{post_install = Bin});
{error, E} -> fpm_error("Failed to read post-install ~s", [E])
end;
parse_args(["--post-uninstall", V|Args], #fpm{} = State) ->
case file:read_file(V) of
{ok, Bin} -> parse_args(Args, State#fpm{post_uninstall = Bin});
{error, E} -> fpm_error("Failed ot read post-uninstall ~s", E)
end;
parse_args(["--pre-uninstall", V|Args], #fpm{} = State) ->
case file:read_file(V) of
{ok, Bin} -> parse_args(Args, State#fpm{pre_uninstall = Bin});
{error, E} -> fpm_error("Failed to read pre-uninstall ~s", [E])
end;
parse_args(["--config-files", V|Args], #fpm{config_files = Conf} = State) ->
parse_args(Args, State#fpm{config_files = Conf ++ [V]});
parse_args(["-t", "deb"|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{target = deb});
parse_args(["-t", Target|_Args], #fpm{} = _State) ->
fpm_error("-t '~s' is not supported\n",[Target]);
parse_args(["-n", V|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{name = V});
parse_args(["--replaces", V|Args], #fpm{replaces = R} = State) ->
parse_args(Args, State#fpm{replaces = R ++ [V]});
parse_args(["--conflicts", V|Args], #fpm{conflicts = R} = State) ->
parse_args(Args, State#fpm{conflicts = R ++ [V]});
parse_args(["-v", V|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{version = V});
parse_args(["-a", V|Args], #fpm{} = State) ->
parse_args(Args, State#fpm{arch = V});
parse_args(["--"++Option, _V|Args], #fpm{} = State) ->
io:format("unknown option '~s'\n", [Option]),
parse_args(Args, State);
parse_args(["-"++Option, _V|Args], #fpm{} = State) ->
io:format("unknown option '~s'\n", [Option]),
parse_args(Args, State);
parse_args(Paths, #fpm{} = State) ->
State#fpm{paths = Paths}.
validate_package(#fpm{name = undefined}) ->
fpm_error("name is required\n");
validate_package(#fpm{arch = undefined}) ->
fpm_error("arch is required\n");
validate_package(#fpm{version = undefined}) ->
fpm_error("version is required\n");
validate_package(_) ->
ok.
make_package(#fpm{target = deb, name = Name, version = Version, arch = Arch} = State) ->
validate_package(State),
pack_control(State),
pack_data(State),
file:write_file("debian-binary", "2.0\n"),
Path = Name++"_" ++ Version++ "_" ++ Arch ++ ".deb",
file:delete(Path),
os:cmd("ar qc "++Path++" debian-binary control.tar.gz data.tar.gz"),
file:delete("control.tar.gz"),
file:delete("data.tar.gz"),
file:delete("debian-binary"),
ok.
pack_control(#fpm{post_install = Postinst, pre_uninstall = Prerm, post_uninstall = Postrm} = State) ->
Files = [{"control", control_content(State)}] ++
possible_file(conffiles, conf_files(State)) ++
possible_file(postinst, Postinst) ++
possible_file(prerm, Prerm) ++
possible_file(postrm, Postrm),
file:delete("control.tar.gz"),
erl_tar:create("control.tar.gz", Files, [compressed]),
ok.
possible_file(_, undefined) -> [];
possible_file(Name, Content) -> [{atom_to_list(Name),iolist_to_binary(Content)}].
conf_files(#fpm{config_files = Conf}) ->
[[C,"\n"] || C <- Conf].
control_content(#fpm{name = Name, version = Version, maintainer = Maintainer, conflicts = Conflicts,
arch = Arch,
replaces = Replaces, category = Category, url = URL, description = Description}) ->
Content = [
header("Package", Name),
header("Version", Version),
header("Architecture", Arch),
header("Maintainer", Maintainer),
header("Conflicts", join_list(Conflicts)),
header("Replaces", join_list(Replaces)),
header("Standards-Version", "3.9.1"),
header("Section",Category),
header("Priority", "extra"),
header("Homepage", URL),
header("Description", Description)
],
iolist_to_binary(Content).
header(_, undefined) -> "";
header(Key, Value) -> [Key, ": ", Value, "\n"].
join_list([]) -> undefined;
join_list(Items) -> string:join(Items, ", ").
pack_data(#fpm{paths = Paths}) ->
AllPaths = lookup_files(Paths),
file:delete("data.tar.gz"),
% {ok, Tar} = erl_tar:open("data.tar.gz", [write,compressed]),
% lists:foreach(fun(Path) ->
% case filelib:is_dir(Path) of
% true ->
% end, AllPaths),
{Dirs, Files} = lists:partition(fun filelib:is_dir/1, AllPaths),
% io:format("dirs: ~p\n",[Dirs]),
% io:format("files: ~p\n",[Files]),
"" = os:cmd(tar()++" --owner=root --numeric-owner --group 0 --no-recursion -cf data.tar.gz "++string:join(Dirs, " ")),
"" = os:cmd(tar()++" --owner=root --numeric-owner --group 0 -rf data.tar.gz "++string:join(Files, " ")),
% os:cmd(tar()++" --owner=root --group=root --no-recursion -cf data.tar.gz "++string:join(AllPaths, " ")),
ok.
lookup_files(Dirs) ->
lookup_files(Dirs, sets:new()).
lookup_files([], Set) ->
lists:usort(sets:to_list(Set));
lookup_files([Dir|Dirs], Set) ->
Set1 = filelib:fold_files(Dir, ".*", true, fun(Path, Acc) ->
add_recursive_path(Path, Acc)
end, Set),
lookup_files(Dirs, Set1).
add_recursive_path("/", Set) ->
Set;
add_recursive_path(".", Set) ->
Set;
add_recursive_path(Path, Set) ->
add_recursive_path(filename:dirname(Path), sets:add_element(Path, Set)).
tar() ->
case os:type() of
{unix,darwin} -> "gnutar";
{unix,linux} -> "tar"
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment