Make pip buildable

This package would be very useful if able to be built with pip.  This
commit makes libcper usable from python with
git clone git@github.com:openbmc/libcper.git
pip install libcper

The API at the moment is primitive, and only supports the parse api.  An
example of its usage is included.

Change-Id: I944565c71f616735a738bcc4f815d25251ed27bb
Signed-off-by: Ed Tanous <etanous@nvidia.com>
diff --git a/meson.build b/meson.build
index 4240cd3..ede0bf1 100644
--- a/meson.build
+++ b/meson.build
@@ -2,10 +2,9 @@
     'libcper',
     'c',
     version: '0.1',
-    meson_version: '>=1.1.1',
+    meson_version: '>=1.2.0',
     default_options: [
         'c_std=c18',
-        'cpp_std=c++23',
         'tests=' + (meson.is_subproject() ? 'disabled' : 'enabled'),
         'warning_level=2',
         'werror=true',
@@ -74,7 +73,11 @@
 
 cc = meson.get_compiler('c')
 
-json_c_dep = dependency('json-c')
+json_c = dependency('json-c', default_options: {'warning_level': '0'})
+json_c_dep = declare_dependency(
+    include_directories: include_directories('subprojects'),
+    dependencies: json_c,
+)
 libcper_include = ['include']
 libcper_include_dir = include_directories(libcper_include, is_system: true)
 
@@ -89,6 +92,8 @@
 
 subdir('include')
 
+install = get_option('install').allowed()
+
 libcper_parse = library(
     'cper-parse',
     libcper_parse_sources,
@@ -97,8 +102,7 @@
     include_directories: libcper_include_dir,
     c_args: '-Wno-address-of-packed-member',
     dependencies: [json_c_dep],
-    install: true,
-    install_dir: get_option('libdir'),
+    install: install,
 )
 libcper_parse_dep = declare_dependency(
     include_directories: libcper_include_dir,
@@ -117,20 +121,21 @@
     include_directories: libcper_include_dir,
     dependencies: [json_c_dep],
     link_with: [libcper_parse],
-    install: true,
-    install_dir: get_option('libdir'),
+    install: install,
 )
 libcper_generate_dep = declare_dependency(
     include_directories: libcper_include_dir,
     link_with: libcper_generate,
 )
 
-import('pkgconfig').generate(
-    libcper_parse,
-    name: meson.project_name(),
-    version: meson.project_version(),
-    description: 'C bindings for parsing CPER',
-)
+if get_option('pkgconfig').allowed()
+    import('pkgconfig').generate(
+        libcper_parse,
+        name: meson.project_name(),
+        version: meson.project_version(),
+        description: 'C bindings for parsing CPER',
+    )
+endif
 
 if get_option('utility').allowed()
     executable(
@@ -139,7 +144,7 @@
         include_directories: libcper_include_dir,
         dependencies: [json_c_dep],
         link_with: [libcper_parse, libcper_generate],
-        install: true,
+        install: install,
         install_dir: get_option('bindir'),
     )
 
@@ -149,7 +154,7 @@
         edk_sources,
         include_directories: libcper_include_dir,
         link_with: [libcper_parse, libcper_generate],
-        install: true,
+        install: install,
         install_dir: get_option('bindir'),
     )
 endif
@@ -161,3 +166,14 @@
         subdir('tests')
     endif
 endif
+
+if get_option('python').allowed()
+    py = import('python').find_installation(pure: false)
+    py.extension_module(
+        'cper',
+        'pycper.c',
+        c_args: ['-DLIBCPER_PYTHON'],
+        dependencies: [libcper_parse_dep, json_c_dep],
+        install: true,
+    )
+endif
diff --git a/meson.options b/meson.options
index 9cda0c4..ffd7c74 100644
--- a/meson.options
+++ b/meson.options
@@ -5,7 +5,25 @@
     description: 'Build fuzz targets',
 )
 option('tests', type: 'feature', value: 'enabled', description: 'Build tests')
-option('utility', type: 'feature', value: 'enabled', description: 'Utility')
+option(
+    'utility',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Compile CLI utilities',
+)
+option(
+    'python',
+    type: 'feature',
+    value: 'disabled',
+    description: 'Build python extensions',
+)
+option('install', type: 'feature', value: 'enabled', description: 'Install')
+option(
+    'pkgconfig',
+    type: 'feature',
+    value: 'enabled',
+    description: 'Install pkgconfig',
+)
 option(
     'output-all-properties',
     type: 'feature',
diff --git a/parse_example.py b/parse_example.py
new file mode 100755
index 0000000..3760b35
--- /dev/null
+++ b/parse_example.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+
+import json
+
+import cper
+
+
+def main():
+    cper_raw = (
+        b"\x43\x50\x45\x52\x01\x01\xff\xff\xff\xff\x01\x00\x03"
+        b"\x00\x00\x00\x00\x00\x00\x00\x28\x01\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x78"
+        b"\xe4\x01\xa9\x73\x11\xef\x11\x96\xa8\x5f\xa6\xbe\xf5"
+        b"\xee\xa4\xac\xd5\xa9\x09\x04\x52\x14\x42\x96\xe5\x94"
+        b"\x99\x2e\x75\x2b\xcd\x00\x00\x00\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc8\x00"
+        b"\x00\x00\x60\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00"
+        b"\x00\xf2\x44\x52\x6d\x12\x27\xec\x11\xbe\xa7\xcb\x3f"
+        b"\xdb\x95\xc7\x86\x4a\x33\x4f\xcc\x63\xc5\xeb\x11\x8f"
+        b"\x88\x9f\x7a\xc7\x6c\x6f\x0c\x03\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x44\x52\x41\x4d\x2d\x43\x48\x41"
+        b"\x4e\x4e\x45\x4c\x53\x00\x00\x00\x00\x20\x00\x00\x03"
+        b"\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\xbe"
+        b"\x02\x04\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00"
+        b"\x00\x04\xbe\x02\x04\x00\x10\x00\x00\xff\xff\xff\xff"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+        b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    )
+
+    result = cper.parse(cper_raw)
+    print(json.dumps(result, indent=4))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/pycper.c b/pycper.c
new file mode 100644
index 0000000..5c5ec22
--- /dev/null
+++ b/pycper.c
@@ -0,0 +1,125 @@
+#if LIBCPER_PYTHON
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <libcper/cper-parse-str.h>
+#include <libcper/cper-parse.h>
+
+PyObject *convert_to_pydict(json_object *jso)
+{
+	PyObject *ret = Py_None;
+	enum json_type type = json_object_get_type(jso);
+	//printf("type: %d ", type);
+	switch (type) {
+	case json_type_null:
+		//printf("json_type_null\n");
+		ret = Py_None;
+		break;
+	case json_type_boolean: {
+		//printf("json_type_boolean\n");
+		int b = json_object_get_boolean(jso);
+		ret = PyBool_FromLong(b);
+	} break;
+	case json_type_double: {
+		//printf("json_type_double\n");
+		double d = json_object_get_double(jso);
+		ret = PyFloat_FromDouble(d);
+	} break;
+
+	case json_type_int: {
+		//printf("json_type_int\n");
+		int64_t i = json_object_get_int64(jso);
+		ret = PyLong_FromLong(i);
+	} break;
+	case json_type_object: {
+		//printf("json_type_object\n");
+		ret = PyDict_New();
+
+		json_object_object_foreach(jso, key1, val)
+		{
+			PyObject *pyobj = convert_to_pydict(val);
+			if (key1 != NULL) {
+				//printf("Parsing %s\n", key1);
+				if (pyobj == NULL) {
+					pyobj = Py_None;
+				}
+				PyDict_SetItemString(ret, key1, pyobj);
+			}
+		}
+	} break;
+	case json_type_array: {
+		//printf("json_type_array\n");
+		ret = PyList_New(0);
+		int arraylen = json_object_array_length(jso);
+
+		for (int i = 0; i < arraylen; i++) {
+			//printf("Parsing %d\n", i);
+			json_object *val = json_object_array_get_idx(jso, i);
+			PyObject *pyobj = convert_to_pydict(val);
+			if (pyobj == NULL) {
+				pyobj = Py_None;
+			}
+			PyList_Append(ret, pyobj);
+		}
+	} break;
+	case json_type_string: {
+		//printf("json_type_string\n");
+		const char *strval = json_object_get_string(jso);
+		ret = PyUnicode_FromString(strval);
+	} break;
+	}
+	return ret;
+}
+
+static PyObject *parse(PyObject *self, PyObject *args)
+{
+	(void)self;
+	PyObject *ret;
+	const unsigned char *data;
+	Py_ssize_t count;
+
+	if (!PyArg_ParseTuple(args, "y#", &data, &count)) {
+		return NULL;
+	}
+
+	char *jstrout = cperbuf_to_str_ir(data, count);
+	if (jstrout == NULL) {
+		free(jstrout);
+		return NULL;
+	}
+	json_object *jout = cper_buf_to_ir(data, count);
+	if (jout == NULL) {
+		free(jstrout);
+		free(jout);
+		return NULL;
+	}
+
+	ret = convert_to_pydict(jout);
+
+	//ret = PyUnicode_FromString(jstrout);
+	free(jout);
+	free(jstrout);
+	return ret;
+}
+
+static PyMethodDef methods[] = {
+	{ "parse", (PyCFunction)parse, METH_VARARGS, NULL },
+	{ NULL, NULL, 0, NULL },
+};
+
+static struct PyModuleDef module = {
+	PyModuleDef_HEAD_INIT,
+	"cper",
+	NULL,
+	-1,
+	methods,
+	NULL,
+	NULL,
+	NULL,
+	NULL,
+};
+
+PyMODINIT_FUNC PyInit_cper(void)
+{
+	return PyModule_Create(&module);
+}
+#endif
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..2116d15
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,35 @@
+[build-system]
+build-backend = 'mesonpy'
+requires = ['meson-python']
+
+[project]
+name = 'cper'
+version = '0.0.4'
+description = 'Package for decoding CPER values'
+license = {file = 'LICENSE'}
+authors = [
+  {name = 'Ed Tanous', email = 'ed@tanous.net'},
+]
+requires-python = ">3.10"
+
+[tool.meson-python.args]
+setup = [
+  '-Ddefault_library=static',
+  '-Dinstall=disabled',
+  '-Dpkgconfig=disabled',
+  '-Dpython=enabled',
+  '-Dtests=disabled',
+  '-Dtests=disabled',
+  '-Dutility=disabled',
+]
+install = ['--skip-subprojects']
+
+[tool.cibuildwheel]
+archs = "auto"
+skip = "*pp*"
+manylinux-x86_64-image = "musllinux_1_2"
+manylinux-i686-image = "musllinux_1_2"
+manylinux-aarch64-image = "musllinux_1_2"
+manylinux-ppc64le-image = "musllinux_1_2"
+manylinux-s390x-image = "musllinux_1_2"
+manylinux-armv7l-image = "musllinux_1_2"
diff --git a/subprojects/json-c.wrap b/subprojects/json-c.wrap
index e68e6ef..a633336 100644
--- a/subprojects/json-c.wrap
+++ b/subprojects/json-c.wrap
@@ -1,7 +1,8 @@
 [wrap-git]
-revision = json-c-0.18-20240915
+revision = HEAD
 url = https://github.com/json-c/json-c.git
 patch_directory = json-c
 
 [provide]
 json-c = json_c_dep
+
diff --git a/subprojects/packagefiles/json-c/config/meson.build b/subprojects/packagefiles/json-c/config/meson.build
new file mode 100644
index 0000000..0c57785
--- /dev/null
+++ b/subprojects/packagefiles/json-c/config/meson.build
@@ -0,0 +1,110 @@
+conf = configuration_data()
+check_headers = [
+    'dlfcn.h',
+    'endian.h',
+    'fcntl.h',
+    'float.h',
+    'inttypes.h',
+    'limits.h',
+    'memory.h',
+    'stdarg.h',
+    'stdint.h',
+    'stdlib.h',
+    'strings.h',
+    'string.h',
+    'syslog.h',
+    'sys/cdefs.h',
+    'sys/param.h',
+    'sys/stat.h',
+    'sys/types.h',
+    'unistd.h',
+    'xlocale.h',
+]
+
+foreach header : check_headers
+    if cc.has_header(header)
+        conf.set('HAVE_@0@'.format(header.underscorify().to_upper()), 1)
+    endif
+endforeach
+
+have_stdc = true
+foreach header : ['stdlib.h', 'stdarg.h', 'string.h', 'float.h']
+    if not conf.has('HAVE_@0@'.format(header.underscorify().to_upper()))
+        have_stdc = false
+    endif
+endforeach
+conf.set10('STDC_HEADERS', have_stdc)
+
+foreach symbol : ['_isnan', '_finite']
+    if cc.has_header_symbol('float.h', symbol)
+        conf.set('HAVE_DECL_@0@'.format(symbol.to_upper()), 1)
+    endif
+endforeach
+
+foreach symbol : ['INFINITY', 'isinf', 'isnan', 'nan']
+    if cc.has_header_symbol('math.h', symbol)
+        conf.set('HAVE_DECL_@0@'.format(symbol.to_upper()), 1)
+    endif
+endforeach
+
+check_function = [
+    'vasprintf',
+    'realloc',
+    'strcasecmp',
+    'strdup',
+    'strerror',
+    'vsyslog',
+    'open',
+    'strtoll',
+]
+
+if conf.has('HAVE_STRINGS_H')
+    check_function += 'strncasecmp'
+endif
+
+foreach function : check_function
+    if cc.has_function(function)
+        conf.set('HAVE_@0@'.format(function.to_upper()), 1)
+    endif
+endforeach
+conf.set10('HAVE_DOPRNT', cc.has_function('_doprnt'))
+
+foreach f : ['snprintf', 'vsnprintf', 'vprintf']
+    if cc.has_header_symbol('stdio.h', f)
+        conf.set('HAVE_@0@'.format(f.to_upper()), 1)
+    endif
+endforeach
+
+size = cc.sizeof('size_t', prefix: '#include <stddef.h>')
+conf.set('SIZEOF_SIZE_T', size)
+
+if cc.get_argument_syntax() == 'msvc'
+    size = cc.sizeof('SSIZE_T', prefix: '#include <BaseTsd.h>')
+else
+    size = cc.sizeof('ssize_t', prefix: '#include <sys/types.h>')
+endif
+conf.set('SIZEOF_SSIZE_T', size)
+
+foreach type : ['int', 'int64_t', 'long', 'long long']
+    size = cc.sizeof(type, prefix: '#include <stdint.h>')
+    conf.set('SIZEOF_@0@'.format(type.underscorify().to_upper()), size)
+endforeach
+
+if cc.links('int main(){__sync_synchronize();}', name: 'atomic builtins')
+    conf.set('HAVE_ATOMIC_BUILTINS', 1)
+endif
+
+if cc.compiles('static __thread int x = 0;', name: '__thread')
+    conf.set('HAVE___THREAD', 1)
+endif
+
+if conf.has('HAVE___THREAD')
+    conf.set('SPEC___THREAD', '__thread')
+elif cc.get_argument_syntax() == 'msvc'
+    conf.set('SPEC___THREAD', '__declspec(thread)')
+endif
+
+conf.set_quoted('VERSION', meson.project_version())
+
+config_h = configure_file(configuration: conf, output: 'config.h')
+
diff --git a/subprojects/packagefiles/json-c/meson.build b/subprojects/packagefiles/json-c/meson.build
new file mode 100644
index 0000000..c2d1c03
--- /dev/null
+++ b/subprojects/packagefiles/json-c/meson.build
@@ -0,0 +1,111 @@
+project(
+    'json-c',
+    'c',
+    version: '0.18',
+    license: 'MIT',
+    meson_version: '>=0.49',
+    default_options: [
+        meson.version().version_compare('>=1.3.0') ? 'c_std=gnu99,c99' : 'c_std=gnu99',
+    ],
+)
+
+cc = meson.get_compiler('c')
+
+if cc.get_argument_syntax() == 'msvc'
+    add_project_arguments('-D_CRT_SECURE_NO_WARNINGS', language: 'c')
+else
+    add_project_arguments('-D_GNU_SOURCE', language: 'c')
+endif
+
+if get_option('default_library') != 'static'
+    add_project_arguments('-DJSON_C_DLL', language: 'c')
+endif
+
+add_project_arguments(
+    cc.get_supported_arguments('-Wno-deprecated-declarations'),
+    language: 'c',
+)
+
+threads = dependency('threads', required: false)
+math = cc.find_library('m', required: false)
+
+subdir('config')
+inc = include_directories('config')
+
+json_config = configuration_data()
+if conf.has('HAVE_INTTYPES_H')
+    json_config.set('JSON_C_HAVE_INTTYPES_H', 1)
+endif
+json_config_h = configure_file(
+    configuration: json_config,
+    output: 'json_config.h',
+)
+
+jconf = configuration_data()
+jconf.set('JSON_H_JSON_POINTER', '#include "json_pointer.h"')
+jconf.set('JSON_H_JSON_PATCH', '#include "json_patch.h"')
+
+json_h = configure_file(
+    input: 'json.h.cmakein',
+    output: 'json.h',
+    format: 'cmake@',
+    configuration: jconf,
+)
+
+public_headers = [
+    json_config_h,
+    json_h,
+    'arraylist.h',
+    'debug.h',
+    'json_c_version.h',
+    'json_inttypes.h',
+    'json_object.h',
+    'json_pointer.h',
+    'json_tokener.h',
+    'json_util.h',
+    'linkhash.h',
+    'printbuf.h',
+]
+
+sources = files(
+    'arraylist.c',
+    'debug.c',
+    'json_c_version.c',
+    'json_object.c',
+    'json_object_iterator.c',
+    'json_pointer.c',
+    'json_tokener.c',
+    'json_util.c',
+    'json_visit.c',
+    'linkhash.c',
+    'printbuf.c',
+    'random_seed.c',
+    'strerror_override.c',
+)
+
+libjson_c = library(
+    'json-c',
+    sources,
+    include_directories: inc,
+    install: true,
+    version: '5.4.0',
+)
+
+json_c_dep = declare_dependency(
+    link_with: libjson_c,
+    include_directories: include_directories('.', '..'),
+    dependencies: [math, threads],
+    sources: json_config_h,
+)
+
+install_headers(public_headers, subdir: 'json-c')
+
+pkgconfig = import('pkgconfig')
+pkgconfig.generate(
+    libjson_c,
+    description: 'A JSON implementation in C',
+    version: meson.project_version(),
+    filebase: meson.project_name(),
+    name: meson.project_name(),
+    subdirs: 'json-c',
+)
diff --git a/subprojects/packagefiles/jsoncdac/meson.build b/subprojects/packagefiles/jsoncdac/meson.build
index f7af58f..b47b676 100644
--- a/subprojects/packagefiles/jsoncdac/meson.build
+++ b/subprojects/packagefiles/jsoncdac/meson.build
@@ -23,7 +23,7 @@
 add_project_arguments('-Wno-unused-parameter', language: 'c')
 add_project_arguments('-Wformat=0', language: 'c')
 
-jsonc = dependency('json-c')
+jsonc = dependency('json-c', static: true)
 deps += jsonc
 
 jsoncdac_sources = files(
@@ -50,10 +50,15 @@
     deps += m_dep
 endif
 
-jsoncdac = library('jsoncdac', jsoncdac_sources, dependencies: deps)
+jsoncdac = library(
+    'jsoncdac',
+    jsoncdac_sources,
+    dependencies: deps,
+    include_directories: include_directories('..'),
+)
 
 jsoncdac_dep = declare_dependency(
     link_with: jsoncdac,
     dependencies: deps,
-    include_directories: include_directories('include'),
+    include_directories: include_directories('include', '..'),
 )