]> git.ipfire.org Git - thirdparty/libsolv.git/commitdiff
Add lua bindings
authorMichael Schroeder <mls@suse.de>
Mon, 25 Mar 2024 11:38:34 +0000 (12:38 +0100)
committerMichael Schroeder <mls@suse.de>
Mon, 25 Mar 2024 11:39:22 +0000 (12:39 +0100)
CMakeLists.txt
bindings/CMakeLists.txt
bindings/lua/CMakeLists.txt [new file with mode: 0644]
bindings/solv.i
examples/luasolv [new file with mode: 0644]

index f899c49ac7f0419947845c0ea208eb390875a4d6..116767dda1d244d4f4ac0cfa516bab741123a437 100644 (file)
@@ -9,6 +9,7 @@ OPTION (ENABLE_PERL "Build the perl bindings?" OFF)
 OPTION (ENABLE_PYTHON "Build the python bindings?" OFF)
 OPTION (ENABLE_RUBY "Build the ruby bindings?" OFF)
 OPTION (ENABLE_TCL "Build the Tcl bindings?" OFF)
+OPTION (ENABLE_LUA "Build the lua bindings?" OFF)
 
 OPTION (USE_VENDORDIRS "Install the bindings in vendor directories?" OFF)
 
@@ -326,6 +327,7 @@ SET (PACKAGE "libsolv")
 SET (VERSION "${LIBSOLV_MAJOR}.${LIBSOLV_MINOR}.${LIBSOLV_PATCH}")
 
 ADD_DEFINITIONS (-D_FILE_OFFSET_BITS=64)
+#ADD_DEFINITIONS (-D_TIME_BITS=64)
 CONFIGURE_FILE (src/solvversion.h.in src/solvversion.h)
 
 SET (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Package dependency solver library")
index 737cee4279e15788fbdffc4a31ec8b77e2d2c208..4889a4c66537ec275585e9e4f21b9c7b13b329fe 100644 (file)
@@ -20,3 +20,6 @@ ENDIF (ENABLE_RUBY)
 IF (ENABLE_TCL)
     ADD_SUBDIRECTORY (tcl)
 ENDIF (ENABLE_TCL)
+IF (ENABLE_LUA)
+    ADD_SUBDIRECTORY (lua)
+ENDIF (ENABLE_LUA)
diff --git a/bindings/lua/CMakeLists.txt b/bindings/lua/CMakeLists.txt
new file mode 100644 (file)
index 0000000..b42df21
--- /dev/null
@@ -0,0 +1,21 @@
+FIND_PACKAGE (Lua)
+
+ADD_CUSTOM_COMMAND (
+    OUTPUT solv_lua.c
+    COMMAND ${SWIG_EXECUTABLE} ${SWIG_FLAGS} -lua -I${CMAKE_SOURCE_DIR}/src -o solv_lua.c ${CMAKE_SOURCE_DIR}/bindings/solv.i
+    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+    DEPENDS ${CMAKE_SOURCE_DIR}/bindings/solv.i
+)
+
+INCLUDE_DIRECTORIES (${LUA_INCLUDE_DIR})
+IF (NOT LUA_INSTALL_DIR)
+  SET(LUA_INSTALL_DIR ${CMAKE_INSTALL_FULL_LIBDIR}/lua/${LUA_VERSION_MAJOR}.${LUA_VERSION_MINOR})
+ENDIF (NOT LUA_INSTALL_DIR)
+
+MESSAGE (STATUS "Lua installation dir: ${LUA_INSTALL_DIR}")
+
+ADD_LIBRARY (bindings_lua SHARED solv_lua.c)
+SET_TARGET_PROPERTIES (bindings_lua PROPERTIES PREFIX "" OUTPUT_NAME "solv" INSTALL_NAME_DIR "${LUA_INSTALL_DIR}")
+TARGET_LINK_LIBRARIES (bindings_lua libsolvext libsolv ${LUA_LIBRARY} ${SYSTEM_LIBRARIES})
+INSTALL (TARGETS bindings_lua LIBRARY DESTINATION ${LUA_INSTALL_DIR})
+
index 9d888e4df64bb20dc84026387666e599f67b0032..1cfea38e75fbdd15a7d9fd48f4891c60000a2c94 100644 (file)
 %}
 #endif
 
+/**
+ ** lua object stashing
+ **/
+#if defined(SWIGLUA)
+%{
+SWIGINTERN
+void prep_stashed_lua_var(lua_State* L, char *name, void *ptr)
+{
+  lua_getglobal(L, "solv"); 
+  if (lua_getfield(L, -1, "_stash") == LUA_TNIL) {
+    lua_pop(L, 1);
+    lua_newtable(L);
+    lua_pushvalue(L, -1);
+    lua_setfield(L, -3, "_stash");
+  }
+  lua_remove(L, -2);
+  lua_pushfstring(L, "%s:%p", name, ptr);
+}
+
+SWIGINTERN
+void set_stashed_lua_var(lua_State* L, int idx, char *name, void *ptr)
+{
+  lua_pushvalue(L, idx);
+  prep_stashed_lua_var(L, name, ptr);
+  lua_pushvalue(L, -3);
+  lua_settable(L, -3);
+  lua_pop(L, 2);
+}
+
+SWIGINTERN
+void get_stashed_lua_var(lua_State* L, char *name, void *ptr)
+{
+  prep_stashed_lua_var(L, name, ptr);
+  lua_gettable(L, -2);
+  lua_remove(L, -2);
+}
+
+SWIGINTERN
+void clr_stashed_lua_var(lua_State* L, char *name, void *ptr)
+{
+  prep_stashed_lua_var(L, name, ptr);
+  lua_pushnil(L);
+  lua_settable(L, -3);
+  lua_pop(L, 1);
+}
+%}
+#endif
+
 /**
  ** binaryblob handling
  **/
@@ -27,6 +75,24 @@ typedef struct {
 } BinaryBlob;
 %}
 
+#if defined(SWIGLUA)
+%typemap(in,noblock=1) (const unsigned char *str, size_t len) (char *buf = 0, size_t size = 0) {
+  if (!lua_isstring(L, $input)) SWIG_fail_arg($symname, $input, "const char *");
+  buf = (char *)lua_tolstring(L, $input, &size);
+  $1 = (unsigned char *)buf;
+  $2 = size;
+}
+%typemap(out,noblock=1) BinaryBlob {
+  if ($1.data) {
+    lua_pushlstring(L, $1.data, $1.len);
+  } else {
+    lua_pushnil(L);
+  }
+  SWIG_arg++;
+}
+
+#else
+
 %typemap(in,noblock=1,fragment="SWIG_AsCharPtrAndSize") (const unsigned char *str, size_t len) (int res, char *buf = 0, size_t size = 0, int alloc = 0) {
 #if defined(SWIGTCL)
   {
@@ -86,6 +152,8 @@ typedef struct {
 #endif
 }
 
+#endif
+
 /**
  ** Queue handling
  **/
@@ -389,6 +457,101 @@ typedef struct {
 
 #endif  /* SWIGTCL */
 
+#if defined(SWIGLUA)
+%typemap(out) Queue {
+  int i;
+  lua_newtable(L);
+  for (i = 0; i < $1.count; i++) {
+    lua_pushnumber(L, $1.elements[i]);
+    lua_rawseti(L, -2, i + 1);
+  }
+  queue_free(&$1);
+  SWIG_arg = 1;
+}
+
+%define Queue2Array(type, step, con) %{
+  int i;
+  int cnt = $1.count / step;
+  Id *idp = $1.elements;
+
+  lua_newtable(L);
+  for (i = 0; i < cnt; i++, idp += step)
+    {
+      Id id = *idp;
+#define result resultx
+      type result = con;
+      $typemap(out, type)
+      lua_rawseti(L, -2, i+1);
+#undef result
+    }
+  queue_free(&$1);
+  SWIG_arg = 1;
+%}
+%enddef
+
+%define Array2Queue(asval_meth,typestr) %{ {
+  int i;
+  luaL_checktype(L, -1, LUA_TTABLE);
+  for (i = 1; i; i++) {
+    lua_rawgeti(L, -1, i);
+    if (lua_type(L, -1) == LUA_TNIL)
+      i = -1;
+    else
+      {
+        int v;
+        int e = asval_meth(L, -1, &v);
+        if (!SWIG_IsOK(e)) {
+          lua_pop(L, 1);
+          SWIG_Lua_pusherrstring(L,"list in argument $argnum must contain only " typestr);
+          SWIG_fail;
+        }
+        queue_push(&$1, v);
+      }
+    lua_pop(L, 1);
+  }
+}
+%}
+%enddef
+
+%define ObjArray2Queue(type, obj2queue) %{ {
+  int i;
+  luaL_checktype(L, -1, LUA_TTABLE);
+  for (i = 1; i; i++) {
+    lua_rawgeti(L, -1, i);
+    if (lua_type(L, -1) == LUA_TNIL)
+      i = -1;
+    else
+      {
+        type obj;
+        int e = SWIG_ConvertPtr(L, -1, (void **)&obj, $descriptor(type), 0 | 0);
+        if (!SWIG_IsOK(e))
+          {
+            lua_pop(L, 1);
+            SWIG_Lua_pusherrstring(L,"list in argument $argnum must contain only "`type`);
+            SWIG_fail;
+          }
+        obj2queue;
+      }
+    lua_pop(L, 1);
+  }
+}
+%}
+%enddef
+
+%{
+
+SWIGINTERN int
+SWIG_AsVal_int(lua_State* L, int idx, int *val) {
+  int ecode = lua_isnumber(L, idx) ? SWIG_OK : SWIG_TypeError;
+  if (ecode == SWIG_OK)
+    *val = (int)lua_tonumber(L, idx);
+  return ecode;
+}
+
+%}
+
+#endif  /* SWIGLUA */
+
 %typemap(in) Queue Array2Queue(SWIG_AsVal_int, "integers")
 %typemap(in) Queue solvejobs ObjArray2Queue(Job *, queue_push2(&$1, obj->how, obj->what))
 %typemap(in) Queue solvables ObjArray2Queue(XSolvable *, queue_push(&$1, obj->id))
@@ -544,6 +707,15 @@ typedef Tcl_Obj *AppObjectPtr;
 %typemap(out) AppObjectPtr {
   Tcl_SetObjResult(interp, $1 ? $1 : Tcl_NewObj());
 }
+#elif defined(SWIGLUA)
+typedef void *AppObjectPtr;
+%typemap(in) AppObjectPtr {
+  $1 = (void *)L;
+}
+%typemap(out) AppObjectPtr {
+  get_stashed_lua_var(L, "appdata", $1);
+  SWIG_arg++;
+}
 #else
 #warning AppObjectPtr not defined for this language!
 #endif
@@ -566,15 +738,23 @@ SWIGINTERN int
 SWIG_AsValSolvFpPtr(VALUE obj, FILE **val) {
 #elif defined(SWIGTCL)
 SWIG_AsValSolvFpPtr SWIG_TCL_DECL_ARGS_2(void *obj, FILE **val) {
+#elif defined(SWIGLUA)
+SWIG_AsValSolvFpPtr(lua_State *L, int idx, FILE **val) {
 #else
 SWIG_AsValSolvFpPtr(void *obj, FILE **val) {
 #endif
   static swig_type_info* desc = 0;
   void *vptr = 0;
+#ifdef SWIGPYTHON
   int ecode;
+#endif
 
   if (!desc) desc = SWIG_TypeQuery("SolvFp *");
+#if defined(SWIGLUA)
+  if ((SWIG_ConvertPtr(L, idx, &vptr, desc, 0)) == SWIG_OK) {
+#else
   if ((SWIG_ConvertPtr(obj, &vptr, desc, 0)) == SWIG_OK) {
+#endif
     if (val)
       *val = vptr ? ((SolvFp *)vptr)->fp : 0;
     return SWIG_OK;
@@ -605,6 +785,8 @@ SWIGINTERN int
 SWIG_AsValDepId(VALUE obj, int *val) {
 #elif defined(SWIGTCL)
 SWIG_AsValDepId SWIG_TCL_DECL_ARGS_2(void *obj, int *val) {
+#elif defined(SWIGLUA)
+SWIG_AsValDepId(lua_State *L, int idx, int *val) {
 #else
 SWIG_AsValDepId(void *obj, int *val) {
 #endif
@@ -614,12 +796,18 @@ SWIG_AsValDepId(void *obj, int *val) {
   if (!desc) desc = SWIG_TypeQuery("Dep *");
 #ifdef SWIGTCL
   ecode = SWIG_AsVal_int SWIG_TCL_CALL_ARGS_2(obj, val);
+#elif defined(SWIGLUA)
+  ecode = SWIG_AsVal_int(L, idx, val);
 #else
   ecode = SWIG_AsVal_int(obj, val);
 #endif
   if (SWIG_IsOK(ecode))
     return ecode;
+#if defined(SWIGLUA)
+  if ((SWIG_ConvertPtr(L, idx, &vptr, desc, 0)) == SWIG_OK) {
+#else
   if ((SWIG_ConvertPtr(obj, &vptr, desc, 0)) == SWIG_OK) {
+#endif
     if (val)
       *val = vptr ? ((Dep *)vptr)->id : 0;
     return SWIG_OK;
@@ -645,12 +833,17 @@ SWIG_AsValDepId(void *obj, int *val) {
   SWIG_ConvertPtr(ST(0), &argp1,SWIGTYPE_p_Pool, SWIG_POINTER_DISOWN |  0 );
 #elif defined(SWIGTCL)
   SWIG_ConvertPtr(objv[1], &argp1, SWIGTYPE_p_Pool, SWIG_POINTER_DISOWN | 0);
+#elif defined(SWIGLUA)
+  SWIG_ConvertPtr(L, 1, (void **)&arg1, SWIGTYPE_p_Pool, SWIG_POINTER_DISOWN | 0);
 #else
 #warning disown_helper not implemented for this language, this is likely going to leak memory
 #endif
 
 #ifdef SWIGTCL
   Tcl_SetObjResult(interp, SWIG_From_int((int)(0)));
+#elif defined(SWIGLUA)
+  $result = 0;
+  lua_pushnumber(L, $result); SWIG_arg++;
 #else
   $result = SWIG_From_int((int)(0));
 #endif
@@ -689,6 +882,22 @@ SWIG_AsValDepId(void *obj, int *val) {
  **/
 
 %include "typemaps.i"
+#if defined(SWIGLUA)
+%runtime "swigerrors.swg";
+%include "typemaps/swigmacros.swg"
+%include "typemaps/valtypes.swg"
+%include "typemaps/inoutlist.swg"
+
+%rename(__call) *::__next__;
+%rename(__tostring) *::__str__;
+%rename(__index) *::__getitem__;
+%rename(__eq) *::__eq__;
+%rename(__ne) *::__ne__;
+
+%typemap(in) void *ign1 {};
+%typemap(in) void *ign2 {};
+%typemap(in,checkfn="lua_isfunction") int lua_function_idx { $1 = $input; };
+#endif
 
 #if defined(SWIGTCL)
 %rename("==") *::__eq__;
@@ -698,12 +907,45 @@ SWIG_AsValDepId(void *obj, int *val) {
 %typemap(in,numinputs=0,noblock=1) XRule **OUTPUT ($*1_ltype temp) {
   $1 = &temp;
 }
+#if defined(SWIGLUA)
+%typemap(argout,noblock=1) XRule **OUTPUT {
+  SWIG_NewPointerObj(L, (void *)(*$1), SWIGTYPE_p_XRule, SWIG_POINTER_OWN); SWIG_arg++;
+}
+#else
 %typemap(argout,noblock=1) XRule **OUTPUT {
   %append_output(SWIG_NewPointerObj((void*)(*$1), SWIGTYPE_p_XRule, SWIG_POINTER_OWN | %newpointer_flags));
 }
+#endif
 
+#if defined(SWIGLUA)
+%typemap(in,noblock=1,fragment="SWIG_AsValSolvFpPtr") FILE * {
+    {
+      FILE *val;
+      int ecode = SWIG_AsValSolvFpPtr(L, $input, &val);
+      if (!SWIG_IsOK(ecode)) SWIG_fail;
+      $1 = val;
+    }
+}
+%typemap(typecheck,precedence=%checkcode(POINTER),fragment="SWIG_AsValSolvFpPtr") FILE * {
+  int res = SWIG_AsValSolvFpPtr(L, $input, NULL);
+  $1 = SWIG_CheckState(res);
+}
+%typemap(in,noblock=1,fragment="SWIG_AsValDepId") DepId {
+    {
+      int val;
+      int ecode = SWIG_AsValDepId(L, $input, &val);
+      if (!SWIG_IsOK(ecode)) SWIG_fail;
+      $1 = val;
+    }
+}
+%typemap(typecheck,precedence=%checkcode(INT32),fragment="SWIG_AsValDepId") DepId {
+  int res = SWIG_AsValDepId(L, $input, NULL);
+  $1 = SWIG_CheckState(res);
+}
+#else
 %typemaps_asval(%checkcode(POINTER), SWIG_AsValSolvFpPtr, "SWIG_AsValSolvFpPtr", FILE*);
 %typemaps_asval(%checkcode(INT32), SWIG_AsValDepId, "SWIG_AsValDepId", DepId);
+#endif
 
 
 /**
@@ -1058,6 +1300,27 @@ SWIGINTERN void *appdata_get_helper(void **appdatap) {
 
 %}
 
+#elif defined(SWIGLUA)
+
+%{
+SWIGINTERN void appdata_disown_helper(void *appdata) {
+}
+SWIGINTERN void appdata_clr_helper(void **appdatap) {
+  if (*appdatap) {
+    void *appdata = *appdatap;
+    clr_stashed_lua_var((lua_State*)appdata, "appdata", (void *)appdatap);
+    *appdatap = 0;
+  }
+}
+SWIGINTERN void appdata_set_helper(void **appdatap, void *appdata) {
+  *appdatap = appdata;
+  set_stashed_lua_var((lua_State*)appdata, -1, "appdata", (void *)appdatap);
+}
+SWIGINTERN void *appdata_get_helper(void **appdatap) {
+  return (void *)appdatap;
+}
+%}
+
 #else
 #warning appdata helpers not implemented for this language
 #endif
@@ -1852,6 +2115,34 @@ returnself(matchsolvable)
       pool_setloadcallback($self, loadcallback, callback_var);
     }
   }
+#elif defined(SWIGLUA)
+  %{
+  SWIGINTERN int loadcallback(Pool *pool, Repodata *data, void *d) {
+    lua_State* L = d;
+    get_stashed_lua_var(L, "loadcallback", pool);
+    XRepodata *xd = new_XRepodata(data->repo, data->repodataid);
+    SWIG_NewPointerObj(L,SWIG_as_voidptr(xd), SWIGTYPE_p_XRepodata, 0);
+    int res = lua_pcall(L, 1, 1, 0);
+    res = res == LUA_OK ? lua_toboolean(L, -1) : 0;
+    lua_pop(L, 1);
+    return res;
+  }
+  %}
+  void clr_loadcallback() {
+    if ($self->loadcallback == loadcallback) {
+      lua_State* L = $self->loadcallbackdata;
+      clr_stashed_lua_var(L, "loadcallback", $self);
+      pool_setloadcallback($self, 0, 0);
+    }
+  }
+  void set_loadcallback(int lua_function_idx, lua_State* L) {
+    clr_stashed_lua_var(L, "loadcallback", $self);
+    if (!lua_isnil(L, lua_function_idx))  {
+      set_stashed_lua_var(L, lua_function_idx, "loadcallback", $self);
+      pool_setloadcallback($self, loadcallback, L);
+    }
+  }
+
 #else
 #warning loadcallback not implemented for this language
 #endif
@@ -2616,7 +2907,11 @@ returnself(matchsolvable)
   perliter(solv::Dataiterator)
 #endif
   %newobject __next__;
+#ifdef SWIGLUA
+  Datamatch *__next__(void *ign1=0, void *ign2=0) {
+#else
   Datamatch *__next__() {
+#endif
     Dataiterator *ndi;
     if (!dataiterator_step($self)) {
       return 0;
@@ -2865,7 +3160,11 @@ returnself(matchsolvable)
   perliter(solv::Pool_solvable_iterator)
 #endif
   %newobject __next__;
+#ifdef SWIGLUA
+  XSolvable *__next__(void *ign1=0, void *ign2=0) {
+#else
   XSolvable *__next__() {
+#endif
     Pool *pool = $self->pool;
     if ($self->id >= pool->nsolvables)
       return 0;
@@ -2919,7 +3218,11 @@ returnself(matchsolvable)
 #ifdef SWIGPERL
   perliter(solv::Pool_repo_iterator)
 #endif
+#ifdef SWIGLUA
+  Repo *__next__(void *ign1=0, void *ign2=0) {
+#else
   Repo *__next__() {
+#endif
     Pool *pool = $self->pool;
     if ($self->id >= pool->nrepos)
       return 0;
@@ -2975,7 +3278,11 @@ returnself(matchsolvable)
   perliter(solv::Repo_solvable_iterator)
 #endif
   %newobject __next__;
+#ifdef SWIGLUA
+  XSolvable *__next__(void *ign1=0, void *ign2=0) {
+#else
   XSolvable *__next__() {
+#endif
     Repo *repo = $self->repo;
     Pool *pool = repo->pool;
     if (repo->start > 0 && $self->id < repo->start)
diff --git a/examples/luasolv b/examples/luasolv
new file mode 100644 (file)
index 0000000..0e1dc58
--- /dev/null
@@ -0,0 +1,590 @@
+#!/usr/bin/lua
+
+releasever = nil
+solvcachedir = '/var/cache/solv'
+
+posix = require("posix")
+posix.sys.stat = require("posix.sys.stat")
+require("solv")
+
+-- helpers
+
+function parse_ini_file(fn)
+  local sections = {}
+  for line in io.open(fn, 'r'):lines() do
+    local match = line:match('^%[([^%[%]]+)%]$')
+    if match then
+      section = match
+      sections[section] = sections[section] or {}
+    else
+      local param, value = line:match('^([%w|_]+)%s-=%s-(.+)$')
+      if param then sections[section][param] = value end
+    end
+  end
+  return sections
+end
+
+function die(str)
+  io.stdout:write(str.."\n")
+  os.exit(1)
+end
+
+function isdir(path)
+  local st = posix.sys.stat.stat(path)
+  return st and posix.S_ISDIR(st.st_mode) ~= 0
+end
+
+function lsdir(path)
+  local content = posix.dirent.dir(path) or {}
+  table.sort(content)
+  return content
+end
+
+function load_stub(repodata)
+  local repo = repodata.repo.appdata
+  if repo then
+    return repo:load_ext(repodata)
+  end
+  return false
+end
+
+-- generic repo implementation
+
+Repo = {}
+
+function Repo.calc_cookie_filename(filename)
+  chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256)
+  chksum:add("1.1")
+  chksum:add_stat(filename)
+  return chksum:raw()
+end
+
+function Repo.calc_cookie_fp(fp)
+  chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256)
+  chksum:add("1.1")
+  chksum:add_fp(fp)
+  return chksum:raw()
+end
+
+function Repo.calc_cookie_ext(f, cookie)
+  chksum = solv.Chksum(solv.REPOKEY_TYPE_SHA256)
+  chksum:add("1.1")
+  chksum:add(cookie)
+  chksum:add_fstat(f:fileno())
+  return chksum:raw()
+end
+
+function Repo:download(file, uncompress, chksum, markincomplete)
+  if not self.baseurl then
+    io.stdout:write(self.alias..": no baseurl\n")
+    return nil
+  end
+  local url = self.baseurl:gsub('/$', '')..'/'..file
+  local tmpfile = io.tmpfile()
+  local fd = posix.stdio.fileno(tmpfile)
+  local status, reason = posix.spawn({'curl', '-f', '-s', '-L', '-o', '/dev/fd/'..fd, '--', url})
+  if posix.unistd.lseek(fd, 0, posix.unistd.SEEK_END) == 0 and (status == 0 or not chksum) then
+    tmpfile:close()
+    return nil
+  end
+  posix.unistd.lseek(fd, 0, posix.unistd.SEEK_SET);
+  if status ~= 0 then
+    print(file..": download error "..status.."\n");
+    tmpfile:close()
+    return nil
+  end
+  if chksum then
+    local fchksum = solv.Chksum(chksum.type);
+    fchksum:add_fd(fd)
+    if fchksum ~= chksum then
+      print(file..": checksum error")
+      if markincomplete then self.incomplete = 1 end
+      return nil
+    end
+  end
+  local ret
+  if uncompress then
+    ret = solv.xfopen_fd(file, fd);
+  else
+    ret = solv.xfopen_fd(nil, fd);
+  end
+  tmpfile:close()
+  return ret
+end
+
+function Repo:load(pool)
+  self.handle = pool:add_repo(self.alias)
+  self.handle.appdata = self
+  self.handle.priority = 99 - self.priority
+  if self:usecachedrepo() then
+    io.stdout:write("repo '"..self.alias.."': cached\n")
+    return true
+  end
+  return false
+end
+
+function Repo:cachepath(ext)
+  local path = self.alias:gsub('^%.', '_');
+  if ext then
+    path = path .. '_' .. ext ..'.solvx'
+  else
+    path = path ..'.solv'
+  end
+  return '/var/cache/solv/' .. path:gsub('/', '_');
+end
+  
+function Repo:usecachedrepo(ext, mark)
+  local cookie
+  if ext then
+    cookie = self.extcookie
+  else
+    cookie = self.cookie
+  end
+  local repopath = self:cachepath(ext)
+  local f = io.open(repopath, 'rb')
+  if not f then
+    return false
+  end
+  f:seek('end', -32)
+  local fcookie = f:read(32)
+  if not fcookie or fcookie:len() ~= 32 then
+    f:close()
+    return false
+  end
+  if cookie and cookie ~= fcookie then
+    f:close()
+    return false
+  end
+  local fextcookie
+  if self.type ~= 'system' and not ext then
+    f:seek('end', -64)
+    fextcookie = f:read(32)
+    if not fcookie or fcookie:len() ~= 32 then
+      f:close()
+      return false
+    end
+  end
+  f:seek('set')
+  posix.unistd.lseek(posix.stdio.fileno(f), 0, posix.unistd.SEEK_SET)
+  local ff = solv.xfopen_fd('', posix.stdio.fileno(f))
+  f:close()
+  local flags = 0
+  if ext then
+    flags = flags | solv.Repo.REPO_USE_LOADING|solv.Repo.REPO_EXTEND_SOLVABLES
+    if ext ~= 'DL' then flags = flags | solv.Repo.REPO_LOCALPOOL end
+  end
+  if not self.handle:add_solv(ff, flags) then
+    ff:close()
+    return false
+  end
+  if self.type ~= 'system' and not ext then
+    self.cookie = fcookie
+    self.extcookie = fextcookie
+  end
+  if mark then
+    posix.utime(repopath)
+  end
+  return true
+end
+
+function Repo:writecachedrepo(ext, repodata)
+  if self.incomplete then return end
+  if not isdir(solvcachedir) then
+    posix.sys.stat.mkdir(solvcachedir, 7 * 64 + 5 * 8 + 5)
+  end
+  local fd, tmpname = posix.stdlib.mkstemp(solvcachedir..'/.newsolv-XXXXXX')
+  if not fd then
+    return
+  end
+  local ff =  solv.xfopen_fd('', fd)
+  if not repodata then
+    self.handle:write(ff)
+  elseif ext then
+    repodata:write(ff)
+  else
+    self.handle:write_first_repodata(ff)
+  end
+  ff:flush()
+  if self.type ~= 'system' and not ext then
+    self.extcookie = self.extcookie or Repo.calc_cookie_ext(ff, self.cookie)
+    ff:write(self.extcookie)
+  end
+  if not ext then
+    ff:write(self.cookie)
+  else
+    ff:write(self.extcookie)
+  end
+  ff:close()
+  os.rename(tmpname, self:cachepath(ext))
+end
+
+function Repo:add_ext_keys(ext, repodata, handle)
+  if ext == 'DL' then
+    repodata:add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOSITORY_DELTAINFO)
+    repodata:add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_FLEXARRAY)
+  elseif ext == 'DU' then
+    repodata:add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_DISKUSAGE)
+    repodata:add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRNUMNUMARRAY)
+  elseif ext == 'FL' then
+    repodata:add_idarray(handle, solv.REPOSITORY_KEYS, solv.SOLVABLE_FILELIST)
+    repodata:add_idarray(handle, solv.REPOSITORY_KEYS, solv.REPOKEY_TYPE_DIRSTRARRAY)
+  end
+end
+
+-- rpmmd repo implementation
+
+Repo_rpmmd = {}
+setmetatable(Repo_rpmmd, {__index = Repo })
+
+function Repo_rpmmd:find(what)
+  local di = self.handle:Dataiterator_meta(solv.REPOSITORY_REPOMD_TYPE, what, solv.Dataiterator.SEARCH_STRING)
+  di:prepend_keyname(solv.REPOSITORY_REPOMD)
+  for d in di do
+    local dp = d:parentpos()
+    local filename = dp:lookup_str(solv.REPOSITORY_REPOMD_LOCATION)
+    if filename then
+      local chksum = dp:lookup_checksum(solv.REPOSITORY_REPOMD_CHECKSUM)
+      if not chksum then
+       print("no "..filename.." file checksum!\n")
+       return
+      end
+      return filename, chksum
+    end
+  end
+end
+
+function Repo_rpmmd:add_ext(repodata, what, ext)
+  local filename, filechksum = self:find(what)
+  if not filename and what == 'deltainfo' then
+    filename, filechksum = self:find('prestodelta')
+  end
+  if filename then
+    local handle = repodata:new_handle()
+    repodata:set_poolstr(handle, solv.REPOSITORY_REPOMD_TYPE, what)
+    repodata:set_str(handle, solv.REPOSITORY_REPOMD_LOCATION, filename)
+    repodata:set_checksum(handle, solv.REPOSITORY_REPOMD_CHECKSUM, filechksum)
+    self:add_ext_keys(ext, repodata, handle)
+    repodata:add_flexarray(solv.SOLVID_META, solv.REPOSITORY_EXTERNAL, handle)
+  end
+end
+
+function Repo_rpmmd:add_exts()
+  repodata = self.handle:add_repodata(0)
+  repodata:extend_to_repo()
+  self:add_ext(repodata, 'deltainfo', 'DL')
+  self:add_ext(repodata, 'filelists', 'FL')
+  repodata:internalize()
+end
+
+function Repo_rpmmd:load(pool)
+  if Repo.load(self, pool) then return true end
+  io.stdout:write("rpmmd repo '"..self.alias.."': ")
+  local f = self:download("repodata/repomd.xml");
+  if not f then
+    print("no repomd.xml file, skipped");
+    self.handle:free(true)
+    self.handle = nil
+    return false
+  end
+  self.cookie = self.calc_cookie_fp(f)
+  if self:usecachedrepo(nil, True) then
+    print("cached")
+    return true
+  end
+  self.handle:add_repomdxml(f, 0)
+  f:close()
+  print("fetching")
+  local filename, filechksum
+  filename, filechksum = self:find('primary')
+  if filename then
+    f = self:download(filename, true, filechksum, true)
+    if f then
+      self.handle:add_rpmmd(f, nil, 0)
+      f:close()
+    end
+    if self.incomplete then return false end
+  end
+  filename, filechksum = self:find('updateinfo')
+  if filename then
+    f = self:download(filename, true, filechksum, true)
+    if f then
+      self.handle:add_updateinfoxml(f, 0)
+      f:close()
+    end
+  end
+  self:add_exts()
+  self:writecachedrepo()
+  self.handle:create_stubs()
+  return true
+end
+
+function Repo_rpmmd:load_ext(repodata)
+  local repomdtype = repodata:lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_TYPE)
+  local ext
+  if repomdtype == 'filelists' then
+    ext = 'FL'
+  elseif repomdtype == 'deltainfo' then
+    ext = 'DL'
+  else
+    return false
+  end
+  io.stdout:write("["..self.alias..":"..ext..": ")
+  if self:usecachedrepo(ext) then
+    io.stdout:write("cached]\n")
+    return true
+  end
+  io.stdout:write("fetching]\n")
+  local filename = repodata:lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_LOCATION)
+  local filechksum = repodata:lookup_str(solv.SOLVID_META, solv.REPOSITORY_REPOMD_CHECKSUM)
+  local f = self:download(filename, true, filechksum)
+  if not f then
+print("download failed")
+    return false
+  end
+  if ext == 'FL' then
+    self.handle:add_rpmmd(f, 'FL', solv.Repo.REPO_USE_LOADING|solv.Repo.REPO_EXTEND_SOLVABLES|solv.Repo.REPO_LOCALPOOL)
+  elseif ext == 'DL' then
+    self.handle:add_deltainfoxml(f, solv.Repo.REPO_USE_LOADING)
+  end
+  self:writecachedrepo(ext, repodata)
+  return true
+end
+
+-- susetags repo implementation
+
+Repo_susetags = {}
+setmetatable(Repo_susetags, {__index = Repo })
+
+-- unknown repo implementation
+
+Repo_unknown = {}
+setmetatable(Repo_unknown, {__index = Repo })
+
+function Repo_unknown:load()
+  print("unsupported repo '"..self.alias.."': skipped")
+  return false
+end
+
+-- system repo implementation
+
+Repo_system = {}
+setmetatable(Repo_system, {__index = Repo })
+
+function Repo_system:load(pool)
+  self.handle = pool:add_repo(self.alias)
+  self.handle.appdata = self
+  pool.installed = self.handle
+  io.stdout:write("rpm database: ")
+  self.cookie = Repo.calc_cookie_filename("/var/lib/rpm/Packages")
+  if self:usecachedrepo() then
+    print("cached")
+    return true
+  end
+  io.stdout:write("reading\n")
+  local oldf = solv.xfopen(self:cachepath())
+  self.handle:add_rpmdb_reffp(oldf, solv.Repo.REPO_REUSE_REPODATA)
+  self:writecachedrepo()
+end
+
+-- main
+
+cmd = arg[1]
+if not cmd then die("Usage: luasolv COMMAND [ARGS]") end
+table.remove(arg, 1)
+
+cmdabbrev = { ['ls']='list'; ['in']='install'; ['rm']='erase'; ['ve']='verify'; ['se']='search'  }
+cmd = cmdabbrev[cmd] or cmd
+
+cmdactions = { ['install']=solv.Job.SOLVER_INSTALL; ['erase']=solv.Job.SOLVER_ERASE; ['up']=solv.Job.SOLVER_UPDATE; ['dup']=solv.Job.SOLVER_DISTUPGRADE; ['verify']=solv.Job.SOLVER_VERIFY; ['list']=0; ['info']=0 }
+
+repos = {}
+reposdir = {}
+if isdir('/etc/zypp/repos.d') then
+  table.insert(reposdir, '/etc/zypp/repos.d')
+elseif isdir('/etc/yum/repos.d') then
+  table.insert(reposdir, '/etc/yum/repos.d')
+end
+for _, repodir in ipairs(reposdir) do
+  for _, e in ipairs(lsdir(repodir)) do
+    if e:sub(-5) == '.repo' then
+      sections = parse_ini_file(repodir..'/'..e)
+      for alias, section in pairs(sections) do
+       repo = { ['alias']=alias; ['enabled']=false; ['priority']=99; ['autorefresh']=true; ['type']='rpm-md'; ['metadata_expire']=900 }
+       if section.name then repo.name = section.name end
+       if section.type then repo.type = section.type end
+       if section.baseurl then repo.baseurl = section.baseurl end
+       if section.metadata_expire then repo.metadata_expire = tonumber(section.metadata_expire) end
+       if section.enabled then repo.enabled = tonumber(section.enabled) ~= 0 end
+       if section.autorefresh then repo.autorefresh = tonumber(section.autorefresh) ~= 0 end
+       if section.gpgcheck then repo.gpgcheck = tonumber(section.gpgcheck) ~= 0 end
+       if section.priority then repo.priority = tonumber(section.priority) end
+       if repo.baseurl and releasever then repo.baseurl = repo.baseurl:gsub('$releasever', releasever) end
+       if repo.type == 'rpm-md' then
+         setmetatable(repo , {__index = Repo_rpmmd })
+       elseif repo['type'] == 'yast2' then
+         setmetatable(repo , {__index = Repo_susetags })
+       else
+         setmetatable(repo , {__index = Repo_unknown })
+       end
+       table.insert(repos, repo)
+      end
+    end
+  end
+end
+
+local pool = solv.Pool()
+pool:setarch()
+pool:set_loadcallback(load_stub)
+
+sysrepo = { ['alias']='@System', ['type']='system' }
+setmetatable(sysrepo , {__index = Repo_system })
+sysrepo:load(pool)
+for _, repo in ipairs(repos) do
+  if repo.enabled then
+    repo:load(pool)
+  end
+end
+
+if cmd == 'search' then
+  pool:createwhatprovides()
+  sel = pool:Selection()
+  di = pool:Dataiterator(solv.SOLVABLE_NAME, arg[1], solv.Dataiterator.SEARCH_SUBSTRING | solv.Dataiterator.SEARCH_NOCASE)
+  for d in di do
+    sel:add_raw(solv.Job.SOLVER_SOLVABLE, d.solvid)
+  end
+  for _, s in ipairs(sel:solvables()) do
+    print('  - '..tostring(s)..' ['..s.repo.name..']: '..s:lookup_str(solv.SOLVABLE_SUMMARY))
+  end
+  os.exit(0)
+end
+
+if not cmdactions[cmd] then die("unknown command '"..cmd.."'") end
+
+pool:addfileprovides()
+pool:createwhatprovides()
+
+jobs = {}
+for _, a in ipairs(arg) do
+  flags = solv.Selection.SELECTION_NAME | solv.Selection.SELECTION_PROVIDES | solv.Selection.SELECTION_GLOB
+  flags = flags | solv.Selection.SELECTION_CANON | solv.Selection.SELECTION_DOTARCH | solv.Selection.SELECTION_REL
+  if a:sub(1, 1) == '/' then
+    flags = flags | solv.Selection.SELECTION_FILELIST
+    if cmd == 'erase' then
+      flags = flags | solv.Selection.SELECTION_INSTALLED_ONLY
+    end
+  end
+  local sel = pool:select(a, flags)
+  if sel:isempty() then
+    sel = pool:select(a, flags | solv.Selection.SELECTION_NOCASE)
+    if not sel:isempty() then
+      print("[ignoring case for '"..a.."']")
+    end
+  end
+  if (sel.flags & solv.Selection.SELECTION_FILELIST) ~= 0 then
+    print("[using file list match for '"..a.."']")
+  end
+  if (sel.flags & solv.Selection.SELECTION_PROVIDES) ~= 0 then
+    print("[using capability match for '"..a.."']")
+  end
+  for _, j in ipairs(sel:jobs(cmdactions[cmd])) do
+     table.insert(jobs, j)
+  end
+end
+
+if #jobs == 0 and (cmd == 'up' or cmd == 'dup' or cmd == 'verify') then
+  for _, j in ipairs(pool:Selection_all():jobs(cmdactions[cmd])) do
+     table.insert(jobs, j)
+  end
+end
+
+if #jobs == 0 then die("no package matched.") end
+
+if cmd == 'list' or cmd == 'info' then
+  for _, job in ipairs(jobs) do
+    for _, s in ipairs(job:solvables()) do
+      if cmd == 'info' then
+       local str
+        print('Name:        '..tostring(s))
+        print('Repo:        '..s.repo.name)
+        print('Summary:     '..s:lookup_str(solv.SOLVABLE_SUMMARY))
+        str = s:lookup_str(solv.SOLVABLE_URL)
+       if str then print('Url:         '..str) end
+        str = s:lookup_str(solv.SOLVABLE_LICENSE)
+       if str then print('License:     '..str) end
+        print("Description\n"..s:lookup_str(solv.SOLVABLE_DESCRIPTION))
+        print('')
+      else
+        print('  - '..tostring(s)..' ['..s.repo.name..']')
+        print('    '..s:lookup_str(solv.SOLVABLE_SUMMARY))
+      end
+    end
+  end
+  os.exit(0)
+end
+
+local solver = pool:Solver()
+if cmd == 'erase' then
+  solver:set_flag(solv.Solver.SOLVER_FLAG_ALLOW_UNINSTALL, 1)
+end
+
+while true do
+  local problems = solver:solve(jobs)
+  if #problems == 0 then break end
+  for _, problem in ipairs(problems) do
+    print(("Problem %d/%d:"):format(problem.id, #problems))
+        print(problem)
+        local solutions = problem:solutions()
+        for _, solution in ipairs(solutions) do
+            print(("  Solution %d:"):format(solution.id))
+            local elements = solution:elements(true)
+            for _, element in ipairs(elements) do
+                print("  - ".. element:str())
+           end
+            print('')
+       end
+  end
+  os.exit(1)
+end
+
+local trans = solver:transaction()
+if trans:isempty() then
+  print("Nothing to do.")
+  os.exit(0)
+end
+print('')
+print("Transaction summary:")
+print('')
+for _, cl in ipairs(trans:classify(solv.Transaction.SOLVER_TRANSACTION_SHOW_OBSOLETES | solv.Transaction.SOLVER_TRANSACTION_OBSOLETE_IS_UPGRADE)) do
+  if cl.type == solv.Transaction.SOLVER_TRANSACTION_ERASE then
+    print(("%d erased packages:"):format(cl.count))
+  elseif cl.type == solv.Transaction.SOLVER_TRANSACTION_INSTALL then
+    print(("%d installed packages:"):format(cl.count))
+  elseif cl.type == solv.Transaction.SOLVER_TRANSACTION_REINSTALLED then
+    print(("%d reinstalled packages:"):format(cl.count))
+  elseif cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED then
+    print(("%d downgraded packages:"):format(cl.count))
+  elseif cl.type == solv.Transaction.SOLVER_TRANSACTION_CHANGED then
+    print(("%d changed packages:"):format(cl.count))
+  elseif cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED then
+    print(("%d upgraded packages:"):format(cl.count))
+  elseif cl.type == solv.Transaction.SOLVER_TRANSACTION_VENDORCHANGE then
+    print(("%d vendor changes from '%s' to '%s':"):format(cl.count, cl.fromstr, cl.tostr))
+  elseif cl.type == solv.Transaction.SOLVER_TRANSACTION_ARCHCHANGE then
+    print(("%d arch changes from '%s' to '%s':"):format(cl.count, cl.fromstr, cl.tostr))
+  else
+    cl = nil
+  end
+  if cl then
+    for _, p in ipairs(cl:solvables()) do
+      if cl.type == solv.Transaction.SOLVER_TRANSACTION_UPGRADED or cl.type == solv.Transaction.SOLVER_TRANSACTION_DOWNGRADED then
+        print(("  - %s -> %s"):format(p, trans:othersolvable(p)))
+      else
+        print(("  - %s"):format(p))
+      end
+    end
+    print('')
+  end
+end
+print(("install size change: %d K"):format(trans:calc_installsizechange()))
+print('')
+