lbkit 0.9.12__tar.gz → 0.9.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. {lbkit-0.9.12/lbkit.egg-info → lbkit-0.9.14}/PKG-INFO +7 -1
  2. lbkit-0.9.14/lbkit/__commit__.py +1 -0
  3. lbkit-0.9.14/lbkit/__init__.py +2 -0
  4. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/cli.py +66 -23
  5. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/template/public.c.mako +3 -4
  6. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/component/arg_parser.py +2 -0
  7. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/component/build.py +3 -1
  8. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/component/template/conanbase.mako +18 -10
  9. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/component/template/deploy.mako +2 -0
  10. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/component/test.py +11 -2
  11. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/misc.py +28 -17
  12. lbkit-0.9.14/lbkit/upgrade.py +326 -0
  13. {lbkit-0.9.12 → lbkit-0.9.14/lbkit.egg-info}/PKG-INFO +7 -1
  14. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit.egg-info/SOURCES.txt +1 -0
  15. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit.egg-info/requires.txt +6 -0
  16. {lbkit-0.9.12 → lbkit-0.9.14}/setup.py +26 -5
  17. lbkit-0.9.12/lbkit/__commit__.py +0 -1
  18. lbkit-0.9.12/lbkit/__init__.py +0 -2
  19. {lbkit-0.9.12 → lbkit-0.9.14}/AUTHORS +0 -0
  20. {lbkit-0.9.12 → lbkit-0.9.14}/MANIFEST.in +0 -0
  21. {lbkit-0.9.12 → lbkit-0.9.14}/README.md +0 -0
  22. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/build_conan_parallel.py +0 -0
  23. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/ci_robot/__init__.py +0 -0
  24. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/.clang-format +0 -0
  25. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/__init__.py +0 -0
  26. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/codegen.py +0 -0
  27. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/ctype_defination.py +0 -0
  28. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/idf_interface.py +0 -0
  29. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/renderer.py +0 -0
  30. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/template/client.c.mako +0 -0
  31. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/template/client.h.mako +0 -0
  32. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/template/interface.introspect.xml.mako +0 -0
  33. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/template/public.h.mako +0 -0
  34. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/template/server.c.mako +0 -0
  35. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/codegen/template/server.h.mako +0 -0
  36. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/component/__init__.py +0 -0
  37. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/download_cache.py +0 -0
  38. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/errors.py +0 -0
  39. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/helper.py +0 -0
  40. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/lbkit.py +0 -0
  41. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/log.py +0 -0
  42. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/skill/__init__.py +0 -0
  43. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/skill/constants.py +0 -0
  44. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/skill/manager.py +0 -0
  45. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/__init__.py +0 -0
  46. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/config.py +0 -0
  47. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/executor.py +0 -0
  48. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/image_maker/__init__.py +0 -0
  49. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/image_maker/make_image.py +0 -0
  50. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/image_maker/make_qemu_image.py +0 -0
  51. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/image_maker/make_rockchip_image.py +0 -0
  52. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/task.py +0 -0
  53. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/task_build_image.py +0 -0
  54. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/task_build_manifest.py +0 -0
  55. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/task_build_prepare.py +0 -0
  56. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/task_build_rootfs.py +0 -0
  57. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/task_download.py +0 -0
  58. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/template/conanfile.py.mako +0 -0
  59. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tasks/template/rootfs.py.mako +0 -0
  60. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/tools.py +0 -0
  61. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/ukr/__init__.py +0 -0
  62. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/ukr/build.py +0 -0
  63. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/utils/__init__.py +0 -0
  64. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/utils/env_detector.py +0 -0
  65. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/utils/fakeroot.py +0 -0
  66. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/utils/images/__init__.py +0 -0
  67. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit/utils/images/emmc.py +0 -0
  68. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit.egg-info/dependency_links.txt +0 -0
  69. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit.egg-info/entry_points.txt +0 -0
  70. {lbkit-0.9.12 → lbkit-0.9.14}/lbkit.egg-info/top_level.txt +0 -0
  71. {lbkit-0.9.12 → lbkit-0.9.14}/setup.cfg +0 -0
  72. {lbkit-0.9.12 → lbkit-0.9.14}/test/__init__.py +0 -0
  73. {lbkit-0.9.12 → lbkit-0.9.14}/test/test_codegen.py +0 -0
  74. {lbkit-0.9.12 → lbkit-0.9.14}/test/test_config.py +0 -0
  75. {lbkit-0.9.12 → lbkit-0.9.14}/test/test_helper.py +0 -0
  76. {lbkit-0.9.12 → lbkit-0.9.14}/test/test_permission.py +0 -0
  77. {lbkit-0.9.12 → lbkit-0.9.14}/test/test_skill.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lbkit
3
- Version: 0.9.12
3
+ Version: 0.9.14
4
4
  Summary: Tools provided by litebmc.com
5
5
  Home-page: https://www.litebmc.com
6
6
  Author: xuhj@litebmc.com
@@ -22,6 +22,12 @@ Requires-Dist: inflection
22
22
  Requires-Dist: meson>=1.4.0
23
23
  Requires-Dist: psutil
24
24
  Requires-Dist: loguru
25
+ Requires-Dist: pre-commit
26
+ Requires-Dist: black
27
+ Requires-Dist: isort
28
+ Requires-Dist: flake8
29
+ Requires-Dist: flake8-bugbear
30
+ Requires-Dist: flake8-comprehensions
25
31
  Dynamic: author
26
32
  Dynamic: author-email
27
33
  Dynamic: classifier
@@ -0,0 +1 @@
1
+ __commit__ = '7fd1830'
@@ -0,0 +1,2 @@
1
+
2
+ __version__ = '0.9.14'
@@ -5,46 +5,60 @@
5
5
  # Free Software Foundation; either version 3 of the License, or (at your
6
6
  # option) any later version.
7
7
  """lbkit命令行入口"""
8
+ import argparse
8
9
  import inspect
9
10
  import os
10
11
  import signal
11
12
  import sys
12
- import argparse
13
13
  import traceback
14
+
14
15
  from lbkit import __version__ as client_version
16
+
15
17
  try:
16
18
  from lbkit.__commit__ import __commit__ as client_commit
17
19
  except ImportError:
18
20
  client_commit = ""
21
+ from lbkit import errors, misc
19
22
  from lbkit.codegen.codegen import CodeGen
23
+ from lbkit.component.arg_parser import ArgParser
20
24
  from lbkit.component.build import BuildComponent
21
25
  from lbkit.component.test import TestComponent
22
- from lbkit.tasks.executor import Executor
23
- from lbkit.component.arg_parser import ArgParser
24
26
  from lbkit.log import Logger
25
- from lbkit import misc
26
- from lbkit import errors
27
+ from lbkit.skill.manager import sync as skill_sync
28
+ from lbkit.tasks.executor import Executor
27
29
  from lbkit.ukr.build import UKRBuild
30
+ from lbkit.upgrade import upgrade as do_upgrade
28
31
  from lbkit.utils.env_detector import EnvDetector
29
- from lbkit.skill.manager import sync as skill_sync
30
32
 
31
33
  log = Logger()
32
34
 
33
35
 
36
+ def ask_user_yes_no(prompt):
37
+ """向用户询问是/否问题"""
38
+ while True:
39
+ answer = input(f"{prompt} [y/N]: ").strip().lower()
40
+ if answer in ("y", "yes"):
41
+ return True
42
+ if answer in ("n", "no", ""):
43
+ return False
44
+ print("请输入 y 或 n")
45
+
46
+
34
47
  class Command(object):
35
48
  """A single command of the lbkit application, with all the first level commands. Manages the
36
49
  parsing of parameters and delegates functionality in collaborators. It can also show the
37
50
  help of the tool.
38
51
  """
52
+
39
53
  def __init__(self):
40
54
  pass
55
+
41
56
  def help(self, *args):
42
57
  """
43
58
  Shows help for a specific command.
44
59
  """
45
60
 
46
- parser = argparse.ArgumentParser(description=self.help.__doc__,
47
- prog="lbkit help")
61
+ parser = argparse.ArgumentParser(description=self.help.__doc__, prog="lbkit help")
48
62
  parser.add_argument("command", help='command', nargs="?")
49
63
  args = parser.parse_args(*args)
50
64
  if not args.command:
@@ -115,16 +129,44 @@ class Command(object):
115
129
  """
116
130
  skill_sync()
117
131
 
132
+ def upgrade(self, *args):
133
+ """
134
+ 自升级lbkit.
135
+
136
+ 通过apt/dnf升级系统包并安装配套的PyPI包
137
+ """
138
+ do_upgrade(*args)
139
+
140
+ def hooks_path(self, *args):
141
+ """
142
+ 输出 pre-commit hooks 安装路径.
143
+
144
+ 供 CI/脚本使用,输出 hooks 目录的绝对路径
145
+ """
146
+ import os
147
+
148
+ hooks_dir = misc.HOOKS_DIR
149
+ if not os.path.isdir(hooks_dir):
150
+ # fallback: 查找源码树内的 hooks
151
+ import lbkit
152
+
153
+ pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(lbkit.__file__)))
154
+ candidate = os.path.join(pkg_dir, "pre-commit-hooks")
155
+ if os.path.isdir(candidate):
156
+ hooks_dir = candidate
157
+ print(hooks_dir)
158
+
118
159
  def _show_help(self):
119
160
  """
120
161
  Prints a summary of all commands.
121
162
  """
122
- grps = [("Code Generate commands", ["gen"]),
123
- ("Build Component commands", ["build", "test"]),
124
- ("Build Product commands", ["build"]),
125
- ("AI Skill commands", ["skill"]),
126
- ("Misc commands", ["help"]),
127
- ]
163
+ grps = [
164
+ ("Code Generate commands", ["gen"]),
165
+ ("Build Component commands", ["build", "test"]),
166
+ ("Build Product commands", ["build"]),
167
+ ("AI Skill commands", ["skill"]),
168
+ ("Misc commands", ["upgrade", "hooks_path", "help"]),
169
+ ]
128
170
 
129
171
  def check_all_commands_listed():
130
172
  """Keep updated the main directory, raise if don't"""
@@ -143,9 +185,9 @@ class Command(object):
143
185
  print(group_name + ":")
144
186
  for name in comm_names:
145
187
  # future-proof way to ensure tabular formatting
146
- output = (fmt % (misc.Color.GREEN + name + misc.Color.RESET_ALL))
188
+ output = fmt % (misc.Color.GREEN + name + misc.Color.RESET_ALL)
147
189
  if len(output) < 32:
148
- space = " "*(32 - len(output))
190
+ space = " " * (32 - len(output))
149
191
  output += space
150
192
 
151
193
  # Help will be all the lines up to the first empty one
@@ -162,15 +204,15 @@ class Command(object):
162
204
  data.append(line)
163
205
 
164
206
  import textwrap
165
- output += textwrap.fill(' '.join(data), 80, subsequent_indent=" "*(max_len+2))
207
+
208
+ output += textwrap.fill(' '.join(data), 80, subsequent_indent=" " * (max_len + 2))
166
209
  print(output)
167
210
 
168
211
  print("")
169
212
  print('LiteBmcKit commands. Type "litebmc <command> -h" for help')
170
213
 
171
214
  def _commands(self):
172
- """ Returns a list of available commands.
173
- """
215
+ """Returns a list of available commands."""
174
216
  result = {}
175
217
  for m in inspect.getmembers(self, predicate=inspect.ismethod):
176
218
  method_name = m[0]
@@ -181,13 +223,13 @@ class Command(object):
181
223
  return result
182
224
 
183
225
  def _warn_python_version(self):
184
- import textwrap
185
-
186
226
  width = 70
187
227
  version = sys.version_info
188
228
  if version.major < 3:
189
- log.info("*"*width + "\nPython 2 support has been dropped. It is strongly "
190
- "recommended to use Python >= 3.0\n" + "*"*width)
229
+ log.info(
230
+ "*" * width + "\nPython 2 support has been dropped. It is strongly "
231
+ "recommended to use Python >= 3.0\n" + "*" * width
232
+ )
191
233
 
192
234
  def run(self, *args):
193
235
  """HIDDEN: entry point for executing commands, dispatcher to class
@@ -248,6 +290,7 @@ def main(args):
248
290
  # 使能revision功能
249
291
  os.environ["CONAN_REVISIONS_ENABLED"] = "1"
250
292
  os.makedirs(misc.LOG_DIR, exist_ok=True)
293
+
251
294
  def ctrl_c_handler(_, __):
252
295
  sys.exit(-3)
253
296
 
@@ -434,7 +434,7 @@ ${name} ${name}_decode(GVariant *in)
434
434
  }
435
435
 
436
436
  const gchar *in_val = g_variant_get_string(in, NULL);
437
- for (int i = 0; i <= ${len(enum.values.parameters)}; i++) {
437
+ for (int i = 0; i < (int)G_N_ELEMENTS(_${name}StrMap); i++) {
438
438
  if (g_strcmp0(in_val, _${name}StrMap[i]) == 0) {
439
439
  return (${name})i;
440
440
  }
@@ -494,7 +494,7 @@ gboolean ${name}_validate_odf(yaml_document_t *doc, yaml_node_t *node,
494
494
  return FALSE;
495
495
  }
496
496
  const gchar *value = (const gchar *)node->data.scalar.value;
497
- for (int i = 0; i <= ${len(enum.values.parameters)}; i++) {
497
+ for (int i = 0; i < ${len(enum.values.parameters)}; i++) {
498
498
  if (g_strcmp0(value, ${name}_as_string(i)) == 0) {
499
499
  return TRUE;
500
500
  }
@@ -502,7 +502,6 @@ gboolean ${name}_validate_odf(yaml_document_t *doc, yaml_node_t *node,
502
502
  *error_list = g_slist_append(*error_list, g_error_new(ODF_ERROR, ODF_ERROR_PROP_VALIDATE_TYPE_ERROR,
503
503
  "the value of property %s is invalid, get %s", prop->str, value));
504
504
  return FALSE;
505
- return FALSE;
506
505
  }
507
506
 
508
507
  gboolean ${name}_validate_odf_v(yaml_document_t *doc, yaml_node_t *node,
@@ -1702,7 +1701,7 @@ ${name} ${name}_load_from_odf(yaml_document_t *doc, yaml_node_t *node)
1702
1701
  return _${name}_Invalid;
1703
1702
  }
1704
1703
 
1705
- for (int i = 0; i <= ${len(enum.values.parameters)}; i++) {
1704
+ for (int i = 0; i < (int)G_N_ELEMENTS(_${name}StrMap); i++) {
1706
1705
  if (g_strcmp0((const gchar *)node->data.scalar.value, ${name}_as_string(i)) == 0) {
1707
1706
  return (${name})i;
1708
1707
  }
@@ -37,6 +37,8 @@ class ArgParser():
37
37
  help=argparse.SUPPRESS)
38
38
  parser.add_argument("--cov", action="store_true",
39
39
  help=argparse.SUPPRESS)
40
+ parser.add_argument("--asan", action="store_true",
41
+ help=argparse.SUPPRESS)
40
42
  parser.add_argument("--test", action="store_true",
41
43
  help=argparse.SUPPRESS)
42
44
  parser.add_argument(
@@ -100,6 +100,8 @@ class BuildComponent():
100
100
  self.base_cmd += f" -pr:b {self.profile_build}"
101
101
  if self.options.cov:
102
102
  self.base_cmd += f" -o {self.name}/*:gcov=True"
103
+ if self.options.asan:
104
+ self.base_cmd += f" -o */*:asan=True"
103
105
  if self.options.test:
104
106
  self.base_cmd += f" -o {self.name}/*:test=True"
105
107
  for pkg_option in self.options.pkg_options:
@@ -278,7 +280,7 @@ class BuildComponent():
278
280
  tools.exec(graph_cmd, verbose=True)
279
281
  bcp = BuildConanParallel(self.orderfile, self.lockfile, self.options.remote)
280
282
  bcp.build()
281
- cmd = f"conan create {self.base_cmd} --build='{self.name}/*'"
283
+ cmd = f"conan create . {self.base_cmd} --build='{self.name}/*'"
282
284
  tools.exec(cmd, verbose=True)
283
285
 
284
286
  graph_cmd = f"conan install . {self.base_cmd} --lockfile={self.lockfile} -f json"
@@ -49,6 +49,7 @@ class LiteBmcConan(ConanFile):
49
49
  "shared": [False, True],
50
50
  % endif
51
51
  "gcov": [False, True],
52
+ "asan": [False, True],
52
53
  "test": [False, True],
53
54
  % if len(pkg.get("options", [])) > 0:
54
55
  % for op, ctx in pkg["options"].items():
@@ -61,6 +62,7 @@ class LiteBmcConan(ConanFile):
61
62
  "shared": True,
62
63
  % endif
63
64
  "gcov": False,
65
+ "asan": False,
64
66
  "test": False,
65
67
  % if len(pkg.get("options", [])) > 0:
66
68
  % for op, ctx in pkg["options"].items():
@@ -184,6 +186,10 @@ class LiteBmcConan(ConanFile):
184
186
  if self.options.gcov:
185
187
  flags.append("-fprofile-arcs")
186
188
  flags.append("-ftest-coverage")
189
+ if self.options.asan:
190
+ flags.append("-fsanitize=address")
191
+ flags.append("-fno-omit-frame-pointer")
192
+ flags.append("-g")
187
193
  if self.settings.build_type == "Release" and self.settings.arch == "armv8":
188
194
  flags.append("-D_FORTIFY_SOURCE=2")
189
195
  return flags
@@ -200,27 +206,27 @@ class LiteBmcConan(ConanFile):
200
206
  tc.variables["CMAKE_PROJECT_VERSION"] = self.version
201
207
  tc.variables["CMAKE_BUILD_TYPE"] = self.settings.build_type
202
208
  % if pkg_type == "static-library":
203
- tc.variables["BUILD_SHARED_LIBS] = False
209
+ tc.variables["BUILD_SHARED_LIBS"] = 0
204
210
  % elif pkg_type == "shared-libraries":
205
- tc.variables["BUILD_SHARED_LIBS"] = True
211
+ tc.variables["BUILD_SHARED_LIBS"] = 1
206
212
  % elif pkg_type == "library":
207
213
  if self.options.shared == False:
208
- tc.variables["BUILD_SHARED_LIBS"] = False
214
+ tc.variables["BUILD_SHARED_LIBS"] = 0
209
215
  else:
210
- tc.variables["BUILD_SHARED_LIBS"] = True
216
+ tc.variables["BUILD_SHARED_LIBS"] = 1
211
217
  % endif
212
218
  if self.options.test == True:
213
- tc.variables["BUILD_TEST"] = True
219
+ tc.variables["BUILD_TEST"] = 1
214
220
  else:
215
- tc.variables["BUILD_TEST"] = False
221
+ tc.variables["BUILD_TEST"] = 0
216
222
 
217
223
  % if len(pkg.get("options", [])) > 0:
218
224
  % for op, value in pkg["options"].items():
219
225
  % if type(value["default"]) == type(False):
220
226
  if self.options.${op}:
221
- value = True
227
+ value = 1
222
228
  else:
223
- value = False
229
+ value = 0
224
230
  % elif type(value["default"]) == type(""):
225
231
  value = str(self.options.${op})
226
232
  % elif type(value["default"]) == type(123):
@@ -234,8 +240,10 @@ class LiteBmcConan(ConanFile):
234
240
  tc.preprocessor_definitions["LB_${op.upper()}"] = value
235
241
  % endfor
236
242
  % endif
237
- tc.extra_cflags = self._append_default_flags()
238
- tc.extra_cxxflags = self._append_default_flags()
243
+ default_flags = self._append_default_flags()
244
+ tc.extra_cflags = default_flags
245
+ tc.extra_cxxflags = default_flags
246
+ tc.extra_ldflags = default_flags
239
247
 
240
248
  tc.generate()
241
249
 
@@ -17,6 +17,7 @@ class DeployConan(ConanFile):
17
17
  }
18
18
  options = {
19
19
  "gcov": [False, True],
20
+ "asan": [False, True],
20
21
  "test": [False, True],
21
22
  % if len(pkg.get("options", [])) > 0:
22
23
  % for op, ctx in pkg["options"].items():
@@ -29,6 +30,7 @@ class DeployConan(ConanFile):
29
30
  "shared": True,
30
31
  % endif
31
32
  "gcov": False,
33
+ "asan": False,
32
34
  "test": False,
33
35
  % if len(pkg.get("options", [])) > 0:
34
36
  % for op, ctx in pkg["options"].items():
@@ -62,9 +62,9 @@ class TestComponent():
62
62
  cmd = f"lcov --compat-libtool -c -q -d {build_folder} -o {coverage_dir}/cover.info"
63
63
  tool.exec(cmd)
64
64
  for dir in test_src_folder:
65
- cmd = f"lcov --compat-libtool --ignore-errors unused -r {coverage_dir}/cover.info \"{build_folder}/{dir}/*\" -o {coverage_dir}/cover.info"
65
+ cmd = f"lcov --compat-libtool --ignore-errors unused,inconsistent -r {coverage_dir}/cover.info \"{build_folder}/{dir}/*\" -o {coverage_dir}/cover.info"
66
66
  tool.exec(cmd)
67
- cmd = f"lcov --compat-libtool --ignore-errors unused -r {coverage_dir}/cover.info \"*/include/*\" -o {coverage_dir}/cover.info"
67
+ cmd = f"lcov --compat-libtool --ignore-errors unused,inconsistent -r {coverage_dir}/cover.info \"*/include/*\" -o {coverage_dir}/cover.info"
68
68
  tool.exec(cmd)
69
69
  cmd = f"genhtml -o {coverage_dir}/html --legend {coverage_dir}/cover.info"
70
70
  tool.exec(cmd)
@@ -184,6 +184,10 @@ class TestComponent():
184
184
  build = BuildComponent(self.build_parser, self.origin_args)
185
185
  build.run()
186
186
 
187
+ if hasattr(build.options, 'asan') and build.options.asan:
188
+ os.environ.setdefault("ASAN_OPTIONS", "halt_on_error=1:detect_leaks=1")
189
+ log.info("ASAN enabled, ASAN_OPTIONS=" + os.environ["ASAN_OPTIONS"])
190
+
187
191
  self._make_ld_library_path(build.rootfs_dir)
188
192
  self._make_dbus_session(build.rootfs_dir)
189
193
 
@@ -213,6 +217,11 @@ class TestComponent():
213
217
  return 0
214
218
 
215
219
  log.success("call test method...")
220
+ # 为增加可定位性,测试时将日志写入文件,等级定为debug
221
+ if os.environ.get("LOG_LEVEL") is None:
222
+ os.environ["LOG_LEVEL"] = "debug"
223
+ if os.environ.get("LOG_TYPE") is None:
224
+ os.environ["LOG_TYPE"] = "file"
216
225
  ret = test.test()
217
226
  if ret is not None:
218
227
  if isinstance(ret, unittest.TestResult) and (len(ret.errors) > 0 or len(ret.failures) > 0):
@@ -4,28 +4,31 @@
4
4
  # under the terms of the GNU General Public License as published by the
5
5
  # Free Software Foundation; either version 3 of the License, or (at your
6
6
  # option) any later version.
7
- import os
8
7
  import argparse
9
- import textwrap
8
+ import fcntl
10
9
  import json
11
- import yaml
10
+ import os
12
11
  import re
13
- import requests
14
- import fcntl
12
+ import textwrap
15
13
  from pathlib import Path
16
14
  from string import Template
15
+
16
+ import requests
17
+ import yaml
17
18
  from colorama import Fore, Style
18
- from jsonschema import validate, ValidationError
19
- from lbkit.errors import PackageConfigException, HttpRequestException
19
+ from jsonschema import ValidationError, validate
20
+
21
+ from lbkit.errors import HttpRequestException, PackageConfigException
20
22
 
21
23
  LOG_DIR = os.path.join(Path.home(), ".cache", "lbkit", "log")
22
24
  DOWNLOAD_CACHE_DIR = os.path.join(Path.home(), ".cache", "litebmc", "download")
23
25
  TARGETS_DIR = "/usr/share/litebmc/targets"
26
+ HOOKS_DIR = "/usr/share/litebmc/pre-commit-hooks"
24
27
 
25
28
 
26
29
  class Color(object):
27
- """ Wrapper around colorama colors that are undefined in importing
28
- """
30
+ """Wrapper around colorama colors that are undefined in importing"""
31
+
29
32
  RED = Fore.RED # @UndefinedVariable
30
33
  WHITE = Fore.WHITE # @UndefinedVariable
31
34
  CYAN = Fore.CYAN # @UndefinedVariable
@@ -40,9 +43,9 @@ class Color(object):
40
43
  BRIGHT_BLUE = Style.BRIGHT + Fore.BLUE # @UndefinedVariable
41
44
  BRIGHT_YELLOW = Style.BRIGHT + Fore.YELLOW # @UndefinedVariable
42
45
  BRIGHT_GREEN = Style.BRIGHT + Fore.GREEN # @UndefinedVariable
43
- BRIGHT_CYAN = Style.BRIGHT + Fore.CYAN # @UndefinedVariable
44
- BRIGHT_WHITE = Style.BRIGHT + Fore.WHITE # @UndefinedVariable
45
- BRIGHT_MAGENTA = Style.BRIGHT + Fore.MAGENTA # @UndefinedVariable
46
+ BRIGHT_CYAN = Style.BRIGHT + Fore.CYAN # @UndefinedVariable
47
+ BRIGHT_WHITE = Style.BRIGHT + Fore.WHITE # @UndefinedVariable
48
+ BRIGHT_MAGENTA = Style.BRIGHT + Fore.MAGENTA # @UndefinedVariable
46
49
 
47
50
 
48
51
  if os.environ.get("COLOR_DARK", 0):
@@ -54,8 +57,10 @@ if os.environ.get("COLOR_DARK", 0):
54
57
  Color.BRIGHT_YELLOW = Fore.MAGENTA
55
58
  Color.BRIGHT_GREEN = Fore.GREEN
56
59
 
60
+
57
61
  class SmartFormatter(argparse.HelpFormatter):
58
62
  """重写HelpFormatter"""
63
+
59
64
  def _fill_text(self, text, width, indent):
60
65
  """重写HelpFormatter"""
61
66
  text = textwrap.dedent(text)
@@ -71,6 +76,7 @@ class SmartFormatter(argparse.HelpFormatter):
71
76
  ret.extend(super()._split_lines(line, width))
72
77
  return ret
73
78
 
79
+
74
80
  def get_json_schema_file(yml_file, default_json_schema_file):
75
81
  """使用json schema文件校验yml_file配置文件"""
76
82
  with open(yml_file, "r") as fp:
@@ -80,6 +86,7 @@ def get_json_schema_file(yml_file, default_json_schema_file):
80
86
  return match.group(1)
81
87
  return default_json_schema_file
82
88
 
89
+
83
90
  def load_json_schema(schema_file):
84
91
  """使用json schema文件校验yml_file配置文件"""
85
92
  if schema_file.startswith("https://litebmc.com/"):
@@ -94,6 +101,7 @@ def load_json_schema(schema_file):
94
101
  tmp = fp.read()
95
102
  return json.loads(tmp)
96
103
 
104
+
97
105
  def load_yml_with_json_schema_validate(yml_file, default_json_schema_file, **kwargs):
98
106
  """使用json schema文件校验yml_file配置文件"""
99
107
  schema_file = get_json_schema_file(yml_file, default_json_schema_file)
@@ -110,11 +118,14 @@ def load_yml_with_json_schema_validate(yml_file, default_json_schema_file, **kwa
110
118
  validate(data, schema)
111
119
  return data
112
120
  except ValidationError as exc:
113
- raise PackageConfigException(f"validate {yml_file} failed, schema file is {schema_file}, "
114
- f"message: {exc.message}\n"
115
- "installing redhat.vscode-yaml plugin in vscode will help you write odf files")
121
+ raise PackageConfigException(
122
+ f"validate {yml_file} failed, schema file is {schema_file}, "
123
+ f"message: {exc.message}\n"
124
+ "installing redhat.vscode-yaml plugin in vscode will help you write odf files"
125
+ )
126
+
116
127
 
117
- class DownloadFlag():
128
+ class DownloadFlag:
118
129
  @staticmethod
119
130
  def clean(filename):
120
131
  """清理文件标记"""
@@ -152,4 +163,4 @@ class DownloadFlag():
152
163
  if len(content) == 0:
153
164
  return "", ""
154
165
  chunk = content.split("|")
155
- return chunk[0], chunk[1]
166
+ return chunk[0], chunk[1]
@@ -0,0 +1,326 @@
1
+ # Copyright (c) 2021-2024 litebmc.com
2
+ #
3
+ # This program is free software; you can redistribute it and/or modify it
4
+ # under the terms of the GNU General Public License as published by the
5
+ # Free Software Foundation; either version 3 of the License, or (at your
6
+ # option) any later version.
7
+ """lbkit 自升级模块"""
8
+ import os
9
+ import subprocess
10
+ import argparse
11
+ import re
12
+
13
+ from packaging.version import Version as PkgVersion
14
+
15
+ from lbkit import __version__ as current_version
16
+ from lbkit.log import Logger
17
+
18
+ log = Logger()
19
+
20
+
21
+ def _is_root():
22
+ return os.geteuid() == 0
23
+
24
+
25
+ def _get_installed_deb_version():
26
+ """获取已安装的 lbkit deb 包版本"""
27
+ result = subprocess.run(
28
+ ["dpkg-query", "-W", "-f=${Version}", "lbkit"],
29
+ capture_output=True, text=True
30
+ )
31
+ if result.returncode != 0:
32
+ return None
33
+ return result.stdout.strip() or None
34
+
35
+
36
+ def _get_installed_rpm_version():
37
+ """获取已安装的 lbkit rpm 包版本"""
38
+ result = subprocess.run(
39
+ ["rpm", "-q", "--queryformat", "%{VERSION}", "lbkit"],
40
+ capture_output=True, text=True
41
+ )
42
+ if result.returncode != 0:
43
+ return None
44
+ return result.stdout.strip() or None
45
+
46
+
47
+ def _get_apt_candidate_version():
48
+ """通过 apt-cache 获取 lbkit 的候选升级版本"""
49
+ is_root = _is_root()
50
+ sudo = [] if is_root else ["sudo"]
51
+
52
+ # apt update
53
+ log.info("正在更新软件包列表...")
54
+ subprocess.run(sudo + ["apt", "update", "-qq"],
55
+ check=True, capture_output=True)
56
+
57
+ result = subprocess.run(
58
+ ["apt-cache", "policy", "lbkit"],
59
+ capture_output=True, text=True
60
+ )
61
+ if result.returncode != 0:
62
+ return None
63
+ for line in result.stdout.splitlines():
64
+ line = line.strip()
65
+ if line.startswith("候选:") or line.startswith("Candidate:"):
66
+ version = line.split(":", 1)[-1].split(": ", 1)[-1].strip()
67
+ if version:
68
+ return version
69
+ return None
70
+
71
+
72
+ def _get_dnf_candidate_version():
73
+ """通过 dnf 获取 lbkit 的候选升级版本"""
74
+ is_root = _is_root()
75
+ sudo = [] if is_root else ["sudo"]
76
+
77
+ # dnf check-update
78
+ log.info("正在更新软件包列表...")
79
+ subprocess.run(sudo + ["dnf", "makecache", "-q"],
80
+ check=True, capture_output=True)
81
+
82
+ result = subprocess.run(
83
+ sudo + ["dnf", "list", "available", "lbkit"],
84
+ capture_output=True, text=True
85
+ )
86
+ if result.returncode != 0:
87
+ return None
88
+ for line in result.stdout.splitlines():
89
+ line = line.strip()
90
+ if line.startswith("lbkit."):
91
+ parts = line.split()
92
+ if len(parts) >= 2:
93
+ # 格式: lbkit.x86_64 0.9.12 repo_name
94
+ return parts[1]
95
+ return None
96
+
97
+
98
+ def _get_pypi_latest_version():
99
+ """查询 PyPI(或用户配置的镜像源)上 lbkit 的最新版本"""
100
+ try:
101
+ result = subprocess.run(
102
+ [subprocess.sys.executable, "-m", "pip", "index", "versions", "lbkit"],
103
+ capture_output=True, text=True, timeout=15
104
+ )
105
+ if result.returncode != 0:
106
+ log.warn(f"查询 PyPI 版本失败:\n{result.stderr}")
107
+ return None
108
+ # 输出格式: "lbkit (0.9.12)"
109
+ match = re.search(r"lbkit\s*\(([^)]+)\)", result.stdout)
110
+ if match:
111
+ return match.group(1)
112
+ return None
113
+ except Exception as e:
114
+ log.warn(f"查询 PyPI 版本失败: {e}")
115
+ return None
116
+
117
+
118
+ def _install_deb(version):
119
+ """通过 apt 安装指定版本的 deb 包"""
120
+ is_root = _is_root()
121
+ sudo = [] if is_root else ["sudo"]
122
+
123
+ installed = _get_installed_deb_version()
124
+ if installed == version:
125
+ log.info(f"deb 包已是目标版本 {version}")
126
+ return True
127
+
128
+ log.info(f"正在安装 deb 包: {installed or '未安装'} → {version}")
129
+ result = subprocess.run(
130
+ sudo + ["apt", "install", "-y", "--only-upgrade", f"lbkit={version}"],
131
+ capture_output=True, text=True
132
+ )
133
+ if result.returncode != 0:
134
+ # 指定版本不存在时回退到安装候选版本
135
+ log.warn(f"apt 源中未找到版本 {version},尝试安装候选版本")
136
+ result = subprocess.run(
137
+ sudo + ["apt", "install", "-y", "--only-upgrade", "lbkit"],
138
+ capture_output=True, text=True
139
+ )
140
+ if result.returncode != 0:
141
+ log.error(f"deb 包安装失败:\n{result.stderr}")
142
+ return False
143
+
144
+ new_version = _get_installed_deb_version()
145
+ if new_version:
146
+ log.success(f"deb 包已安装: {new_version}")
147
+ return True
148
+
149
+
150
+ def _install_rpm(version):
151
+ """通过 dnf 安装指定版本的 rpm 包"""
152
+ is_root = _is_root()
153
+ sudo = [] if is_root else ["sudo"]
154
+
155
+ installed = _get_installed_rpm_version()
156
+ if installed == version:
157
+ log.info(f"rpm 包已是目标版本 {version}")
158
+ return True
159
+
160
+ log.info(f"正在安装 rpm 包: {installed or '未安装'} → {version}")
161
+ result = subprocess.run(
162
+ sudo + ["dnf", "install", "-y", f"lbkit-{version}"],
163
+ capture_output=True, text=True
164
+ )
165
+ if result.returncode != 0:
166
+ log.error(f"rpm 包安装失败:\n{result.stderr}")
167
+ return False
168
+
169
+ new_version = _get_installed_rpm_version()
170
+ if new_version:
171
+ log.success(f"rpm 包已安装: {new_version}")
172
+ return True
173
+
174
+
175
+ def _is_deb_system():
176
+ """检测是否为 Debian/Ubuntu 系列系统"""
177
+ return os.path.isfile("/etc/debian_version")
178
+
179
+
180
+ def _is_rpm_system():
181
+ """检测是否为 RHEL/Rocky Linux/CentOS 系列系统"""
182
+ if os.path.isfile("/etc/os-release"):
183
+ with open("/etc/os-release", "r") as fp:
184
+ for line in fp:
185
+ if line.strip().startswith("ID="):
186
+ distro_id = line.strip().split("=", 1)[1].strip('"')
187
+ if distro_id in ("rocky", "centos", "almalinux", "rhel", "fedora"):
188
+ return True
189
+ if line.strip().startswith("ID_LIKE="):
190
+ id_like = line.strip().split("=", 1)[1].strip('"')
191
+ if "rhel" in id_like:
192
+ return True
193
+ return os.path.isfile("/etc/redhat-release")
194
+
195
+
196
+ def _pip_install(version):
197
+ """安装指定版本的 PyPI 包"""
198
+ is_root = _is_root()
199
+ log.info(f"正在安装 PyPI 包 lbkit=={version}...")
200
+
201
+ cmd = [subprocess.sys.executable, "-m", "pip", "install",
202
+ f"lbkit=={version}", "--break-system-packages"]
203
+ if is_root:
204
+ cmd.append("--break-system-packages")
205
+ else:
206
+ cmd.append("--user")
207
+
208
+ result = subprocess.run(cmd, capture_output=True, text=True)
209
+ if result.returncode != 0:
210
+ log.error(f"PyPI 包安装失败:\n{result.stderr}")
211
+ return False
212
+ log.success(f"PyPI 包 lbkit=={version} 安装完成")
213
+ return True
214
+
215
+
216
+ def _check_path():
217
+ """检查 ~/.local/bin 是否在 PATH 中,不在则提示用户"""
218
+ local_bin = os.path.expanduser("~/.local/bin")
219
+ path_env = os.environ.get("PATH", "")
220
+ path_dirs = path_env.split(":")
221
+
222
+ if local_bin in path_dirs:
223
+ return
224
+
225
+ log.warn(f"{local_bin} 不在 PATH 环境变量中")
226
+ log.warn("lbk 命令可能无法直接使用")
227
+
228
+ from lbkit.cli import ask_user_yes_no
229
+ if not ask_user_yes_no(f"是否将 {local_bin} 添加到 ~/.bashrc?"):
230
+ log.info(f"请手动执行: echo 'export PATH=\"{local_bin}:$PATH\"' >> ~/.bashrc")
231
+ return
232
+
233
+ bashrc = os.path.expanduser("~/.bashrc")
234
+ export_line = f'export PATH="{local_bin}:$PATH"\n'
235
+ already_in_bashrc = False
236
+ if os.path.isfile(bashrc):
237
+ with open(bashrc, "r") as f:
238
+ already_in_bashrc = local_bin in f.read()
239
+
240
+ if already_in_bashrc:
241
+ log.info(f"{local_bin} 已在 ~/.bashrc 中,可能是当前 shell 未加载")
242
+ return
243
+
244
+ with open(bashrc, "a") as f:
245
+ f.write("\n# Added by lbkit upgrade\n")
246
+ f.write(export_line)
247
+ log.success(f"已将 {local_bin} 添加到 ~/.bashrc")
248
+ log.info("请执行 source ~/.bashrc 或重新打开终端使配置生效")
249
+
250
+
251
+ def upgrade(*args):
252
+ """执行 lbkit 自升级"""
253
+ parser = argparse.ArgumentParser(
254
+ description="自升级lbkit,查询系统包管理器和PyPI可用版本后安装最新版",
255
+ prog="lbkit upgrade"
256
+ )
257
+ parser.parse_args(*args)
258
+
259
+ log.info(f"当前版本: {current_version}")
260
+
261
+ # 检测系统类型
262
+ is_deb = _is_deb_system()
263
+ is_rpm = _is_rpm_system()
264
+
265
+ if not is_deb and not is_rpm:
266
+ log.error("当前系统不是 Debian/Ubuntu 或 RHEL/Rocky Linux 系列,暂不支持自动升级")
267
+ return 1
268
+
269
+ # 1. 查询系统包管理器候选版本
270
+ sys_ver = None
271
+ if is_deb:
272
+ sys_ver = _get_apt_candidate_version()
273
+ if sys_ver:
274
+ log.info(f"apt 可用版本: {sys_ver}")
275
+ else:
276
+ log.warn("apt 源中未找到 lbkit 包")
277
+ elif is_rpm:
278
+ sys_ver = _get_dnf_candidate_version()
279
+ if sys_ver:
280
+ log.info(f"dnf 可用版本: {sys_ver}")
281
+ else:
282
+ log.warn("dnf 源中未找到 lbkit 包")
283
+
284
+ # 2. 查询 PyPI 最新版本
285
+ pypi_ver = _get_pypi_latest_version()
286
+ if pypi_ver:
287
+ log.info(f"PyPI 最新版本: {pypi_ver}")
288
+ else:
289
+ log.warn("无法查询 PyPI 版本")
290
+
291
+ # 3. 取最大版本号
292
+ versions = {}
293
+ if sys_ver:
294
+ versions["system"] = sys_ver
295
+ if pypi_ver:
296
+ versions["pypi"] = pypi_ver
297
+
298
+ if not versions:
299
+ log.error("未找到任何可安装的版本")
300
+ return 1
301
+
302
+ target = max(versions.values(), key=PkgVersion)
303
+ log.info(f"目标版本: {target}")
304
+
305
+ if PkgVersion(target) <= PkgVersion(current_version):
306
+ log.info(f"当前已是最新版本 {current_version}")
307
+ return 0
308
+
309
+ # 4. 安装系统包(如果系统包管理器有可用版本)
310
+ if sys_ver:
311
+ if is_deb:
312
+ if not _install_deb(target):
313
+ return 1
314
+ elif is_rpm:
315
+ if not _install_rpm(target):
316
+ return 1
317
+
318
+ # 5. 安装 PyPI 包
319
+ if not _pip_install(target):
320
+ return 1
321
+
322
+ # 6. 检查 PATH
323
+ _check_path()
324
+
325
+ log.success(f"升级完成: {current_version} → {target}")
326
+ return 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lbkit
3
- Version: 0.9.12
3
+ Version: 0.9.14
4
4
  Summary: Tools provided by litebmc.com
5
5
  Home-page: https://www.litebmc.com
6
6
  Author: xuhj@litebmc.com
@@ -22,6 +22,12 @@ Requires-Dist: inflection
22
22
  Requires-Dist: meson>=1.4.0
23
23
  Requires-Dist: psutil
24
24
  Requires-Dist: loguru
25
+ Requires-Dist: pre-commit
26
+ Requires-Dist: black
27
+ Requires-Dist: isort
28
+ Requires-Dist: flake8
29
+ Requires-Dist: flake8-bugbear
30
+ Requires-Dist: flake8-comprehensions
25
31
  Dynamic: author
26
32
  Dynamic: author-email
27
33
  Dynamic: classifier
@@ -13,6 +13,7 @@ lbkit/lbkit.py
13
13
  lbkit/log.py
14
14
  lbkit/misc.py
15
15
  lbkit/tools.py
16
+ lbkit/upgrade.py
16
17
  lbkit.egg-info/PKG-INFO
17
18
  lbkit.egg-info/SOURCES.txt
18
19
  lbkit.egg-info/dependency_links.txt
@@ -10,3 +10,9 @@ inflection
10
10
  meson>=1.4.0
11
11
  psutil
12
12
  loguru
13
+ pre-commit
14
+ black
15
+ isort
16
+ flake8
17
+ flake8-bugbear
18
+ flake8-comprehensions
@@ -6,17 +6,19 @@
6
6
  # option) any later version.
7
7
  import os
8
8
  import re
9
- from setuptools import setup, find_packages
9
+
10
+ from setuptools import find_packages, setup
11
+
10
12
 
11
13
  def load_version():
12
- """ Loads a file content """
13
- filename = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)),
14
- "lbkit", "__init__.py"))
14
+ """Loads a file content"""
15
+ filename = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "lbkit", "__init__.py"))
15
16
  with open(filename, "rt") as version_file:
16
17
  litebmc_init = version_file.read()
17
18
  version = re.search(r"__version__ = '([0-9.]+)'", litebmc_init).group(1)
18
19
  return version
19
20
 
21
+
20
22
  setup(
21
23
  name="lbkit",
22
24
  version=load_version(),
@@ -25,7 +27,26 @@ setup(
25
27
  description="Tools provided by litebmc.com",
26
28
  long_description="build and code generate tools",
27
29
  long_description_content_type="text/markdown",
28
- install_requires=["pyyaml", "colorama", "mako", "node-semver>=0.6.1", "jsonschema", "conan", "requests", "gitpython", "inflection", "meson>=1.4.0", "psutil", "loguru"],
30
+ install_requires=[
31
+ "pyyaml",
32
+ "colorama",
33
+ "mako",
34
+ "node-semver>=0.6.1",
35
+ "jsonschema",
36
+ "conan",
37
+ "requests",
38
+ "gitpython",
39
+ "inflection",
40
+ "meson>=1.4.0",
41
+ "psutil",
42
+ "loguru",
43
+ "pre-commit",
44
+ "black",
45
+ "isort",
46
+ "flake8",
47
+ "flake8-bugbear",
48
+ "flake8-comprehensions",
49
+ ],
29
50
  url="https://www.litebmc.com",
30
51
  packages=find_packages(),
31
52
  include_package_data=True,
@@ -1 +0,0 @@
1
- __commit__ = 'e5ea324'
@@ -1,2 +0,0 @@
1
-
2
- __version__ = '0.9.12'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes