Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix python variant of merge_config #5417

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions cmake/kconfig.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,8 @@ endif()
if(CREATE_NEW_DOTCONFIG)
execute_process(
COMMAND
${PROJECT_SOURCE_DIR}/scripts/kconfig/merge_config.sh
-m
-q
${PYTHON_EXECUTABLE}
${PROJECT_SOURCE_DIR}/scripts/kconfig/merge_config.py
-O ${PROJECT_BINARY_DIR}
${merge_config_files}
WORKING_DIRECTORY ${APPLICATION_SOURCE_DIR}
Expand Down
103 changes: 72 additions & 31 deletions scripts/kconfig/merge_config.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,91 @@
import argparse
import re


def arguments_parse():
parser = argparse.ArgumentParser()
parser.add_argument('-q', dest="quiet", action='store_true')
parser.add_argument('-m', dest='runmake', action='store_false',
help='only merge the fragments, do not execute the make command')
parser.add_argument('-n', dest='alltarget', action='store_const', const='allnoconfig', default='alldefconfig',
help='use allnoconfig instead of alldefconfig')
parser.add_argument('-r', dest='warnredun', action='store_true',
help='list redundant entries when merging fragments')
help='list redundant entries when merging fragments')
parser.add_argument('-O', dest='output_dir',
help='to put generated output files', default='.')
help='to put generated output files', default='.')
parser.add_argument('config', nargs='+')

return parser.parse_args()


def get_config_name(line):
# '# CONFIG_FOO is not set' should be treated by merge_config as a
# state like any other

is_not_set_pattern = re.compile('^# (CONFIG_[a-zA-Z0-9_]*) is not set.*')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this change a lot.

pattern = re.compile('^(CONFIG_[a-zA-Z0-9_]*)[= ].*')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has issues:

  1. It matches strings like "CONFIG_FOO did not actually receive a value".

  2. If the base file sets "# CONFIG_FOO is not set", and some other fragment sets "CONFIG_FOO=n", that gets treated like an override, even though it's redundant (and CONFIG_FOO=n should be changed to "is not set").

I think two changes are needed:

  1. this function should probably patch things up for now by converting "=n" to "is not set", then the .conf fragments should be changed to "is not set" syntax. The "patch-up" can then be removed if necessary.

  2. if the line doesn't match "CONFIG_FOO=n" exactly, the pattern should be '^(CONFIG_[a-zA-Z0-9_]*)\s*=.*' instead

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"It matches strings like "CONFIG_FOO did not actually receive a value"

That is a minor problem because that is invalid syntax, but it would be great for
usability if you were able to fix this!

If the base file sets "# CONFIG_FOO is not set", and some other fragment sets "CONFIG_FOO=n", that gets treated like an override, even though it's redundant (and CONFIG_FOO=n should be changed to "is not set").

I don't see how this affects the user, but if you can find a clean fix then why not :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a minor problem because that is invalid syntax, but it would be great for
usability if you were able to fix this!

Glad you agree it's OK to fix. It's fixed in my patch.

I don't see how this affects the user, but if you can find a clean fix then why not :)

This script prints warnings on overrides. Warnings affect users.


match = pattern.match(line)
match_is_not_set = is_not_set_pattern.match(line)

if match_is_not_set:
return match_is_not_set.group(1)
elif match:
return match.group(1)

return "" # No match

def main():
args = arguments_parse()

pattern = re.compile('^(CONFIG_[a-zA-Z0-9_]*)[= ].*')
config_values = {}

for config_file in args.config[:]:
print("Merging %s" % config_file)
with open(config_file, 'r') as file:
for line in file:
match = pattern.match(line)
if match:
config_name = match.group(1)
config_full = match.group(0)
if config_name in config_values:
if config_values[config_name] != config_full and not args.quiet:
print("Value of %s is redefined by fragment %s" % (config_name, config_file))
print("Previous value %s" % config_values[config_name])
print("new value %s" % config_full)
elif args.warnredun:
print("Value of %s is redundant by fragment %s" % (config_name, config_file))
config_values[config_name] = config_full

if args.runmake:
print("Running make not yet supported")

with open('%s/.config' % args.output_dir, 'w') as out_config:
out_config.write('\n'.join(map(lambda x: x[1], config_values.items())))
# Create a datastructure in 'conf' that looks like this:
# [
# ("CONFIG_UART", "CONFIG_UART=y"),
# ("CONFIG_ARM", "# CONFIG_ARM is not set")
# ]
# In other words [(config_name, config_line), ... ]
#
# Note that "# CONFIG_ARM is not set" is not the same as a
# comment, it has meaning (for now).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think the confusing "is not set" syntax is the "correct" syntax within kconfig.

Some data on "# CONFIG_FOO is not set" versus "CONFIG_FOO=n":

  • 465 defconfig files in Linux v4.14 use "# CONFIG_FOO is not set" syntax. defconfig files are not full .config files, but they do determine the default settings, so they are fragments that need to be merged properly.

  • In contrast, only 1 defconfig (for the unicore arch, so not exactly common) and only 38 files total in 4.14 use CONFIG_FOO=n, and almost all of those are in the rcutorture selftests. So that seems to be the "wrong" syntax for config fragments, since it isn't really used anywhere, even though people say CONFIG_FOO=n informally.

All of the following also seem to say that "is not set" is valid:

So I think that the Zephyr habit of writing CONFIG_FOO=n directly in the .conf fragments should probably be deprecated.

Don't get me wrong; I think this is an extremely annoying and confusing "feature" of Kconfig. It's even worse that CONFIG_FOO=n usage happens sometimes, even in upstream Linux. But I think it's important to preserve behavior in the Python re-implementations as long as Zephyr still uses Kconfig.

Copy link
Collaborator

@SebastianBoe SebastianBoe Dec 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. IMHO "# is not set" should eventually be deprecated in favour of "=n" for obvious reasons.

Zephyr usability trumps Linux kernel Kconfig compatibility.

EDIT: #5443

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. IMHO "# is not set" should eventually be deprecated in favour of "=n" for obvious reasons.

I can see why you say so.

There are many more people who are familiar with the Linux style than the Zephyr style, though, and I think it's "obvious" that we want to obey principle of least surprise, especially since Zephyr is a Linux Foundation project.

@nashif, what's your decision?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I may weigh in, I think that both formats should be supported and should do exactly the same:

CONFIG_BT=n
# CONFIG_BT is not set

Should both turn Bluetooth OFF if present in the .conf or defconfig files

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I've missed something, but does conf treat those 2 differently in the .config file?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To get this merged, I suggest we retain KBuild compatibilty for now, and then we can discuss the long-term Kconfig syntax at a later point.

# https://github.com/zephyrproject-rtos/zephyr/issues/5443
conf = []
for i, fragment_path in enumerate(args.config):
with open(fragment_path, "r") as f:
fragment = f.read()
fragment_list = fragment.split("\n")

fragment_conf = []
for line in fragment_list:
config_name = get_config_name(line)
if config_name:
fragment_conf.append( (config_name, line) )

if i == 0:
print("-- Using {} as base".format(fragment_path))
else:
print("-- Merging {}".format(fragment_path))

for config_name, line in fragment_conf:
for (i, (prev_config_name, prev_line)) in enumerate(list(conf)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the worst case, if there are m config fragments with n CONFIG_ lines each, this runtime is O(m * n^2). It could be O(m * n) by keeping a hash of the configs you've seen before and handling the "seen it before" test with a membership test. This can be done by using an OrderedDict from CONFIG_ to line instead of list of (CONFIG_, line) tuples.

if config_name == prev_config_name:
# The fragment is defining a config that has
# already been defined, the fragment has
# priority, so we remove the existing entry
# and then possibly issue a warning.
conf.pop(i)

is_redundant = line == prev_line
is_redefine = line != prev_line
if not args.quiet:
if is_redefine:
print("-- Value of {} is redefined by fragment {}".format(config_name, fragment_path))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be an improvement to print these to stderr

print("-- Previous value: {}".format(prev_line))
print("-- New value: {}".format(line))

if is_redundant and args.warnredun:
print("-- Value of {} is redundant by fragment {}".format(config_name, fragment_path))

conf.extend(fragment_conf)

with open('{}/.config'.format(args.output_dir), 'w') as f:
for (config_name, line) in conf:
f.write("{}\n".format(line))

if __name__ == "__main__":
main()