Skip to content

Commit cac1f74

Browse files
committed
docs: make SPDX-FileCopyrightText optional
According to the Berne Convention, copyright is automatic, and the license header is informative only. This adds a REUSE.toml file to silence the lint on .kt files, and an update to the docs and lint. Due to the brevity of the fix, a `LintFix` is now offered on the in-IDE lint warning Our header matches the style of the Linux Kernel. Their general rationale for making the copyright side of the header optional: - Copyright notices are not mandatory in order for the contributor to retain ownership of their copyright. - Copyright notices are rarely kept up to date as a file evolves, resulting in inaccurate statements. - Trying to keep notices up to date, or to correct notices that have become inaccurate, increases the burden on developers without tangible benefit. - Developers and maintainers often do not want to have to worry about e.g. whether a minor contribution (such as a typo fix) means that a new copyright notice should be added. - The specific individual or legal entity that owns the copyright might not be known to the contributor; it could be you, your employer, or some other entity. Note that the GPL states that per-file copyright is the safest option: > To do so, attach the following notices to the program. It is safest > to attach them to the start of each source file to most effectively > state the exclusion of warranty https://docs.kernel.org/process/license-rules.html#license-identifier-syntax https://www.linuxfoundation.org/blog/blog/copyright-notices-in-open-source-software-projects Fixes 21013
1 parent c99b6b5 commit cac1f74

6 files changed

Lines changed: 178 additions & 43 deletions

File tree

CONTRIBUTING.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,26 @@ git push origin HEAD
112112

113113
See also: [Wiki - Git - One time setup guide](https://github.com/ankidroid/Anki-Android/wiki/Development-Guide/#initial-setup-one-time)
114114

115+
### License/copyright headers
116+
117+
Files you create MUST either:
118+
1. Start with a [SPDX License ID](https://spdx.dev/learn/handling-license-info/): `// SPDX-License-Identifier: GPL-3.0-or-later`
119+
- Recommended for most files
120+
- Use `LGPL-3.0-or-later` for the API
121+
2. Start with a [long-form license](https://softwarefreedom.org/resources/2007/gpl-non-gpl-collaboration.html#x1-40002.2) including copyright.
122+
- If the license is non-GPL
123+
- Pre-2026 mono-licensed GPL files may use this form, it is not recommended for new mono-licensed GPL files.
124+
3. Follow the [`REUSE.toml` spec](https://reuse.software/spec-3.2/#reusetoml)
125+
- For new files where the above is not possible/desired (images etc...).
126+
127+
#### Copyright headers
128+
129+
Copyright headers are **optional** and must come after the license identifier line.
130+
You own the copyright of your contributions, regardless of the copyright header.
131+
132+
See [docs/contributing/copyright-headers.md](docs/contributing/copyright-headers.md) for how to
133+
apply a copyright header.
134+
115135
## Before submitting a Pull Request (PR)
116136

117137
> [!WARNING]

REUSE.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version = 1
2+
3+
# make FileCopyrightText optional on all .kt files
4+
# see docs/contributing/copyright-headers.md
5+
[[annotations]]
6+
path = "**.kt"
7+
precedence = "aggregate"
8+
SPDX-FileCopyrightText = "Contributors to the AnkiDroid project"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright headers
2+
3+
AnkiDroid uses git history as the authoritative record of authorship.
4+
You own the copyright of your contributions, regardless of the copyright header.
5+
Copyright headers are optional
6+
7+
Please contact the maintainers, or raise an issue if you have any questions/concerns.
8+
9+
## Applying a copyright header
10+
11+
You may add a copyright header to work which you have created or nontrivially modified.
12+
Your name may be a pseudonym, and the email is optional.
13+
14+
The copyright line should be added at the end of the header and it is recommended to be formatted as follows:
15+
16+
```diff
17+
// SPDX-License-Identifier: GPL-3.0-or-later
18+
// SPDX-FileCopyrightText: 2023 Existing Contributor <email@example.com>
19+
// SPDX-FileCopyrightText: 2026 New Contributor Name <email@example.com>
20+
21+
package com.ichi2.anki
22+
```
23+
24+
Alternate formats are listed: https://reuse.software/faq/#copyright-symbol
25+
26+
You may request this header be added to your work retroactively (ideally via a pull request).
27+
28+
## Collective copyright
29+
30+
The project adds a collective copyright line to `.kt` files for license-compliance tooling:
31+
32+
`SPDX-FileCopyrightText: Contributors to the AnkiDroid project`
33+
34+
This is informational only and does not affect your copyright. See [REUSE.toml](REUSE.toml) for the implementation.
35+
36+
37+
## Removing copyright headers
38+
39+
Existing copyright notices must be preserved **as-is** and **must not** be removed. The only exception is when this copyright header is your own.

lint-rules/src/main/java/com/ichi2/anki/lint/IssueRegistry.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import com.android.tools.lint.client.api.Vendor
2222
import com.android.tools.lint.detector.api.CURRENT_API
2323
import com.android.tools.lint.detector.api.Issue
2424
import com.ichi2.anki.lint.rules.AvoidAlertDialogUsage
25-
import com.ichi2.anki.lint.rules.CopyrightHeaderExists
2625
import com.ichi2.anki.lint.rules.DirectCalendarInstanceUsage
2726
import com.ichi2.anki.lint.rules.DirectDateInstantiation
2827
import com.ichi2.anki.lint.rules.DirectGregorianInstantiation
@@ -36,6 +35,7 @@ import com.ichi2.anki.lint.rules.HardcodedPreferenceKey
3635
import com.ichi2.anki.lint.rules.InvalidStringFormatDetector
3736
import com.ichi2.anki.lint.rules.JUnitNullAssertionDetector
3837
import com.ichi2.anki.lint.rules.LayoutPrefixDetector
38+
import com.ichi2.anki.lint.rules.LicenseHeaderExists
3939
import com.ichi2.anki.lint.rules.LocaleRootDetector
4040
import com.ichi2.anki.lint.rules.NonPositionalFormatSubstitutions
4141
import com.ichi2.anki.lint.rules.OpenInputStreamSafeDetector
@@ -51,7 +51,6 @@ class IssueRegistry : IssueRegistry() {
5151
// Keep this list lexicographically ordered.
5252
return listOf(
5353
LayoutPrefixDetector.ISSUE,
54-
CopyrightHeaderExists.ISSUE,
5554
DirectCalendarInstanceUsage.ISSUE,
5655
DirectDateInstantiation.ISSUE,
5756
DirectGregorianInstantiation.ISSUE,
@@ -62,6 +61,7 @@ class IssueRegistry : IssueRegistry() {
6261
DuplicateCrowdInStrings.ISSUE,
6362
HardcodedPreferenceKey.ISSUE,
6463
JUnitNullAssertionDetector.ISSUE,
64+
LicenseHeaderExists.ISSUE,
6565
LocaleRootDetector.ISSUE,
6666
PrintStackTraceUsage.ISSUE,
6767
NonPositionalFormatSubstitutions.ISSUE,

lint-rules/src/main/java/com/ichi2/anki/lint/rules/CopyrightHeaderExists.kt renamed to lint-rules/src/main/java/com/ichi2/anki/lint/rules/LicenseHeaderExists.kt

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.android.tools.lint.detector.api.Context
2121
import com.android.tools.lint.detector.api.Detector
2222
import com.android.tools.lint.detector.api.Implementation
2323
import com.android.tools.lint.detector.api.Issue
24+
import com.android.tools.lint.detector.api.LintFix
2425
import com.android.tools.lint.detector.api.Location
2526
import com.android.tools.lint.detector.api.Scope
2627
import com.android.tools.lint.detector.api.SourceCodeScanner
@@ -40,7 +41,7 @@ import java.util.regex.Pattern
4041
* @see .EXPLANATION
4142
*/
4243
@Beta
43-
class CopyrightHeaderExists :
44+
class LicenseHeaderExists :
4445
Detector(),
4546
SourceCodeScanner {
4647
companion object {
@@ -71,25 +72,26 @@ class CopyrightHeaderExists :
7172
const val ID = "MissingCopyrightHeader"
7273

7374
@VisibleForTesting
74-
const val DESCRIPTION = "All files in AnkiDroid must contain a GPLv3-compatible copyright header"
75+
const val DESCRIPTION = "All files in AnkiDroid must contain a GPLv3-compatible license identifier"
7576
private const val EXPLANATION =
7677
"All files in AnkiDroid must start with a " +
77-
"GPLv3-compatible copyright header: \n" +
78+
"GPLv3-compatible license identifier: \n" +
7879
"```" +
79-
$$"// SPDX-FileCopyrightText: $today.year Your Name <email@example.com> // name + email optional\n" +
80-
"// SPDX-License-Identifier: GPL-3.0-or-later" +
80+
$$"// SPDX-License-Identifier: GPL-3.0-or-later" +
8181
"```\n" +
8282
"The copyright header can be set in " +
8383
"`Settings - Editor - Copyright - Copyright Profiles - Add Profile - AnkiDroid`" +
84-
"or search in Settings for 'Copyright'. " +
84+
"or search in Settings for 'Copyright'. \n" +
85+
"You may optionally add your copyright to the file. " +
86+
"See https://github.com/ankidroid/Anki-Android/blob/main/docs/contributing/copyright-headers.md.\n" +
8587
"A long-form header may also be used: " +
8688
"https://github.com/ankidroid/Anki-Android/issues/8211#issuecomment-825269673\n\n" +
8789
"If the file is under a GPL-Compatible License " +
8890
"(https://www.gnu.org/licenses/license-list.en.html#GPLCompatibleLicenses) " +
8991
"then this warning may be suppressed either by adding a GPL header alongside the license " +
9092
"(https://softwarefreedom.org/resources/2007/gpl-non-gpl-collaboration.html#x1-40002.2) or by " +
9193
"adding \"//noinspection MissingCopyrightHeader <reason>\" as the first line of the file."
92-
private val implementation = Implementation(CopyrightHeaderExists::class.java, EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES))
94+
private val implementation = Implementation(LicenseHeaderExists::class.java, EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES))
9395
val ISSUE: Issue =
9496
Issue.create(
9597
ID,
@@ -128,6 +130,25 @@ class CopyrightHeaderExists :
128130
// If there is no line break, highlight the contents
129131
val endOffset = if (end == 0) contents.length else end
130132
val location: Location = Location.create(context.file, contents.subSequence(0, endOffset), 0, endOffset)
131-
context.report(ISSUE, location, DESCRIPTION)
133+
context.report(ISSUE, location, DESCRIPTION, createFix(context, location))
134+
}
135+
136+
/**
137+
* Builds a [LintFix] that prepends `// SPDX-License-Identifier` to the file
138+
*/
139+
private fun createFix(
140+
context: Context,
141+
location: Location,
142+
): LintFix {
143+
val license = if (context.project.name == "api") "LGPL" else "GPL"
144+
return LintFix
145+
.create()
146+
.name("Add SPDX-License-Identifier: $license-3.0-or-later")
147+
.replace()
148+
.range(location)
149+
.beginning()
150+
.with("// SPDX-License-Identifier: $license-3.0-or-later\n\n")
151+
.autoFix()
152+
.build()
132153
}
133154
}

lint-rules/src/test/java/com/ichi2/anki/lint/rules/CopyrightHeaderExistsTest.kt renamed to lint-rules/src/test/java/com/ichi2/anki/lint/rules/LicenseHeaderExistsTest.kt

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,20 @@
1515
*/
1616
package com.ichi2.anki.lint.rules
1717

18+
import com.android.tools.lint.checks.infrastructure.ProjectDescription
1819
import com.android.tools.lint.checks.infrastructure.TestFile.JavaTestFile.create
1920
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
2021
import com.google.common.annotations.Beta
2122
import org.intellij.lang.annotations.Language
2223
import org.junit.Assert.assertTrue
2324
import org.junit.Test
2425

25-
/** Test for [CopyrightHeaderExists] */
26+
/** Test for [LicenseHeaderExists] */
2627
@Suppress("UnstableApiUsage")
2728
@Beta
28-
class CopyrightHeaderExistsTest {
29+
class LicenseHeaderExistsTest {
2930
@Language("JAVA")
30-
private val copyrightHeader = """/*
31+
private val fileWithLicense = """/*
3132
* This program is free software; you can redistribute it and/or modify it under
3233
* the terms of the GNU General Public License as published by the Free Software
3334
* Foundation; either version 3 of the License, or (at your option) any later
@@ -42,34 +43,34 @@ class CopyrightHeaderExistsTest {
4243
*/"""
4344

4445
@Language("JAVA")
45-
private val spdxCopyrightHeader =
46-
"""// SPDX-FileCopyrightText: 2025 David Allison <david@example.com>
47-
// SPDX-License-Identifier: GPL-3.0-or-later"""
46+
private val spdxGplHeader =
47+
"// SPDX-License-Identifier: GPL-3.0-or-later"
4848

4949
@Language("JAVA")
50-
private val spdxLgplCopyrightHeader =
51-
"""// SPDX-FileCopyrightText: 2025 David Allison <david@example.com>
52-
// SPDX-License-Identifier: LGPL-3.0-or-later"""
50+
private val spdxLgplHeader =
51+
"// SPDX-License-Identifier: LGPL-3.0-or-later"
5352

5453
// invalid
5554
@Language("JAVA")
5655
private val spdxOldGplHeader =
57-
"""// SPDX-FileCopyrightText: 2025 David Allison <david@example.com>
58-
// SPDX-License-Identifier: GPL-3.0"""
56+
"// SPDX-License-Identifier: GPL-3.0"
5957

6058
@Language("JAVA")
6159
private val spdxGplOnlyHeader =
62-
"""// SPDX-FileCopyrightText: 2025 David Allison <david@example.com>
63-
// SPDX-License-Identifier: GPL-3.0-only"""
60+
"// SPDX-License-Identifier: GPL-3.0-only"
6461

6562
// invalid
6663
@Language("JAVA")
6764
private val spdxLgplOnlyHeader =
68-
"""// SPDX-FileCopyrightText: 2025 David Allison <ddavid@example.com>
69-
// SPDX-License-Identifier: LGPL-3.0"""
65+
"// SPDX-License-Identifier: LGPL-3.0"
7066

7167
@Language("JAVA")
72-
private val noCopyrightHeader =
68+
private val spdxGplAndCopyrightHeader =
69+
"""// SPDX-License-Identifier: GPL-3.0-or-later
70+
// SPDX-FileCopyrightText: 2025 David Allison <david@example.com>"""
71+
72+
@Language("JAVA")
73+
private val noHeader =
7374
"""
7475
7576
package com.ichi2.upgrade;
@@ -86,31 +87,41 @@ class CopyrightHeaderExistsTest {
8687
""".trimIndent()
8788

8889
@Test
89-
fun fileWithCopyrightHeaderPasses() {
90+
fun fileWithLicensePasses() {
91+
lint()
92+
.allowMissingSdk()
93+
.files(create(fileWithLicense))
94+
.issues(LicenseHeaderExists.ISSUE)
95+
.run()
96+
.expectClean()
97+
}
98+
99+
@Test
100+
fun fileWithSpdxLicenseHeaderPasses() {
90101
lint()
91102
.allowMissingSdk()
92-
.files(create(copyrightHeader))
93-
.issues(CopyrightHeaderExists.ISSUE)
103+
.files(create(spdxGplHeader))
104+
.issues(LicenseHeaderExists.ISSUE)
94105
.run()
95106
.expectClean()
96107
}
97108

98109
@Test
99-
fun fileWithSpdxCopyrightHeaderPasses() {
110+
fun fileWithSpdxLgplLicenseHeaderPasses() {
100111
lint()
101112
.allowMissingSdk()
102-
.files(create(spdxCopyrightHeader))
103-
.issues(CopyrightHeaderExists.ISSUE)
113+
.files(create(spdxLgplHeader))
114+
.issues(LicenseHeaderExists.ISSUE)
104115
.run()
105116
.expectClean()
106117
}
107118

108119
@Test
109-
fun fileWithSpdxLgplCopyrightHeaderPasses() {
120+
fun fileWithGplAndCopyrightHeaderPasses() {
110121
lint()
111122
.allowMissingSdk()
112-
.files(create(spdxLgplCopyrightHeader))
113-
.issues(CopyrightHeaderExists.ISSUE)
123+
.files(create(spdxGplAndCopyrightHeader))
124+
.issues(LicenseHeaderExists.ISSUE)
114125
.run()
115126
.expectClean()
116127
}
@@ -121,7 +132,7 @@ class CopyrightHeaderExistsTest {
121132
.allowMissingSdk()
122133
.allowCompilationErrors()
123134
.files(create(spdxOldGplHeader))
124-
.issues(CopyrightHeaderExists.ISSUE)
135+
.issues(LicenseHeaderExists.ISSUE)
125136
.run()
126137
.expectErrorCount(1)
127138
}
@@ -132,7 +143,7 @@ class CopyrightHeaderExistsTest {
132143
.allowMissingSdk()
133144
.allowCompilationErrors()
134145
.files(create(spdxGplOnlyHeader))
135-
.issues(CopyrightHeaderExists.ISSUE)
146+
.issues(LicenseHeaderExists.ISSUE)
136147
.run()
137148
.expectErrorCount(1)
138149
}
@@ -143,23 +154,59 @@ class CopyrightHeaderExistsTest {
143154
.allowMissingSdk()
144155
.allowCompilationErrors()
145156
.files(create(spdxLgplOnlyHeader))
146-
.issues(CopyrightHeaderExists.ISSUE)
157+
.issues(LicenseHeaderExists.ISSUE)
147158
.run()
148159
.expectErrorCount(1)
149160
}
150161

151162
@Test
152-
fun fileWithNoCopyrightHeaderFails() {
163+
fun fileWithNoLicenseHeaderFails() {
153164
lint()
154165
.allowMissingSdk()
155166
.allowCompilationErrors() // import failures
156-
.files(create(noCopyrightHeader))
157-
.issues(CopyrightHeaderExists.ISSUE)
167+
.files(create(noHeader))
168+
.issues(LicenseHeaderExists.ISSUE)
158169
.run()
159170
.expectErrorCount(1)
160171
.check({ output: String ->
161-
assertTrue(output.contains(CopyrightHeaderExists.ID))
162-
assertTrue(output.contains(CopyrightHeaderExists.DESCRIPTION))
172+
assertTrue(output.contains(LicenseHeaderExists.ID))
173+
assertTrue(output.contains(LicenseHeaderExists.DESCRIPTION))
163174
})
164175
}
176+
177+
@Test
178+
fun autofixPrependsSpdxIdentifier() {
179+
lint()
180+
.allowMissingSdk()
181+
.allowCompilationErrors()
182+
.files(create(noHeader))
183+
.issues(LicenseHeaderExists.ISSUE)
184+
.run()
185+
.expectFixDiffs(
186+
"""
187+
Autofix for src/com/ichi2/upgrade/Upgrade.java line 1: Add SPDX-License-Identifier: GPL-3.0-or-later:
188+
@@ -1 +1
189+
+ // SPDX-License-Identifier: GPL-3.0-or-later
190+
+
191+
""".trimIndent(),
192+
)
193+
}
194+
195+
@Test
196+
fun autofixInApiModuleUsesLgpl() {
197+
lint()
198+
.allowMissingSdk()
199+
.allowCompilationErrors()
200+
.projects(ProjectDescription(create(noHeader)).name("api"))
201+
.issues(LicenseHeaderExists.ISSUE)
202+
.run()
203+
.expectFixDiffs(
204+
"""
205+
Autofix for src/com/ichi2/upgrade/Upgrade.java line 1: Add SPDX-License-Identifier: LGPL-3.0-or-later:
206+
@@ -1 +1
207+
+ // SPDX-License-Identifier: LGPL-3.0-or-later
208+
+
209+
""".trimIndent(),
210+
)
211+
}
165212
}

0 commit comments

Comments
 (0)