Repository: mesonbuild/meson

Issue:
add_project_arguments should ensure it is called before build targets only in the current project, not subprojects
If a subproject is defined, and `add_project_arguments` is called after said subproject, then meson will error out with
```
Tried to set project arguments after a build target has been declared.
This is not permitted. Please declare all project arguments before your targets.
```

This doesn't seem correct, as `add_project_arguments` differese from `add_global_arguments` specifcially in that it doesn't affect subprojects. Specfically this is annoying because it means that if a dependency needs to add define as an argument rather than as configuration_data that must be done before the subproject, making it impossible to use the fallback parameter.


Discussion:
It's talking about [build targets](https://github.com/mesonbuild/meson/wiki/Reference-manual#build_target), not subprojects. You have to do all dependency fallbacks and subproject calls before defining any build targets (executable, library, etc).
Also, for the case of a dependency adding a define as an argument, you should be using the `c_args:` keyword argument to build targets, right? The usual way to do this is to keep a list of arguments that apply to (most) of the build targets in your definitions and append to that.

`add_project_arguments()` is there for the rare case when you want args to apply to all build targets, including examples and tools.
Hmm, I see what you mean about it not applying to subprojects, sorry. It should check if build targets have been defined in the current project, not subprojects.

Resolving patch:
diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py
--- a/mesonbuild/interpreter.py
+++ b/mesonbuild/interpreter.py
@@ -1239,7 +1239,8 @@ def __init__(self, build, backend, subproject='', subdir='', subproject_dir='sub
         self.builtin.update({'meson': MesonMain(build, self)})
         self.generators = []
         self.visited_subdirs = {}
-        self.args_frozen = False
+        self.project_args_frozen = False
+        self.global_args_frozen = False  # implies self.project_args_frozen
         self.subprojects = {}
         self.subproject_stack = []
         self.default_project_options = default_project_options[:] # Passed from the outside, only used in subprojects.
@@ -1522,7 +1523,7 @@ def do_subproject(self, dirname, kwargs):
             raise InterpreterException(msg.format(os.path.join(self.subproject_dir, dirname), e))
         subdir = os.path.join(self.subproject_dir, resolved)
         os.makedirs(os.path.join(self.build.environment.get_build_dir(), subdir), exist_ok=True)
-        self.args_frozen = True
+        self.global_args_frozen = True
         mlog.log('\nExecuting subproject ', mlog.bold(dirname), '.\n', sep='')
         subi = Interpreter(self.build, self.backend, dirname, subdir, self.subproject_dir,
                            mesonlib.stringlistify(kwargs.get('default_options', [])))
@@ -2439,83 +2440,51 @@ def func_add_test_setup(self, node, args, kwargs):
 
     @stringArgs
     def func_add_global_arguments(self, node, args, kwargs):
-        if self.subproject != '':
-            msg = 'Global arguments can not be set in subprojects because ' \
-                  'there is no way to make that reliable.\nPlease only call ' \
-                  'this if is_subproject() returns false. Alternatively, ' \
-                  'define a variable that\ncontains your language-specific ' \
-                  'arguments and add it to the appropriate *_args kwarg ' \
-                  'in each target.'
-            raise InvalidCode(msg)
-        if self.args_frozen:
-            msg = 'Tried to set global arguments after a build target has ' \
-                  'been declared.\nThis is not permitted. Please declare all ' \
-                  'global arguments before your targets.'
-            raise InvalidCode(msg)
-        if 'language' not in kwargs:
-            raise InvalidCode('Missing language definition in add_global_arguments')
-        lang = kwargs['language'].lower()
-        if lang in self.build.global_args:
-            self.build.global_args[lang] += args
-        else:
-            self.build.global_args[lang] = args
+        self.add_global_arguments(node, self.build.global_args, args, kwargs)
 
     @stringArgs
     def func_add_global_link_arguments(self, node, args, kwargs):
+        self.add_global_arguments(node, self.build.global_link_args, args, kwargs)
+
+    @stringArgs
+    def func_add_project_arguments(self, node, args, kwargs):
+        self.add_project_arguments(node, self.build.projects_args, args, kwargs)
+
+    @stringArgs
+    def func_add_project_link_arguments(self, node, args, kwargs):
+        self.add_project_arguments(node, self.build.projects_link_args, args, kwargs)
+
+    def add_global_arguments(self, node, argsdict, args, kwargs):
         if self.subproject != '':
-            msg = 'Global link arguments can not be set in subprojects because ' \
+            msg = 'Function \'{}\' cannot be used in subprojects because ' \
                   'there is no way to make that reliable.\nPlease only call ' \
                   'this if is_subproject() returns false. Alternatively, ' \
                   'define a variable that\ncontains your language-specific ' \
                   'arguments and add it to the appropriate *_args kwarg ' \
-                  'in each target.'
+                  'in each target.'.format(node.func_name)
             raise InvalidCode(msg)
-        if self.args_frozen:
-            msg = 'Tried to set global link arguments after a build target has ' \
-                  'been declared.\nThis is not permitted. Please declare all ' \
-                  'global arguments before your targets.'
+        frozen = self.project_args_frozen or self.global_args_frozen
+        self.add_arguments(node, argsdict, frozen, args, kwargs)
+
+    def add_project_arguments(self, node, argsdict, args, kwargs):
+        if self.subproject not in argsdict:
+            argsdict[self.subproject] = {}
+        self.add_arguments(node, argsdict[self.subproject],
+                           self.project_args_frozen, args, kwargs)
+
+    def add_arguments(self, node, argsdict, args_frozen, args, kwargs):
+        if args_frozen:
+            msg = 'Tried to use \'{}\' after a build target has been declared.\n' \
+                  'This is not permitted. Please declare all ' \
+                  'arguments before your targets.'.format(node.func_name)
             raise InvalidCode(msg)
-        if 'language' not in kwargs:
-            raise InvalidCode('Missing language definition in add_global_link_arguments')
-        lang = kwargs['language'].lower()
-        if lang in self.build.global_link_args:
-            self.build.global_link_args[lang] += args
-        else:
-            self.build.global_link_args[lang] = args
 
-    @stringArgs
-    def func_add_project_link_arguments(self, node, args, kwargs):
-        if self.args_frozen:
-            msg = 'Tried to set project link arguments after a build target has ' \
-                  'been declared.\nThis is not permitted. Please declare all ' \
-                  'project link arguments before your targets.'
-            raise InvalidCode(msg)
         if 'language' not in kwargs:
-            raise InvalidCode('Missing language definition in add_project_link_arguments')
-        lang = kwargs['language'].lower()
-        if self.subproject not in self.build.projects_link_args:
-            self.build.projects_link_args[self.subproject] = {}
+            raise InvalidCode('Missing language definition in {}'.format(node.func_name))
 
-        args = self.build.projects_link_args[self.subproject].get(lang, []) + args
-        self.build.projects_link_args[self.subproject][lang] = args
-
-    @stringArgs
-    def func_add_project_arguments(self, node, args, kwargs):
-        if self.args_frozen:
-            msg = 'Tried to set project arguments after a build target has ' \
-                  'been declared.\nThis is not permitted. Please declare all ' \
-                  'project arguments before your targets.'
-            raise InvalidCode(msg)
-
-        if 'language' not in kwargs:
-            raise InvalidCode('Missing language definition in add_project_arguments')
-
-        if self.subproject not in self.build.projects_args:
-            self.build.projects_args[self.subproject] = {}
-
-        lang = kwargs['language'].lower()
-        args = self.build.projects_args[self.subproject].get(lang, []) + args
-        self.build.projects_args[self.subproject][lang] = args
+        for lang in mesonlib.stringlistify(kwargs['language']):
+            lang = lang.lower()
+            argsdict[lang] = argsdict.get(lang, []) + args
 
     def func_environment(self, node, args, kwargs):
         return EnvironmentVariablesHolder()
@@ -2600,7 +2569,7 @@ def build_target(self, node, args, kwargs, targetholder):
             self.add_cross_stdlib_info(target)
         l = targetholder(target, self)
         self.add_target(name, l.held_object)
-        self.args_frozen = True
+        self.project_args_frozen = True
         return l
 
     def get_used_languages(self, target):


Test patch:
diff --git a/test cases/common/123 subproject project arguments/exe.c b/test cases/common/123 subproject project arguments/exe.c
--- a/test cases/common/123 subproject project arguments/exe.c	
+++ b/test cases/common/123 subproject project arguments/exe.c	
@@ -18,6 +18,10 @@
 #error
 #endif
 
+#ifndef PROJECT_OPTION_C_CPP
+#error
+#endif
+
 int main(int argc, char **argv) {
     return 0;
 }
diff --git a/test cases/common/123 subproject project arguments/exe.cpp b/test cases/common/123 subproject project arguments/exe.cpp
--- a/test cases/common/123 subproject project arguments/exe.cpp	
+++ b/test cases/common/123 subproject project arguments/exe.cpp	
@@ -18,6 +18,10 @@
 #error
 #endif
 
+#ifndef PROJECT_OPTION_C_CPP
+#error
+#endif
+
 int main(int argc, char **argv) {
     return 0;
 }
diff --git a/test cases/common/123 subproject project arguments/meson.build b/test cases/common/123 subproject project arguments/meson.build
--- a/test cases/common/123 subproject project arguments/meson.build	
+++ b/test cases/common/123 subproject project arguments/meson.build	
@@ -4,10 +4,13 @@ project('project options tester', 'c', 'cpp',
 
 add_global_arguments('-DGLOBAL_ARGUMENT', language: 'c')
 add_project_arguments('-DPROJECT_OPTION', language: 'c')
-add_project_arguments('-DPROJECT_OPTION_1', language: 'c')
 add_project_arguments('-DPROJECT_OPTION_CPP', language: 'cpp')
+add_project_arguments('-DPROJECT_OPTION_C_CPP', language: ['c', 'cpp'])
 
 sub = subproject('subexe', version : '1.0.0')
+
+add_project_arguments('-DPROJECT_OPTION_1', language: 'c')
+
 e = executable('exe', 'exe.c')
 e = executable('execpp', 'exe.cpp')
 test('exetest', e)
diff --git a/test cases/common/123 subproject project arguments/subprojects/subexe/subexe.c b/test cases/common/123 subproject project arguments/subprojects/subexe/subexe.c
--- a/test cases/common/123 subproject project arguments/subprojects/subexe/subexe.c	
+++ b/test cases/common/123 subproject project arguments/subprojects/subexe/subexe.c	
@@ -6,6 +6,10 @@
 #error
 #endif
 
+#ifdef PROJECT_OPTION_C_CPP
+#error
+#endif
+
 #ifndef GLOBAL_ARGUMENT
 #error
 #endif
diff --git a/test cases/common/23 global arg/meson.build b/test cases/common/23 global arg/meson.build
--- a/test cases/common/23 global arg/meson.build	
+++ b/test cases/common/23 global arg/meson.build	
@@ -3,6 +3,8 @@ project('global arg test', 'cpp', 'c')
 add_global_arguments('-DMYTHING', language : 'c')
 add_global_arguments('-DMYCPPTHING', language : 'cpp')
 
+add_global_arguments('-DMYCANDCPPTHING', language: ['c', 'cpp'])
+
 exe1 = executable('prog', 'prog.c')
 exe2 = executable('prog2', 'prog.cc')
 
diff --git a/test cases/common/23 global arg/prog.c b/test cases/common/23 global arg/prog.c
--- a/test cases/common/23 global arg/prog.c	
+++ b/test cases/common/23 global arg/prog.c	
@@ -6,6 +6,10 @@
 #error "Wrong global argument set"
 #endif
 
+#ifndef MYCANDCPPTHING
+#error "Global argument not set"
+#endif
+
 int main(int argc, char **argv) {
     return 0;
 }
diff --git a/test cases/common/23 global arg/prog.cc b/test cases/common/23 global arg/prog.cc
--- a/test cases/common/23 global arg/prog.cc	
+++ b/test cases/common/23 global arg/prog.cc	
@@ -6,6 +6,10 @@
 #error "Global argument not set"
 #endif
 
+#ifndef MYCANDCPPTHING
+#error "Global argument not set"
+#endif
+
 int main(int argc, char **argv) {
     return 0;
 }